refactor: Projektstruktur in app/ und backend/ aufgeteilt

This commit is contained in:
2026-03-24 15:06:44 +01:00
parent 5f5d5995c5
commit 2673ef658d
449 changed files with 28551 additions and 167 deletions

173
app/lib/app_state.dart Normal file
View File

@@ -0,0 +1,173 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'models/job.dart';
import 'services/database_service.dart';
import 'services/dart_mq.dart';
import 'l10n/app_localizations.dart';
/// Global notifier for language changes
final ValueNotifier<Locale> localeNotifier = ValueNotifier<Locale>(const Locale('de'));
class AppState {
static final AppState _instance = AppState._internal();
factory AppState() => _instance;
AppState._internal();
String? _loggedInEmail;
List<Job> _assignedJobs = [];
final DatabaseService _databaseService = DatabaseService();
// Language settings
String _languageCode = 'de';
String get languageCode => _languageCode;
/// Get current locale
Locale get currentLocale => Locale(_languageCode);
/// Set language and update the global notifier
Future<void> setLanguage(String languageCode) async {
if (supportedLanguageCodes.contains(languageCode)) {
_languageCode = languageCode;
await _databaseService.saveLanguagePreference(languageCode);
localeNotifier.value = Locale(languageCode);
}
}
/// Load language preference from database
Future<void> loadLanguagePreference() async {
final savedLanguage = await _databaseService.loadLanguagePreference();
if (savedLanguage != null && supportedLanguageCodes.contains(savedLanguage)) {
_languageCode = savedLanguage;
localeNotifier.value = Locale(savedLanguage);
}
}
// Serialize persistence to avoid overlapping DB save/load cycles
bool _isPersistingJobs = false;
List<Job>? _pendingJobs; // holds the latest jobs to persist if calls overlap
// Jobs update notification (emitted once after DB was updated)
final StreamController<void> _jobsUpdatedController = StreamController<void>.broadcast();
Stream<void> get jobsUpdated => _jobsUpdatedController.stream;
/// The logged-in user's email (used as local identifier for chats)
String? get loggedInEmail => _loggedInEmail;
List<Job> get assignedJobs => List.unmodifiable(_assignedJobs);
void setLoggedInEmail(String email) {
_loggedInEmail = email;
}
Future<void> clearLogin() async {
_loggedInEmail = null;
_assignedJobs.clear();
// Clear database
await _databaseService.clearAllData();
// Notify listeners/UI that jobs were cleared
_jobsUpdatedController.add(null);
DartMQ().publish<void>(MQTopics.jobsUpdated, null);
}
bool get isLoggedIn => _loggedInEmail != null;
Future<void> setAssignedJobs(List<Job> jobs) async {
// Coalesce overlapping calls: if a persist is already running, remember only the latest
if (_isPersistingJobs) {
_pendingJobs = jobs;
return;
}
_isPersistingJobs = true;
try {
// Start with the initial batch to persist
var toPersist = jobs;
while (true) {
// Normalize first
final normalized = toPersist.map((j) => j.normalized()).toList();
// Persist normalized list to DB only (no UI notifications here)
await _databaseService.saveJobs(normalized);
// If another request came in during persistence, handle only the latest once
if (_pendingJobs != null) {
toPersist = _pendingJobs!;
_pendingJobs = null;
continue;
}
break;
}
// After DB is updated with the latest data, notify listeners once to refresh UI
_jobsUpdatedController.add(null);
// Also publish via dart_mq for app-wide decoupled messaging
DartMQ().publish<void>(MQTopics.jobsUpdated, null);
} finally {
_isPersistingJobs = false;
}
}
void addJob(Job job) {
if (!_assignedJobs.contains(job)) {
_assignedJobs.add(job);
}
}
void removeJob(String jobId) {
_assignedJobs.removeWhere((job) => job.id == jobId);
// Update database
_databaseService.saveJobs(_assignedJobs);
}
/// Delete a job by ID (called when server sends job_deleted event)
Future<void> deleteJob(String jobId) async {
_assignedJobs.removeWhere((job) => job.id == jobId);
// Delete from database
await _databaseService.deleteJob(jobId);
// Notify listeners
_jobsUpdatedController.add(null);
DartMQ().publish<void>(MQTopics.jobsUpdated, null);
}
/// Add a new job (called when server sends job_created event)
Future<void> addNewJob(Job job) async {
// Check if job already exists
if (_assignedJobs.any((j) => j.id == job.id)) {
return;
}
// Add to memory
_assignedJobs.insert(0, job);
// Persist to database
await _databaseService.saveOrUpdateJob(job);
// Notify listeners
_jobsUpdatedController.add(null);
DartMQ().publish<void>(MQTopics.jobsUpdated, null);
}
/// Load login state from saved credentials on app start
Future<void> loadLoginFromDatabase() async {
final credentials = await _databaseService.loadCredentials();
if (credentials != null) {
_loggedInEmail = credentials.email;
}
}
void updateJob(Job updatedJob) {
final index = _assignedJobs.indexWhere((job) => job.id == updatedJob.id);
if (index != -1) {
_assignedJobs[index] = updatedJob;
}
}
/// Refresh in-memory jobs from the database without emitting other notifications
Future<void> refreshJobsFromDatabase() async {
final jobs = await _databaseService.loadJobs();
_assignedJobs = jobs;
}
/// Persistently upsert a single job and refresh in-memory list
Future<void> upsertJob(Job job) async {
await _databaseService.saveOrUpdateJob(job);
final persisted = await _databaseService.loadJobs();
_assignedJobs = persisted.isNotEmpty ? persisted : _assignedJobs;
}
}

View File

@@ -0,0 +1,437 @@
import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart';
import 'models/delivery_station.dart';
import 'models/job.dart';
import 'services/database_service.dart';
import 'task_view.dart';
import 'widgets/offline_banner.dart';
@visibleForTesting
Color? deliveryStationCardBackgroundColor(
DeliveryStation station,
Map<String, bool> taskStatuses,
) {
if (station.tasks.isEmpty) {
return null;
}
final isCompleted = station.tasks.every(
(task) => taskStatuses[task.id] ?? task.completed,
);
return isCompleted ? Colors.green[50] : null;
}
class CargoItemsView extends StatefulWidget {
final Job job;
const CargoItemsView({super.key, required this.job});
@override
State<CargoItemsView> createState() => _CargoItemsViewState();
}
class _CargoItemsViewState extends State<CargoItemsView> {
final DatabaseService _databaseService = DatabaseService();
Map<String, bool> _taskStatuses = const {};
@override
void initState() {
super.initState();
_loadLocalTaskStatuses();
}
Future<void> _loadLocalTaskStatuses() async {
final map = await _databaseService.loadAllTaskStatuses();
if (!mounted) return;
setState(() {
_taskStatuses = map;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.job.jobNumber),
backgroundColor: Colors.deepPurple[100],
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
},
),
actions: [
IconButton(
icon: const Icon(Icons.chat),
onPressed: () {
Navigator.of(context).pushNamed('/chats');
},
tooltip: AppLocalizations.of(context).openChat,
),
],
),
body: Column(
children: [
OfflineBanner(),
// Main content area
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Job summary card
Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.job.jobNumber.isNotEmpty
? widget.job.jobNumber
: widget.job.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
if (widget.job.customerSelection.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
widget.job.customerSelection,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
],
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.arrow_upward,
size: 16,
color: Colors.green[600],
),
const SizedBox(width: 4),
Text(
widget.job.pickupCity,
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
),
),
const SizedBox(width: 8),
Icon(
Icons.arrow_forward,
size: 16,
color: Colors.grey[600],
),
const SizedBox(width: 8),
Icon(
Icons.arrow_downward,
size: 16,
color: Colors.blue[600],
),
const SizedBox(width: 4),
Text(
widget.job.deliveryCitiesDisplay.isNotEmpty
? widget.job.deliveryCitiesDisplay
: widget.job.deliveryCity,
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
),
),
],
),
],
),
),
),
// Delivery stations section header
Row(
children: [
Icon(
Icons.local_shipping_outlined,
size: 24,
color: Colors.deepPurple[600],
),
const SizedBox(width: 8),
Text(
'Lieferstationen (${_deliveryStations.length})',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Expanded(child: _buildDeliveryStationsList()),
],
),
),
),
],
),
);
}
List<DeliveryStation> get _deliveryStations {
if (widget.job.deliveryStations.isNotEmpty) {
return widget.job.deliveryStations;
}
return [
DeliveryStation(
stationOrder: 0,
company: widget.job.deliveryCompany,
salutation: widget.job.deliverySalutation,
firstName: widget.job.deliveryFirstName,
lastName: widget.job.deliveryLastName,
phone: widget.job.deliveryPhone,
street: widget.job.deliveryStreet,
houseNumber: widget.job.deliveryHouseNumber,
addressAddition: widget.job.deliveryAddressAddition,
zip: widget.job.deliveryZip,
city: widget.job.deliveryCity,
deliveryDate: widget.job.deliveryDate,
deliveryTime: widget.job.deliveryTime,
tasks: widget.job.tasks,
),
];
}
Widget _buildDeliveryStationsList() {
if (_deliveryStations.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.local_shipping_outlined,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Keine Lieferstationen',
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
'Dieser Job enthält aktuell keine Lieferstationen.',
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
textAlign: TextAlign.center,
),
],
),
);
}
return ListView.builder(
itemCount: _deliveryStations.length,
itemBuilder: (context, index) {
final station = _deliveryStations[index];
return _buildDeliveryStationCard(station);
},
);
}
Widget _buildDeliveryStationCard(DeliveryStation station) {
final backgroundColor = deliveryStationCardBackgroundColor(
station,
_taskStatuses,
);
final title =
station.displayName.isNotEmpty ? station.displayName : station.company;
final subtitle =
station.company.isNotEmpty && station.company != title
? station.company
: null;
final addressLines =
<String>[
[
station.street,
station.houseNumber,
].where((part) => part.trim().isNotEmpty).join(' '),
if (station.addressAddition.trim().isNotEmpty)
station.addressAddition,
[
station.zip,
station.city,
].where((part) => part.trim().isNotEmpty).join(' '),
].where((line) => line.trim().isNotEmpty).toList();
return Card(
color: backgroundColor,
margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.grey[300]!, width: 1),
),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => TaskView(
job: widget.job,
stationOrder: station.stationOrder,
stationTitle:
station.displayName.isNotEmpty
? station.displayName
: 'Station ${station.stationOrder + 1}',
),
),
);
await _loadLocalTaskStatuses();
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.deepPurple[100],
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Station ${station.stationOrder + 1}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.deepPurple[700],
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title.isNotEmpty ? title : 'Unbenannte Station',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
fontSize: 13,
color: Colors.grey[700],
),
),
],
],
),
),
],
),
const SizedBox(height: 12),
_buildDetailItem(
Icons.location_on_outlined,
AppLocalizations.of(context).location,
addressLines.join('\n'),
Colors.blue,
),
if (station.phone.trim().isNotEmpty) ...[
const SizedBox(height: 12),
_buildDetailItem(
Icons.phone_outlined,
'Telefon',
station.phone,
Colors.green,
),
],
if (station.deliveryDate.trim().isNotEmpty ||
station.deliveryTime.trim().isNotEmpty) ...[
const SizedBox(height: 12),
_buildDetailItem(
Icons.schedule,
AppLocalizations.of(context).delivery,
[
station.deliveryDate,
station.deliveryTime,
].where((part) => part.trim().isNotEmpty).join(' '),
Colors.orange,
),
],
const SizedBox(height: 12),
_buildDetailItem(
Icons.task_alt,
AppLocalizations.of(context).tasks,
'${station.tasks.length}',
Colors.deepPurple,
),
],
),
),
),
);
}
Widget _buildDetailItem(
IconData icon,
String label,
String value,
Color color,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 16, color: color.withValues(alpha: 0.8)),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,692 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'l10n/app_localizations.dart';
import 'app_state.dart';
import 'models/chat.dart';
import 'models/chat_message.dart';
import 'services/chat_service.dart';
import 'services/websocket_service.dart';
import 'services/notification_service.dart';
import 'widgets/chat_photo_dialog.dart';
import 'widgets/offline_banner.dart';
class ChatDetailsView extends StatefulWidget {
final Chat chat;
const ChatDetailsView({super.key, required this.chat});
@override
State<ChatDetailsView> createState() => _ChatDetailsViewState();
}
class _PreparedImage {
const _PreparedImage({required this.base64DataUri, required this.bytes});
final String base64DataUri;
final Uint8List bytes;
}
class _ChatDetailsViewState extends State<ChatDetailsView> {
final TextEditingController _messageController = TextEditingController();
final ScrollController _scrollController = ScrollController();
late List<ChatMessage> _messages;
final WebSocketService _webSocketService = WebSocketService();
StreamSubscription<List<Chat>>? _chatsStreamSubscription;
String? _currentUserId;
late final String _conversationKey;
final ChatService _chatService = ChatService();
final Map<String, Uint8List> _imageCache = <String, Uint8List>{};
late Chat _activeChat;
static const int _maxDisplayMessages = 30;
@override
void initState() {
super.initState();
_activeChat = widget.chat;
_conversationKey = _activeChat.id;
NotificationService().activeConversationKey = _conversationKey;
_messages = _lastMessages(_activeChat.messages);
_currentUserId = AppState().loggedInEmail;
_chatsStreamSubscription = _chatService.chatsStream.listen(
_handleChatsUpdate,
);
_chatService.initialize().then((_) async {
_syncActiveChatFromService(replaceMessages: _messages.isEmpty);
final history = await _chatService.loadMessagesForChat(_conversationKey);
if (!mounted) return;
if (history.isNotEmpty) {
setState(() {
_imageCache.clear();
_messages = _lastMessages(history);
});
_scrollToBottom(immediate: true);
}
_syncActiveChatFromService();
await _chatService.markConversationRead(_conversationKey);
});
// Scroll to bottom after initial build is complete
_scrollToBottom(immediate: true);
}
@override
void dispose() {
if (NotificationService().activeConversationKey == _conversationKey) {
NotificationService().activeConversationKey = null;
}
_chatsStreamSubscription?.cancel();
_messageController.dispose();
_scrollController.dispose();
super.dispose();
}
void _scrollToBottom({bool immediate = false}) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_scrollController.hasClients) {
return;
}
final target = _scrollController.position.maxScrollExtent;
if (immediate) {
_scrollController.jumpTo(target);
} else {
_scrollController.animateTo(
target,
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
);
}
});
}
void _handleChatsUpdate(List<Chat> chats) {
if (!mounted) {
return;
}
final updated = _findChatById(chats);
if (updated == null) {
return;
}
final shouldReplace = _shouldReplaceMessages(updated);
setState(() {
_activeChat = updated;
if (shouldReplace) {
_imageCache.clear();
_messages = _lastMessages(updated.messages);
}
});
if (shouldReplace) {
_scrollToBottom();
unawaited(_chatService.markConversationRead(_conversationKey));
}
}
bool _shouldReplaceMessages(Chat chat) {
if (chat.messages.length != _messages.length) {
return true;
}
if (_messages.isEmpty) {
return false;
}
final currentLast = _messages.last;
final updatedLast = chat.messages.last;
return currentLast.id != updatedLast.id ||
currentLast.content != updatedLast.content ||
currentLast.contentType != updatedLast.contentType;
}
List<ChatMessage> _lastMessages(List<ChatMessage> messages) {
final sorted = List<ChatMessage>.from(messages)
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
if (sorted.length > _maxDisplayMessages) {
return sorted.sublist(sorted.length - _maxDisplayMessages);
}
return sorted;
}
Chat? _findChatById(List<Chat> chats) {
for (final chat in chats) {
if (chat.id == _conversationKey) {
return chat;
}
}
return null;
}
void _syncActiveChatFromService({bool replaceMessages = false}) {
final updated = _findChatById(_chatService.currentChats);
if (updated == null || !mounted) {
return;
}
final shouldReplace = replaceMessages || _shouldReplaceMessages(updated);
setState(() {
_activeChat = updated;
if (shouldReplace) {
_imageCache.clear();
_messages = _lastMessages(updated.messages);
}
});
if (shouldReplace) {
_scrollToBottom();
unawaited(_chatService.markConversationRead(_conversationKey));
}
}
Future<void> _sendMessage() async {
final text = _messageController.text.trim();
if (text.isEmpty) {
return;
}
final sender = _currentUserId;
final receiver = _activeChat.receiver;
if (sender == null || sender.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).noSenderMessage),
),
);
}
return;
}
if (receiver == null || receiver.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).noRecipientMessage),
),
);
}
return;
}
final result = await _webSocketService.sendChatMessage(
sender: sender,
receiver: receiver,
content: text,
jobId: _activeChat.jobId,
jobNumber: _activeChat.jobNumber,
);
if (result == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).messageSendError),
),
);
}
return;
}
await _chatService.saveOutgoingMessage(result);
_syncActiveChatFromService();
_messageController.clear();
_scrollToBottom();
}
@override
Widget build(BuildContext context) {
final isJobChat = _activeChat.type == ChatType.jobSpecific;
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_activeChat.title, style: const TextStyle(fontSize: 16)),
if (isJobChat && _activeChat.jobNumber != null)
Text(
'Job-Nr: ${_activeChat.jobNumber}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.normal,
),
),
],
),
backgroundColor: Colors.deepPurple[100],
actions: [
IconButton(
icon: Icon(isJobChat ? Icons.work : Icons.support_agent),
onPressed: () {
// Show chat info
_showChatInfo();
},
tooltip: AppLocalizations.of(context).chatInfo,
),
],
),
body: Column(
children: [
const OfflineBanner(),
// Messages list
Expanded(
child: Container(
decoration: BoxDecoration(color: Colors.grey[50]),
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.fromLTRB(8, 8, 8, 96),
itemCount: _messages.length,
itemBuilder: (context, index) {
final message = _messages[index];
return _buildMessageBubble(message);
},
),
),
),
// Message input
_buildMessageInput(),
],
),
);
}
Widget _buildMessageBubble(ChatMessage message) {
final isOwn = message.isOwn;
final isImage = message.contentType == ChatContentType.image;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment:
isOwn ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
if (!isOwn) const SizedBox(width: 8),
Flexible(
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
margin: EdgeInsets.only(
left: isOwn ? 40 : 0,
right: isOwn ? 0 : 40,
),
padding: EdgeInsets.symmetric(
horizontal: isImage ? 6 : 12,
vertical: isImage ? 6 : 8,
),
decoration: BoxDecoration(
color: isOwn ? Colors.deepPurple[100] : Colors.white,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(12),
topRight: const Radius.circular(12),
bottomLeft: Radius.circular(isOwn ? 12 : 4),
bottomRight: Radius.circular(isOwn ? 4 : 12),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMessageContent(message, isImage: isImage),
const SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
_formatMessageTime(message.createdAt),
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
if (isOwn) ...[
const SizedBox(width: 4),
Icon(
message.pendingSync
? Icons.schedule
: (message.read ? Icons.done_all : Icons.done),
size: 14,
color:
message.pendingSync
? Colors.orange[700]
: (message.read
? Colors.deepPurple[400]
: Colors.grey[600]),
),
],
],
),
],
),
),
),
if (isOwn) const SizedBox(width: 8),
],
),
);
}
Widget _buildMessageContent(ChatMessage message, {required bool isImage}) {
if (!isImage) {
return Text(
message.content,
style: TextStyle(fontSize: 15, color: Colors.grey[800]),
);
}
final imageBytes = _imageCache[message.id] ?? _decodeImageBytes(message);
if (imageBytes == null) {
return const Text(
'Bild konnte nicht geladen werden.',
style: TextStyle(fontSize: 15),
);
}
return GestureDetector(
onTap: () => _showImagePreview(imageBytes),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
color: Colors.black.withValues(alpha: 0.05),
constraints: const BoxConstraints(maxWidth: 260, minWidth: 140),
child: AspectRatio(
aspectRatio: 4 / 3,
child: Image.memory(imageBytes, fit: BoxFit.cover),
),
),
),
);
}
Uint8List? _decodeImageBytes(ChatMessage message) {
final rawContent = message.content.trim();
if (rawContent.isEmpty) {
return null;
}
final base64Payload =
rawContent.startsWith('data:')
? rawContent.substring(rawContent.indexOf(',') + 1)
: rawContent;
final normalized = base64Payload.replaceAll(RegExp(r'\s'), '');
try {
final bytes = base64Decode(normalized);
_imageCache[message.id] = bytes;
return bytes;
} catch (_) {
return null;
}
}
Future<void> _showImagePreview(Uint8List imageBytes) async {
if (!mounted) return;
await showDialog<void>(
context: context,
builder: (context) {
return Dialog(
insetPadding: const EdgeInsets.all(16),
backgroundColor: Colors.black,
child: InteractiveViewer(
child: Image.memory(imageBytes, fit: BoxFit.contain),
),
);
},
);
}
Widget _buildMessageInput() {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey[300]!)),
),
child: SafeArea(
child: Row(
children: [
GestureDetector(
onTap: _handleAttachmentTap,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.attach_file,
color: Colors.black87,
size: 20,
),
),
),
const SizedBox(width: 8),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(20),
),
child: TextField(
controller: _messageController,
decoration: InputDecoration(
hintText: AppLocalizations.of(context).typeMessage,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
border: InputBorder.none,
),
maxLines: null,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendMessage(),
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
_sendMessage();
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.deepPurple,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Icons.send, color: Colors.white, size: 20),
),
),
],
),
),
);
}
Future<void> _handleAttachmentTap() async {
if (!mounted) return;
final Uint8List? photoBytes = await showDialog<Uint8List>(
context: context,
builder: (context) => const ChatPhotoDialog(),
);
if (photoBytes == null || photoBytes.isEmpty) {
return;
}
await _sendImageMessage(photoBytes);
}
Future<void> _sendImageMessage(Uint8List imageBytes) async {
final sender = _currentUserId;
final receiver = _activeChat.receiver;
if (sender == null || sender.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).noSenderMessage),
),
);
}
return;
}
if (receiver == null || receiver.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).noRecipientMessage),
),
);
}
return;
}
final prepared = await _prepareImagePayload(imageBytes);
if (prepared == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).photoProcessError),
),
);
}
return;
}
final result = await _webSocketService.sendChatMessage(
sender: sender,
receiver: receiver,
content: prepared.base64DataUri,
contentType: ChatContentType.image,
jobId: _activeChat.jobId,
jobNumber: _activeChat.jobNumber,
);
if (result == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).imageSendError)),
);
}
return;
}
await _chatService.saveOutgoingMessage(result);
_syncActiveChatFromService();
if (prepared.bytes.isNotEmpty) {
_imageCache[result.id] = prepared.bytes;
}
}
Future<_PreparedImage?> _prepareImagePayload(Uint8List originalBytes) async {
try {
final decoded = img.decodeImage(originalBytes);
if (decoded == null) {
return null;
}
final baked = img.bakeOrientation(decoded);
const maxDimension = 1280;
img.Image processed = baked;
if (baked.width > maxDimension || baked.height > maxDimension) {
final scale =
baked.width > baked.height
? maxDimension / baked.width
: maxDimension / baked.height;
final targetWidth = (baked.width * scale).round();
final targetHeight = (baked.height * scale).round();
processed = img.copyResize(
baked,
width: targetWidth,
height: targetHeight,
interpolation: img.Interpolation.average,
);
}
final encodedBytes = Uint8List.fromList(
img.encodeJpg(processed, quality: 85),
);
final base64Payload = base64Encode(encodedBytes);
final dataUri = 'data:image/jpeg;base64,$base64Payload';
return _PreparedImage(base64DataUri: dataUri, bytes: encodedBytes);
} catch (_) {
return null;
}
}
String _formatMessageTime(DateTime dateTime) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final messageDate = DateTime(dateTime.year, dateTime.month, dateTime.day);
if (messageDate == today) {
// Today - show only time
return '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} else if (messageDate == today.subtract(const Duration(days: 1))) {
// Yesterday
return 'Gestern ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} else {
// Older - show date and time
return '${dateTime.day.toString().padLeft(2, '0')}.${dateTime.month.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
}
void _showChatInfo() {
final isJobChat = _activeChat.type == ChatType.jobSpecific;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(_activeChat.title),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${AppLocalizations.of(context).status}: ${isJobChat ? AppLocalizations.of(context).chatTypeJob : AppLocalizations.of(context).chatTypeGeneral}'),
const SizedBox(height: 8),
if (isJobChat && _activeChat.jobNumber != null) ...[
Text('${AppLocalizations.of(context).jobNumber}: ${_activeChat.jobNumber}'),
const SizedBox(height: 8),
],
Text('${AppLocalizations.of(context).messages}: ${_messages.length}'),
const SizedBox(height: 8),
Text(
'Erstellt: ${_formatMessageTime(_messages.isNotEmpty ? _messages.first.createdAt : DateTime.now())}',
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(context).close),
),
],
);
},
);
}
}

186
app/lib/chats_view.dart Normal file
View File

@@ -0,0 +1,186 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart';
import 'models/chat.dart';
import 'services/chat_service.dart';
import 'widgets/offline_banner.dart';
class ChatsView extends StatefulWidget {
const ChatsView({super.key});
@override
State<ChatsView> createState() => _ChatsViewState();
}
class _ChatsViewState extends State<ChatsView> {
final ChatService _chatService = ChatService();
List<Chat> _chats = const [];
StreamSubscription<List<Chat>>? _chatSubscription;
bool _isInitializing = true;
@override
void initState() {
super.initState();
_initializeChats();
}
Future<void> _initializeChats() async {
await _chatService.initialize();
if (!mounted) return;
setState(() {
_chats = _chatService.currentChats;
_isInitializing = false;
});
_chatSubscription = _chatService.chatsStream.listen((chats) {
if (!mounted) return;
setState(() {
_chats = chats;
});
});
}
@override
void dispose() {
_chatSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).chats),
backgroundColor: Colors.deepPurple[100],
),
body: Column(
children: [
const OfflineBanner(),
Expanded(child: _buildBody()),
],
),
);
}
Widget _buildBody() {
if (_isInitializing) {
return const Center(child: CircularProgressIndicator());
}
if (_chats.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.chat_outlined, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'Keine Chats verfügbar',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
);
}
return ListView.builder(
itemCount: _chats.length,
itemBuilder: (context, index) {
final chat = _chats[index];
return _buildChatTile(chat);
},
);
}
Widget _buildChatTile(Chat chat) {
final isJobChat = chat.type == ChatType.jobSpecific;
final hasMessages = chat.messages.isNotEmpty;
final previewText =
hasMessages ? chat.lastMessagePreview : 'Noch keine Nachrichten';
final timeLabel = hasMessages ? _formatTime(chat.lastMessageTime) : '--';
final jobId = chat.jobId?.trim();
final jobNumber = chat.jobNumber?.trim();
final showJobId = isJobChat && jobId != null && jobId.isNotEmpty;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: isJobChat ? Colors.blue[100] : Colors.green[100],
child: Icon(
isJobChat ? Icons.work : Icons.support_agent,
color: isJobChat ? Colors.blue[700] : Colors.green[700],
),
),
title: Text(() {
if (isJobChat) {
if (jobNumber != null && jobNumber.isNotEmpty) {
return 'Job $jobNumber';
}
if (showJobId) {
return 'Job $jobId';
}
}
return chat.type == ChatType.general
? 'Allgemeine Nachrichten'
: chat.title;
}(), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
subtitle: Text(
previewText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
),
trailing: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
timeLabel,
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isJobChat ? Colors.blue[50] : Colors.green[50],
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isJobChat ? Colors.blue[200]! : Colors.green[200]!,
),
),
child: Text(
isJobChat ? 'JOB' : 'ALLG',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: isJobChat ? Colors.blue[700] : Colors.green[700],
),
),
),
],
),
onTap: () {
Navigator.of(context).pushNamed('/chat_details', arguments: chat);
},
),
);
}
String _formatTime(DateTime dateTime) {
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inDays > 0) {
return '${difference.inDays}T';
} else if (difference.inHours > 0) {
return '${difference.inHours}h';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}m';
} else {
return 'jetzt';
}
}
}

View File

@@ -0,0 +1,32 @@
enum TranslationBackend { lmStudio, moonshot }
class TranslationConfig {
TranslationConfig._();
/// Das aktive Übersetzungs-Backend.
/// Hier umschalten zwischen LM Studio (lokal) und Moonshot AI (Cloud).
static const TranslationBackend activeBackend = TranslationBackend.moonshot;
// ---------------------------------------------------------------------------
// LM Studio (lokales Modell)
// ---------------------------------------------------------------------------
/// Basis-URL des LM Studio REST-Servers (lokales Netzwerk)
static const String lmStudioBaseUrl = 'http://lmstudio.appcreation.de';
/// Modellname LM Studio ignoriert diesen Wert normalerweise
static const String lmStudioModel = 'local-model';
// ---------------------------------------------------------------------------
// Moonshot AI (Kimi Cloud API)
// ---------------------------------------------------------------------------
/// Basis-URL der Moonshot AI API
static const String moonshotBaseUrl = 'https://api.moonshot.ai/v1';
/// API-Key für die Moonshot AI Authentifizierung
static const String moonshotApiKey = 'sk-EfHJfwCsxiZbOoBJ21OLWb9RUJQXSXAFIFGKnOedKke5JYZp';
/// Moonshot-Modell: moonshot-v1-8k (kurze Texte), moonshot-v1-32k, moonshot-v1-128k
static const String moonshotModel = 'moonshot-v1-8k';
}

View File

@@ -0,0 +1,48 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class ChatMessageEntity {
@Id()
int id = 0;
@Unique()
String messageId;
@Index()
String conversationKey;
String content;
String contentType; // 'TEXT' or 'IMAGE'
@Property(type: PropertyType.date)
@Index()
DateTime createdAt;
String origin; // 'INCOMING' or 'OUTGOING'
String messageType; // 'NORMAL', 'JOB_ASSIGNMENT', etc.
String? jobId;
String? jobNumber;
bool read;
bool pendingSync;
ChatMessageEntity({
required this.messageId,
required this.conversationKey,
required this.content,
this.contentType = 'TEXT',
required this.createdAt,
required this.origin,
required this.messageType,
this.jobId,
this.jobNumber,
this.read = false,
this.pendingSync = false,
});
}

View File

@@ -0,0 +1,26 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class JobEntity {
@Id()
int id = 0;
@Unique()
String jobId; // The original job ID from the Job model
String jobData; // JSON-encoded job data
@Property(type: PropertyType.date)
DateTime createdAt;
@Property(type: PropertyType.date)
DateTime updatedAt;
JobEntity({
required this.jobId,
required this.jobData,
required this.createdAt,
required this.updatedAt,
});
}

View File

@@ -0,0 +1,23 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class PhotoEntity {
@Id()
int id = 0;
String taskId;
int photoIndex;
String data; // Base64-encoded photo data
@Property(type: PropertyType.date)
DateTime createdAt;
PhotoEntity({
required this.taskId,
required this.photoIndex,
required this.data,
required this.createdAt,
});
}

View File

@@ -0,0 +1,27 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class QueuedMessageEntity {
@Id()
int id = 0;
@Unique()
String messageId;
String topic;
String payload; // JSON-encoded payload
@Property(type: PropertyType.date)
DateTime createdAt;
int retryCount;
QueuedMessageEntity({
required this.messageId,
required this.topic,
required this.payload,
required this.createdAt,
this.retryCount = 0,
});
}

View File

@@ -0,0 +1,30 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class TaskStatusEntity {
@Id()
int id = 0;
@Unique()
String taskId;
bool completed;
@Property(type: PropertyType.date)
DateTime? completedAt;
@Property(type: PropertyType.date)
DateTime createdAt;
@Property(type: PropertyType.date)
DateTime updatedAt;
TaskStatusEntity({
required this.taskId,
required this.completed,
this.completedAt,
required this.createdAt,
required this.updatedAt,
});
}

View File

@@ -0,0 +1,26 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class UserDataEntity {
@Id()
int id = 0;
@Unique()
String key;
String value;
@Property(type: PropertyType.date)
DateTime createdAt;
@Property(type: PropertyType.date)
DateTime updatedAt;
UserDataEntity({
required this.key,
required this.value,
required this.createdAt,
required this.updatedAt,
});
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'navigation_observer.dart';
mixin RouteAwareState<T extends StatefulWidget> on State<T> implements RouteAware {
@override
void didPopNext() {
// When returning to this route, subclasses can override to refresh state.
}
void subscribeRouteAware() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final route = ModalRoute.of(context);
if (route != null) {
routeObserver.subscribe(this, route);
}
});
}
void unsubscribeRouteAware() {
routeObserver.unsubscribe(this);
}
}

1866
app/lib/jobs_view.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,267 @@
import 'package:flutter/material.dart';
import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
import 'app_localizations_es.dart';
import 'app_localizations_fr.dart';
import 'app_localizations_pl.dart';
import 'app_localizations_ru.dart';
import 'app_localizations_tr.dart';
import 'app_localizations_et.dart';
import 'app_localizations_lv.dart';
import 'app_localizations_lt.dart';
/// Supported language codes
const List<String> supportedLanguageCodes = ['de', 'en', 'es', 'fr', 'pl', 'ru', 'tr', 'et', 'lv', 'lt'];
/// AppLocalizations provides localized strings for the app
abstract class AppLocalizations {
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations) ?? AppLocalizationsDe();
}
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
/// Language name
String get languageName;
/// Flag emoji
String get flagEmoji;
// ==================== GENERAL ====================
String get appTitle;
String get ok;
String get cancel;
String get save;
String get delete;
String get close;
String get confirm;
String get error;
String get success;
String get loading;
String get refresh;
String get version;
String get unknown;
// ==================== NAVIGATION ====================
String get jobs;
String get availableJobs;
String get chats;
String get settings;
String get logout;
String get logoutConfirm;
String get logoutConfirmMessage;
String get openChat;
String get chatInfo;
String get routePlan;
// ==================== LOGIN ====================
String get welcomeBack;
String get loginSubtitle;
String get email;
String get password;
String get login;
String get loggingIn;
String get forgotPassword;
String get forgotPasswordMessage;
String get loginSuccess;
String get loginFailed;
String get connectionFailed;
String get connectionTimeout;
String get connecting;
String get connectionError;
String get loginError;
// ==================== JOBS ====================
String get noJobsAssigned;
String get noJobsMessage;
String get pullToRefresh;
String get newLabel;
String get tasksToComplete;
String get pickup;
String get delivery;
String get created;
String get status;
String get priority;
String get dueDate;
String get location;
String get description;
String get cargo;
String get quantity;
String get weight;
String get dimensions;
String get jobDeleted;
String get jobDeleteError;
String get jobCompleted;
String get from;
String get to;
String get jobsUpdated;
String get connectionRestored;
String get connectionLost;
String get offline;
String get deleteJob;
String get jobRemoved;
String get newJobReceived;
// ==================== TASKS ====================
String get tasks;
String get noTasks;
String get noTasksMessage;
String get taskOrder;
String get confirmationRequired;
String get confirmationDescription;
String get checklist;
String get checklistDescription;
String get completeTask;
String get completeTaskConfirm;
String get completeTaskNote;
String get taskCompleted;
String get comment;
String get commentRequired;
String get enterComment;
String get commentDescription;
String get finish;
String get signature;
String get signatureCapture;
String get signatureRequired;
String get clear;
String get signatureError;
String get signatureInstruction;
String get photoCapture;
String get requiredPhotos;
String get photosTaken;
String get photos;
String get takePhoto;
String get selectFromLibrary;
String get retakePhoto;
String get photoRequired;
String get minPhotos;
String get maxPhotos;
String get photoError;
String get deletePhoto;
String get deletePhotoConfirm;
String get barcode;
String get barcodeScan;
String get scanBarcode;
String get barcodeRequired;
String get minBarcodes;
String get maxBarcodes;
String get scanned;
String get scannedBarcodes;
String get barcodesRequired;
String get enterBarcode;
String get barcodeEnterDescription;
String barcodeNumberRequired(int number);
String barcodeNumberOptional(int number);
String get barcodeError;
String get cameraError;
String get cameraNotReady;
String get cameraNotAvailable;
String get cameraNotSupportedMessage;
String get cameraNotSupportedOnPlatform;
String get maxPhotosReached;
String get cameraReadyNoPreview;
String get cameraLoading;
String get cameraInitializing;
String get cameraLoadingMessage;
String get addPhotos;
String get addPhotosInstruction;
String get photoOf;
// ==================== CHAT ====================
String get typeMessage;
String get send;
String get noSender;
String get noSenderMessage;
String get noRecipient;
String get noRecipientMessage;
String get messageSendError;
String get photoSendError;
String get photoProcessError;
String get imageSendError;
String get chatTypeJob;
String get chatTypeGeneral;
String get jobNumber;
String get messages;
String get selectPhoto;
String get unreadMessages;
// ==================== CARGO ====================
String get cargoDetails;
String get itemName;
String get itemNumber;
String get item;
String get weightUnit;
String get dimensionUnit;
String get noCargoItems;
String get noCargoItemsMessage;
String get article;
// ==================== TASK TYPES ====================
String get takePhotos;
String get photosCount;
String get checklistPoints;
String get signatureRequiredText;
String get scanBarcodes;
String get barcodeCount;
String get commentOptional;
String get genericTask;
String get complete;
String get abort;
String get optional;
String get skipTask;
// ==================== SETTINGS ====================
String get language;
String get languageChanged;
String get appInfo;
// ==================== STATUS ====================
String get statusCreated;
String get statusAssigned;
String get statusInProgress;
String get statusCompleted;
String get priorityLow;
String get priorityMedium;
String get priorityHigh;
String get priorityUrgent;
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
bool isSupported(Locale locale) {
return supportedLanguageCodes.contains(locale.languageCode);
}
@override
Future<AppLocalizations> load(Locale locale) async {
switch (locale.languageCode) {
case 'de':
return AppLocalizationsDe();
case 'en':
return AppLocalizationsEn();
case 'es':
return AppLocalizationsEs();
case 'fr':
return AppLocalizationsFr();
case 'pl':
return AppLocalizationsPl();
case 'ru':
return AppLocalizationsRu();
case 'tr':
return AppLocalizationsTr();
case 'et':
return AppLocalizationsEt();
case 'lv':
return AppLocalizationsLv();
case 'lt':
return AppLocalizationsLt();
default:
return AppLocalizationsDe();
}
}
@override
bool shouldReload(LocalizationsDelegate<AppLocalizations> old) => false;
}

View File

@@ -0,0 +1,551 @@
import 'app_localizations.dart';
class AppLocalizationsDe extends AppLocalizations {
@override
String get languageName => 'Deutsch';
@override
String get flagEmoji => '🇩🇪';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Abbrechen';
@override
String get save => 'Speichern';
@override
String get delete => 'Löschen';
@override
String get close => 'Schließen';
@override
String get confirm => 'Bestätigen';
@override
String get error => 'Fehler';
@override
String get success => 'Erfolg';
@override
String get loading => 'Laden...';
@override
String get refresh => 'Aktualisieren';
@override
String get version => 'Version';
@override
String get unknown => 'Unbekannt';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Jobs';
@override
String get availableJobs => 'Verfügbare Jobs';
@override
String get chats => 'Chats';
@override
String get settings => 'Einstellungen';
@override
String get logout => 'Abmelden';
@override
String get logoutConfirm => 'Abmelden';
@override
String get logoutConfirmMessage => 'Möchten Sie sich wirklich abmelden?';
@override
String get openChat => 'Chat öffnen';
@override
String get chatInfo => 'Chat-Info';
@override
String get routePlan => 'Route planen';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Willkommen zurück';
@override
String get loginSubtitle => 'Melden Sie sich in Ihrem Konto an';
@override
String get email => 'E-Mail';
@override
String get password => 'Passwort';
@override
String get login => 'Anmelden';
@override
String get loggingIn => 'Verbinden…';
@override
String get forgotPassword => 'Passwort vergessen?';
@override
String get forgotPasswordMessage => 'Passwort vergessen Funktion noch nicht implementiert';
@override
String get loginSuccess => 'Erfolgreich abgemeldet';
@override
String get loginFailed => 'Anmeldung fehlgeschlagen';
@override
String get connectionFailed => 'Verbindung zum Server fehlgeschlagen (Timeout).';
@override
String get connectionTimeout => 'Verbindung zum Server fehlgeschlagen (Timeout).';
@override
String get connecting => 'Verbindung zum Server wird hergestellt...';
@override
String get connectionError => 'Verbindungsfehler';
@override
String get loginError => 'Fehler bei der Anmeldung';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Keine Jobs zugewiesen';
@override
String get noJobsMessage => 'Ihre zugewiesenen Jobs werden hier angezeigt.';
@override
String get pullToRefresh => 'Nach unten ziehen zum Aktualisieren';
@override
String get newLabel => 'NEU';
@override
String get tasksToComplete => 'Zu erledigende Aufgaben';
@override
String get pickup => 'Abholung';
@override
String get delivery => 'Zustellung';
@override
String get created => 'Erstellt';
@override
String get status => 'Status';
@override
String get priority => 'Priorität';
@override
String get dueDate => 'Fälligkeitsdatum';
@override
String get location => 'Ort';
@override
String get description => 'Beschreibung';
@override
String get cargo => 'Fracht';
@override
String get quantity => 'Anzahl';
@override
String get weight => 'Gewicht';
@override
String get dimensions => 'Abmessungen';
@override
String get jobDeleted => 'Job gelöscht';
@override
String get jobDeleteError => 'Fehler beim Löschen des Jobs';
@override
String get jobCompleted => 'Job abgeschlossen';
@override
String get from => 'Von';
@override
String get to => 'nach';
@override
String get jobsUpdated => 'Jobs aktualisiert';
@override
String get connectionRestored => 'Verbindung wiederhergestellt. Lade Jobs...';
@override
String get connectionLost => 'Verbindung verloren. Offline.';
@override
String get offline => 'Offline';
@override
String get deleteJob => 'Job löschen';
@override
String get jobRemoved => 'wurde entfernt';
@override
String get newJobReceived => 'Neuer Job erhalten';
// ==================== TASKS ====================
@override
String get tasks => 'Aufgaben';
@override
String get noTasks => 'Keine Aufgaben';
@override
String get noTasksMessage => 'Für diesen Job sind keine Aufgaben definiert.';
@override
String get taskOrder => 'Reihenfolge';
@override
String get confirmationRequired => 'Bestätigung erforderlich';
@override
String get confirmationDescription => 'Klicken Sie auf den Button um die Aufgabe zu erledigen.';
@override
String get checklist => 'Checkliste';
@override
String get checklistDescription => 'Bitte alle Punkte abhaken:';
@override
String get completeTask => 'Aufgabe abschließen';
@override
String get completeTaskConfirm => 'Möchten Sie diese Aufgabe als erledigt markieren?';
@override
String get completeTaskNote => 'Notiz (optional)';
@override
String get taskCompleted => 'Aufgabe erledigt';
@override
String get comment => 'Kommentar';
@override
String get commentRequired => 'Kommentar (erforderlich)';
@override
String get enterComment => 'Kommentar eingeben';
@override
String get commentDescription => 'Bitte geben Sie einen Kommentar ein:';
@override
String get finish => 'Fertig';
@override
String get signature => 'Unterschrift';
@override
String get signatureCapture => 'Unterschrift erfassen';
@override
String get signatureRequired => 'Bitte eine Unterschrift erfassen.';
@override
String get clear => 'Leeren';
@override
String get signatureError => 'Fehler beim Speichern der Unterschrift';
@override
String get signatureInstruction => 'Bitte unterschreiben Sie im Feld unten (Maus oder Finger).';
@override
String get photoCapture => 'Fotos aufnehmen';
@override
String get requiredPhotos => 'Benötigte Fotos';
@override
String get photosTaken => 'Aufgenommen';
@override
String get photos => 'Fotos';
@override
String get takePhoto => 'Foto aufnehmen';
@override
String get selectFromLibrary => 'Aus Bibliothek wählen';
@override
String get retakePhoto => 'Neu aufnehmen';
@override
String get photoRequired => 'Foto erforderlich';
@override
String get minPhotos => 'Mindestens';
@override
String get maxPhotos => 'Maximal';
@override
String get photoError => 'Fehler beim Aufnehmen des Fotos';
@override
String get deletePhoto => 'Foto löschen';
@override
String get deletePhotoConfirm => 'Möchten Sie dieses Foto wirklich löschen?';
@override
String get barcode => 'Barcode';
@override
String get barcodeScan => 'Barcode scannen';
@override
String get scanBarcode => 'Barcode scannen';
@override
String get barcodeRequired => 'Barcode erforderlich';
@override
String get minBarcodes => 'Mindestens';
@override
String get maxBarcodes => 'Maximal';
@override
String get scanned => 'Gescannt';
@override
String get scannedBarcodes => 'Gescannte Barcodes';
@override
String get barcodesRequired => 'Barcodes erforderlich';
@override
String get enterBarcode => 'Barcode eingeben';
@override
String get barcodeEnterDescription => 'Bitte geben Sie die Barcodes ein:';
@override
String barcodeNumberRequired(int number) => 'Barcode $number (erforderlich)';
@override
String barcodeNumberOptional(int number) => 'Barcode $number (optional)';
@override
String get barcodeError => 'Fehler beim Scannen des Barcodes';
@override
String get cameraError => 'Fehler beim Initialisieren der Kamera';
@override
String get cameraNotReady => 'Kamera ist nicht bereit oder nicht verfügbar';
@override
String get cameraNotAvailable => 'Kamera nicht verfügbar';
@override
String get cameraNotSupportedMessage => 'Auf dieser Plattform wird die Kamera nicht unterstützt.';
@override
String get cameraNotSupportedOnPlatform => 'Nicht unterstützt auf dieser Plattform';
@override
String get maxPhotosReached => 'Maximum erreicht';
@override
String get cameraReadyNoPreview => 'Kamera bereit (ohne Vorschau)';
@override
String get cameraLoading => 'Kamera lädt...';
@override
String get cameraInitializing => 'Kamera wird initialisiert...';
@override
String get cameraLoadingMessage => 'Bitte warten Sie, während die Kamera geladen wird';
@override
String get addPhotos => 'Fotos hinzufügen';
@override
String get addPhotosInstruction => 'Verwenden Sie den Button „Foto auswählen", um Bilder von Ihrer Kamera oder Festplatte hinzuzufügen.';
@override
String get photoOf => 'von';
// ==================== CHAT ====================
@override
String get typeMessage => 'Nachricht eingeben...';
@override
String get send => 'Senden';
@override
String get noSender => 'Kein Absender verfügbar';
@override
String get noSenderMessage => 'Kein Absender verfügbar. Bitte erneut anmelden.';
@override
String get noRecipient => 'Kein Empfänger konfiguriert';
@override
String get noRecipientMessage => 'Kein Empfänger für diesen Chat konfiguriert.';
@override
String get messageSendError => 'Nachricht konnte nicht gesendet werden.';
@override
String get photoSendError => 'Foto konnte nicht gesendet werden.';
@override
String get photoProcessError => 'Foto konnte nicht verarbeitet werden.';
@override
String get imageSendError => 'Bild konnte nicht gesendet werden.';
@override
String get chatTypeJob => 'Job-spezifisch';
@override
String get chatTypeGeneral => 'Allgemein';
@override
String get jobNumber => 'Job-Nummer';
@override
String get messages => 'Nachrichten';
@override
String get selectPhoto => 'Foto auswählen';
@override
String get unreadMessages => 'Ungelesene Nachrichten';
// ==================== SETTINGS ====================
@override
String get language => 'Sprache';
@override
String get languageChanged => 'Sprache geändert zu';
@override
String get appInfo => 'APP-INFO';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Frachtdetails';
@override
String get itemName => 'Bezeichnung';
@override
String get itemNumber => 'Positions-Nr.';
@override
String get item => 'Position';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Keine Frachtgüter';
@override
String get noCargoItemsMessage => 'Für diesen Job sind keine Frachtgüter definiert.';
@override
String get article => 'Artikel';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Fotos aufnehmen';
@override
String get photosCount => 'Fotos';
@override
String get checklistPoints => 'Punkte';
@override
String get signatureRequiredText => 'Unterschrift erforderlich';
@override
String get scanBarcodes => 'Barcode scannen';
@override
String get barcodeCount => 'Codes';
@override
String get commentOptional => 'Kommentar';
@override
String get genericTask => 'Allgemeine Aufgabe';
@override
String get complete => 'Abschließen';
@override
String get abort => 'Abbrechen';
@override
String get optional => 'Optional';
@override
String get skipTask => 'Überspringen';
// ==================== STATUS ====================
@override
String get statusCreated => 'Erstellt';
@override
String get statusAssigned => 'Zugewiesen';
@override
String get statusInProgress => 'In Bearbeitung';
@override
String get statusCompleted => 'Abgeschlossen';
@override
String get priorityLow => 'Niedrig';
@override
String get priorityMedium => 'Mittel';
@override
String get priorityHigh => 'Hoch';
@override
String get priorityUrgent => 'Dringend';
}

View File

@@ -0,0 +1,551 @@
import 'app_localizations.dart';
class AppLocalizationsEn extends AppLocalizations {
@override
String get languageName => 'English';
@override
String get flagEmoji => '🇬🇧';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Cancel';
@override
String get save => 'Save';
@override
String get delete => 'Delete';
@override
String get close => 'Close';
@override
String get confirm => 'Confirm';
@override
String get error => 'Error';
@override
String get success => 'Success';
@override
String get loading => 'Loading...';
@override
String get refresh => 'Refresh';
@override
String get version => 'Version';
@override
String get unknown => 'Unknown';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Jobs';
@override
String get availableJobs => 'Available Jobs';
@override
String get chats => 'Chats';
@override
String get settings => 'Settings';
@override
String get logout => 'Logout';
@override
String get logoutConfirm => 'Logout';
@override
String get logoutConfirmMessage => 'Do you really want to logout?';
@override
String get openChat => 'Open Chat';
@override
String get chatInfo => 'Chat Info';
@override
String get routePlan => 'Plan Route';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Welcome Back';
@override
String get loginSubtitle => 'Sign in to your account';
@override
String get email => 'Email';
@override
String get password => 'Password';
@override
String get login => 'Login';
@override
String get loggingIn => 'Connecting...';
@override
String get forgotPassword => 'Forgot Password?';
@override
String get forgotPasswordMessage => 'Forgot password feature not yet implemented';
@override
String get loginSuccess => 'Successfully logged out';
@override
String get loginFailed => 'Login failed';
@override
String get connectionFailed => 'Connection to server failed (Timeout).';
@override
String get connectionTimeout => 'Connection to server failed (Timeout).';
@override
String get connecting => 'Connecting to server...';
@override
String get connectionError => 'Connection error';
@override
String get loginError => 'Error during login';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'No Jobs Assigned';
@override
String get noJobsMessage => 'Your assigned jobs will be displayed here.';
@override
String get pullToRefresh => 'Pull down to refresh';
@override
String get newLabel => 'NEW';
@override
String get tasksToComplete => 'Tasks to Complete';
@override
String get pickup => 'Pickup';
@override
String get delivery => 'Delivery';
@override
String get created => 'Created';
@override
String get status => 'Status';
@override
String get priority => 'Priority';
@override
String get dueDate => 'Due Date';
@override
String get location => 'Location';
@override
String get description => 'Description';
@override
String get cargo => 'Cargo';
@override
String get quantity => 'Quantity';
@override
String get weight => 'Weight';
@override
String get dimensions => 'Dimensions';
@override
String get jobDeleted => 'Job deleted';
@override
String get jobDeleteError => 'Error deleting job';
@override
String get jobCompleted => 'Job completed';
@override
String get from => 'From';
@override
String get to => 'to';
@override
String get jobsUpdated => 'Jobs updated';
@override
String get connectionRestored => 'Connection restored. Loading jobs...';
@override
String get connectionLost => 'Connection lost. Offline.';
@override
String get offline => 'Offline';
@override
String get deleteJob => 'Delete Job';
@override
String get jobRemoved => 'was removed';
@override
String get newJobReceived => 'New job received';
// ==================== TASKS ====================
@override
String get tasks => 'Tasks';
@override
String get noTasks => 'No Tasks';
@override
String get noTasksMessage => 'No tasks defined for this job.';
@override
String get taskOrder => 'Order';
@override
String get confirmationRequired => 'Confirmation Required';
@override
String get confirmationDescription => 'Click the button to complete the task.';
@override
String get checklist => 'Checklist';
@override
String get checklistDescription => 'Please check all items:';
@override
String get completeTask => 'Complete Task';
@override
String get completeTaskConfirm => 'Do you want to mark this task as completed?';
@override
String get completeTaskNote => 'Note (optional)';
@override
String get taskCompleted => 'Task completed';
@override
String get comment => 'Comment';
@override
String get commentRequired => 'Comment (required)';
@override
String get enterComment => 'Enter Comment';
@override
String get commentDescription => 'Please enter a comment:';
@override
String get finish => 'Finish';
@override
String get signature => 'Signature';
@override
String get signatureCapture => 'Capture Signature';
@override
String get signatureRequired => 'Please capture a signature.';
@override
String get clear => 'Clear';
@override
String get signatureError => 'Error saving signature';
@override
String get signatureInstruction => 'Please sign in the field below (mouse or finger).';
@override
String get photoCapture => 'Take Photos';
@override
String get requiredPhotos => 'Required Photos';
@override
String get photosTaken => 'Taken';
@override
String get photos => 'Photos';
@override
String get takePhoto => 'Take Photo';
@override
String get selectFromLibrary => 'Select from Library';
@override
String get retakePhoto => 'Retake';
@override
String get photoRequired => 'Photo required';
@override
String get minPhotos => 'At least';
@override
String get maxPhotos => 'Maximum';
@override
String get photoError => 'Error taking photo';
@override
String get deletePhoto => 'Delete Photo';
@override
String get deletePhotoConfirm => 'Do you really want to delete this photo?';
@override
String get barcode => 'Barcode';
@override
String get barcodeScan => 'Scan Barcode';
@override
String get scanBarcode => 'Scan Barcode';
@override
String get barcodeRequired => 'Barcode required';
@override
String get minBarcodes => 'At least';
@override
String get maxBarcodes => 'Maximum';
@override
String get scanned => 'Scanned';
@override
String get scannedBarcodes => 'Scanned Barcodes';
@override
String get barcodesRequired => 'Barcodes Required';
@override
String get enterBarcode => 'Enter Barcode';
@override
String get barcodeEnterDescription => 'Please enter the barcodes:';
@override
String barcodeNumberRequired(int number) => 'Barcode $number (required)';
@override
String barcodeNumberOptional(int number) => 'Barcode $number (optional)';
@override
String get barcodeError => 'Error scanning barcode';
@override
String get cameraError => 'Error initializing camera';
@override
String get cameraNotReady => 'Camera is not ready or not available';
@override
String get cameraNotAvailable => 'Camera not available';
@override
String get cameraNotSupportedMessage => 'The camera is not supported on this platform.';
@override
String get cameraNotSupportedOnPlatform => 'Not supported on this platform';
@override
String get maxPhotosReached => 'Maximum reached';
@override
String get cameraReadyNoPreview => 'Camera ready (no preview)';
@override
String get cameraLoading => 'Camera loading...';
@override
String get cameraInitializing => 'Initializing camera...';
@override
String get cameraLoadingMessage => 'Please wait while the camera is loading';
@override
String get addPhotos => 'Add photos';
@override
String get addPhotosInstruction => 'Use the "Select photo" button to add images from your camera or hard drive.';
@override
String get photoOf => 'of';
// ==================== CHAT ====================
@override
String get typeMessage => 'Type a message...';
@override
String get send => 'Send';
@override
String get noSender => 'No sender available';
@override
String get noSenderMessage => 'No sender available. Please login again.';
@override
String get noRecipient => 'No recipient configured';
@override
String get noRecipientMessage => 'No recipient configured for this chat.';
@override
String get messageSendError => 'Message could not be sent.';
@override
String get photoSendError => 'Photo could not be sent.';
@override
String get photoProcessError => 'Photo could not be processed.';
@override
String get imageSendError => 'Image could not be sent.';
@override
String get chatTypeJob => 'Job-specific';
@override
String get chatTypeGeneral => 'General';
@override
String get jobNumber => 'Job Number';
@override
String get messages => 'Messages';
@override
String get selectPhoto => 'Select Photo';
@override
String get unreadMessages => 'Unread Messages';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Cargo Details';
@override
String get itemName => 'Description';
@override
String get itemNumber => 'Item Number';
@override
String get item => 'Item';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'No Cargo Items';
@override
String get noCargoItemsMessage => 'No cargo items defined for this job.';
@override
String get article => 'Article';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Take Photos';
@override
String get photosCount => 'Photos';
@override
String get checklistPoints => 'Points';
@override
String get signatureRequiredText => 'Signature Required';
@override
String get scanBarcodes => 'Scan Barcodes';
@override
String get barcodeCount => 'Codes';
@override
String get commentOptional => 'Comment';
@override
String get genericTask => 'Generic Task';
@override
String get complete => 'Complete';
@override
String get abort => 'Cancel';
@override
String get optional => 'Optional';
@override
String get skipTask => 'Skip';
// ==================== SETTINGS ====================
@override
String get language => 'Language';
@override
String get languageChanged => 'Language changed to';
@override
String get appInfo => 'APP INFO';
// ==================== STATUS ====================
@override
String get statusCreated => 'Created';
@override
String get statusAssigned => 'Assigned';
@override
String get statusInProgress => 'In Progress';
@override
String get statusCompleted => 'Completed';
@override
String get priorityLow => 'Low';
@override
String get priorityMedium => 'Medium';
@override
String get priorityHigh => 'High';
@override
String get priorityUrgent => 'Urgent';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsEs extends AppLocalizations {
@override
String get languageName => 'Español';
@override
String get flagEmoji => '🇪🇸';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Cancelar';
@override
String get save => 'Guardar';
@override
String get delete => 'Eliminar';
@override
String get close => 'Cerrar';
@override
String get confirm => 'Confirmar';
@override
String get error => 'Error';
@override
String get success => 'Éxito';
@override
String get loading => 'Cargando...';
@override
String get refresh => 'Actualizar';
@override
String get version => 'Versión';
@override
String get unknown => 'Desconocido';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Trabajos';
@override
String get availableJobs => 'Trabajos Disponibles';
@override
String get chats => 'Chats';
@override
String get settings => 'Ajustes';
@override
String get logout => 'Cerrar sesión';
@override
String get logoutConfirm => 'Cerrar sesión';
@override
String get logoutConfirmMessage => '¿Realmente desea cerrar sesión?';
@override
String get openChat => 'Abrir chat';
@override
String get chatInfo => 'Info del chat';
@override
String get routePlan => 'Planificar ruta';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Bienvenido de nuevo';
@override
String get loginSubtitle => 'Inicie sesión en su cuenta';
@override
String get email => 'Correo electrónico';
@override
String get password => 'Contraseña';
@override
String get login => 'Iniciar sesión';
@override
String get loggingIn => 'Conectando...';
@override
String get forgotPassword => '¿Olvidó su contraseña?';
@override
String get forgotPasswordMessage => 'Función de contraseña olvidada aún no implementada';
@override
String get loginSuccess => 'Sesión cerrada correctamente';
@override
String get loginFailed => 'Error al iniciar sesión';
@override
String get connectionFailed => 'Error de conexión al servidor (Tiempo agotado).';
@override
String get connectionTimeout => 'Error de conexión al servidor (Tiempo agotado).';
@override
String get connecting => 'Conectando al servidor...';
@override
String get connectionError => 'Error de conexión';
@override
String get loginError => 'Error durante el inicio de sesión';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'No hay trabajos asignados';
@override
String get noJobsMessage => 'Sus trabajos asignados se mostrarán aquí.';
@override
String get pullToRefresh => 'Deslice hacia abajo para actualizar';
@override
String get newLabel => 'NUEVO';
@override
String get tasksToComplete => 'Tareas por completar';
@override
String get pickup => 'Recogida';
@override
String get delivery => 'Entrega';
@override
String get created => 'Creado';
@override
String get status => 'Estado';
@override
String get priority => 'Prioridad';
@override
String get dueDate => 'Fecha de vencimiento';
@override
String get location => 'Ubicación';
@override
String get description => 'Descripción';
@override
String get cargo => 'Carga';
@override
String get quantity => 'Cantidad';
@override
String get weight => 'Peso';
@override
String get dimensions => 'Dimensiones';
@override
String get jobDeleted => 'Trabajo eliminado';
@override
String get jobDeleteError => 'Error al eliminar el trabajo';
@override
String get jobCompleted => 'Trabajo completado';
@override
String get from => 'De';
@override
String get to => 'a';
@override
String get jobsUpdated => 'Trabajos actualizados';
@override
String get connectionRestored => 'Conexión restaurada. Cargando trabajos...';
@override
String get connectionLost => 'Conexión perdida. Sin conexión.';
@override
String get offline => 'Sin conexión';
@override
String get deleteJob => 'Eliminar trabajo';
@override
String get jobRemoved => 'fue eliminado';
@override
String get newJobReceived => 'Nuevo trabajo recibido';
// ==================== TASKS ====================
@override
String get tasks => 'Tareas';
@override
String get noTasks => 'Sin tareas';
@override
String get noTasksMessage => 'No hay tareas definidas para este trabajo.';
@override
String get taskOrder => 'Orden';
@override
String get confirmationRequired => 'Confirmación requerida';
@override
String get confirmationDescription => 'Haga clic en el botón para completar la tarea.';
@override
String get checklist => 'Lista de verificación';
@override
String get checklistDescription => 'Por favor marque todos los elementos:';
@override
String get completeTask => 'Completar tarea';
@override
String get completeTaskConfirm => '¿Desea marcar esta tarea como completada?';
@override
String get completeTaskNote => 'Nota (opcional)';
@override
String get taskCompleted => 'Tarea completada';
@override
String get comment => 'Comentario';
@override
String get commentRequired => 'Comentario (requerido)';
@override
String get enterComment => 'Ingrese comentario';
@override
String get commentDescription => 'Por favor ingrese un comentario:';
@override
String get finish => 'Finalizar';
@override
String get signature => 'Firma';
@override
String get signatureCapture => 'Capturar firma';
@override
String get signatureRequired => 'Por favor capture una firma.';
@override
String get clear => 'Limpiar';
@override
String get signatureError => 'Error al guardar la firma';
@override
String get signatureInstruction => 'Por favor, firme en el campo de abajo (ratón o dedo).';
@override
String get photoCapture => 'Tomar fotos';
@override
String get requiredPhotos => 'Fotos requeridas';
@override
String get photosTaken => 'Tomadas';
@override
String get photos => 'Fotos';
@override
String get takePhoto => 'Tomar foto';
@override
String get selectFromLibrary => 'Seleccionar de la biblioteca';
@override
String get retakePhoto => 'Volver a tomar';
@override
String get photoRequired => 'Foto requerida';
@override
String get minPhotos => 'Al menos';
@override
String get maxPhotos => 'Máximo';
@override
String get photoError => 'Error al tomar la foto';
@override
String get deletePhoto => 'Eliminar foto';
@override
String get deletePhotoConfirm => '¿Realmente desea eliminar esta foto?';
@override
String get barcode => 'Código de barras';
@override
String get barcodeScan => 'Escanear código de barras';
@override
String get scanBarcode => 'Escanear código de barras';
@override
String get barcodeRequired => 'Código de barras requerido';
@override
String get minBarcodes => 'Al menos';
@override
String get maxBarcodes => 'Máximo';
@override
String get scanned => 'Escaneado';
@override
String get scannedBarcodes => 'Códigos de barras escaneados';
@override
String get barcodesRequired => 'Códigos de barras requeridos';
@override
String get enterBarcode => 'Ingresar código de barras';
@override
String get barcodeEnterDescription => 'Por favor ingrese los códigos de barras:';
@override
String barcodeNumberRequired(int number) => 'Código de barras $number (requerido)';
@override
String barcodeNumberOptional(int number) => 'Código de barras $number (opcional)';
@override
String get barcodeError => 'Error al escanear el código de barras';
@override
String get cameraError => 'Error al inicializar la cámara';
@override
String get cameraNotReady => 'La cámara no está lista o no disponible';
@override
String get cameraNotAvailable => 'Cámara no disponible';
@override
String get cameraNotSupportedMessage => 'La cámara no es compatible con esta plataforma.';
@override
String get cameraNotSupportedOnPlatform => 'No soportado en esta plataforma';
@override
String get maxPhotosReached => 'Máximo alcanzado';
@override
String get cameraReadyNoPreview => 'Cámara lista (sin vista previa)';
@override
String get cameraLoading => 'Cargando cámara...';
@override
String get cameraInitializing => 'Inicializando cámara...';
@override
String get cameraLoadingMessage => 'Por favor espere mientras se carga la cámara';
@override
String get addPhotos => 'Añadir fotos';
@override
String get addPhotosInstruction => 'Use el botón "Seleccionar foto" para añadir imágenes de su cámara o disco duro.';
@override
String get photoOf => 'de';
// ==================== CHAT ====================
@override
String get typeMessage => 'Escriba un mensaje...';
@override
String get send => 'Enviar';
@override
String get noSender => 'No hay remitente disponible';
@override
String get noSenderMessage => 'No hay remitente disponible. Por favor inicie sesión de nuevo.';
@override
String get noRecipient => 'No hay destinatario configurado';
@override
String get noRecipientMessage => 'No hay destinatario configurado para este chat.';
@override
String get messageSendError => 'El mensaje no pudo ser enviado.';
@override
String get photoSendError => 'La foto no pudo ser enviada.';
@override
String get photoProcessError => 'La foto no pudo ser procesada.';
@override
String get imageSendError => 'La imagen no pudo ser enviada.';
@override
String get chatTypeJob => 'Específico del trabajo';
@override
String get chatTypeGeneral => 'General';
@override
String get jobNumber => 'Número de trabajo';
@override
String get messages => 'Mensajes';
@override
String get selectPhoto => 'Seleccionar foto';
@override
String get unreadMessages => 'Mensajes no leídos';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Detalles de carga';
@override
String get itemName => 'Descripción';
@override
String get itemNumber => 'Nº de posición';
@override
String get item => 'Posición';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Sin artículos de carga';
@override
String get noCargoItemsMessage => 'No hay artículos de carga definidos para este trabajo.';
@override
String get article => 'Artículo';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Tomar fotos';
@override
String get photosCount => 'Fotos';
@override
String get checklistPoints => 'Puntos';
@override
String get signatureRequiredText => 'Firma requerida';
@override
String get scanBarcodes => 'Escanear códigos';
@override
String get barcodeCount => 'Códigos';
@override
String get commentOptional => 'Comentario';
@override
String get genericTask => 'Tarea genérica';
@override
String get complete => 'Completar';
@override
String get abort => 'Cancelar';
@override
String get optional => 'Opcional';
@override
String get skipTask => 'Omitir';
// ==================== SETTINGS ====================
@override
String get language => 'Idioma';
@override
String get languageChanged => 'Idioma cambiado a';
@override
String get appInfo => 'INFO DE LA APP';
// ==================== STATUS ====================
@override
String get statusCreated => 'Creado';
@override
String get statusAssigned => 'Asignado';
@override
String get statusInProgress => 'En progreso';
@override
String get statusCompleted => 'Completado';
@override
String get priorityLow => 'Baja';
@override
String get priorityMedium => 'Media';
@override
String get priorityHigh => 'Alta';
@override
String get priorityUrgent => 'Urgente';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsEt extends AppLocalizations {
@override
String get languageName => 'Eesti';
@override
String get flagEmoji => '🇪🇪';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Tühista';
@override
String get save => 'Salvesta';
@override
String get delete => 'Kustuta';
@override
String get close => 'Sulge';
@override
String get confirm => 'Kinnita';
@override
String get error => 'Viga';
@override
String get success => 'Edu';
@override
String get loading => 'Laadimine...';
@override
String get refresh => 'Värskenda';
@override
String get version => 'Versioon';
@override
String get unknown => 'Tundmatu';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Tööd';
@override
String get availableJobs => 'Saadaolevad tööd';
@override
String get chats => 'Vestlused';
@override
String get settings => 'Seaded';
@override
String get logout => 'Logi välja';
@override
String get logoutConfirm => 'Logi välja';
@override
String get logoutConfirmMessage => 'Kas soovite tõesti välja logida?';
@override
String get openChat => 'Ava vestlus';
@override
String get chatInfo => 'Vestluse info';
@override
String get routePlan => 'Kavanda marsruut';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Tere tulemast tagasi';
@override
String get loginSubtitle => 'Logige oma kontosse sisse';
@override
String get email => 'E-post';
@override
String get password => 'Parool';
@override
String get login => 'Logi sisse';
@override
String get loggingIn => 'Ühendamine...';
@override
String get forgotPassword => 'Unustasid parooli?';
@override
String get forgotPasswordMessage => 'Unustatud parooli funktsioon pole veel rakendatud';
@override
String get loginSuccess => 'Edukalt välja logitud';
@override
String get loginFailed => 'Sisselogimine ebaõnnestus';
@override
String get connectionFailed => 'Serveriga ühenduse loomine ebaõnnestus (Aegunud).';
@override
String get connectionTimeout => 'Serveriga ühenduse loomine ebaõnnestus (Aegunud).';
@override
String get connecting => 'Serveriga ühendamine...';
@override
String get connectionError => 'Ühenduse viga';
@override
String get loginError => 'Viga sisselogimisel';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Ülesandeid pole määratud';
@override
String get noJobsMessage => 'Teie määratud tööd kuvatakse siin.';
@override
String get pullToRefresh => 'Värskendamiseks tõmmake alla';
@override
String get newLabel => 'UUS';
@override
String get tasksToComplete => 'Täitmiseks ülesanded';
@override
String get pickup => 'Pealevõtt';
@override
String get delivery => 'Kohaletoimetamine';
@override
String get created => 'Loodud';
@override
String get status => 'Olek';
@override
String get priority => 'Prioriteet';
@override
String get dueDate => 'Tähtaeg';
@override
String get location => 'Asukoht';
@override
String get description => 'Kirjeldus';
@override
String get cargo => 'Kaup';
@override
String get quantity => 'Kogus';
@override
String get weight => 'Kaal';
@override
String get dimensions => 'Mõõtmed';
@override
String get jobDeleted => 'Töö kustutatud';
@override
String get jobDeleteError => 'Viga töö kustutamisel';
@override
String get jobCompleted => 'Töö lõpetatud';
@override
String get from => 'Kust';
@override
String get to => 'kus';
@override
String get jobsUpdated => 'Tööd värskendatud';
@override
String get connectionRestored => 'Ühendus taastatud. Tööde laadimine...';
@override
String get connectionLost => 'Ühendus kaotatud. Võrguühenduseta.';
@override
String get offline => 'Võrguühenduseta';
@override
String get deleteJob => 'Kustuta töö';
@override
String get jobRemoved => 'eemaldati';
@override
String get newJobReceived => 'Uus töö saadud';
// ==================== TASKS ====================
@override
String get tasks => 'Ülesanded';
@override
String get noTasks => 'Ülesandeid pole';
@override
String get noTasksMessage => 'Selle töö jaoks pole ülesandeid määratud.';
@override
String get taskOrder => 'Järjekord';
@override
String get confirmationRequired => 'Vajalik kinnitus';
@override
String get confirmationDescription => 'Ülesande lõpuleviimiseks klõpsake nuppu.';
@override
String get checklist => 'Kontrollnimekiri';
@override
String get checklistDescription => 'Palun märkige kõik punktid:';
@override
String get completeTask => 'Lõpeta ülesanne';
@override
String get completeTaskConfirm => 'Kas soovite selle ülesande lõpetatuks märgistada?';
@override
String get completeTaskNote => 'Märkus (valikuline)';
@override
String get taskCompleted => 'Ülesanne lõpetatud';
@override
String get comment => 'Kommentaar';
@override
String get commentRequired => 'Kommentaar (nõutav)';
@override
String get enterComment => 'Sisesta kommentaar';
@override
String get commentDescription => 'Palun sisestage kommentaar:';
@override
String get finish => 'Lõpeta';
@override
String get signature => 'Allkiri';
@override
String get signatureCapture => 'Salvesta allkiri';
@override
String get signatureRequired => 'Palun salvestage allkiri.';
@override
String get clear => 'Tühjenda';
@override
String get signatureError => 'Viga allkirja salvestamisel';
@override
String get signatureInstruction => 'Palun allkirjastage allolevas väljas (hiir või sõrm).';
@override
String get photoCapture => 'Tee pilte';
@override
String get requiredPhotos => 'Vajalikud fotod';
@override
String get photosTaken => 'Tehtud';
@override
String get photos => 'Fotod';
@override
String get takePhoto => 'Tee foto';
@override
String get selectFromLibrary => 'Vali galeriist';
@override
String get retakePhoto => 'Pildista uuesti';
@override
String get photoRequired => 'Foto nõutav';
@override
String get minPhotos => 'Vähemalt';
@override
String get maxPhotos => 'Maksimum';
@override
String get photoError => 'Viga foto tegemisel';
@override
String get deletePhoto => 'Kustuta foto';
@override
String get deletePhotoConfirm => 'Kas soovite tõesti selle foto kustutada?';
@override
String get barcode => 'Vöötkood';
@override
String get barcodeScan => 'Skaneeri vöötkood';
@override
String get scanBarcode => 'Skaneeri vöötkood';
@override
String get barcodeRequired => 'Vöötkood nõutav';
@override
String get minBarcodes => 'Vähemalt';
@override
String get maxBarcodes => 'Maksimum';
@override
String get scanned => 'Skaneeritud';
@override
String get scannedBarcodes => 'Skaneeritud vöötkoodid';
@override
String get barcodesRequired => 'Vöötkoodid nõutavad';
@override
String get enterBarcode => 'Sisesta vöötkood';
@override
String get barcodeEnterDescription => 'Palun sisestage vöötkoodid:';
@override
String barcodeNumberRequired(int number) => 'Vöötkood $number (nõutav)';
@override
String barcodeNumberOptional(int number) => 'Vöötkood $number (valikuline)';
@override
String get barcodeError => 'Viga vöötkoodi skaneerimisel';
@override
String get cameraError => 'Viga kaamera käivitamisel';
@override
String get cameraNotReady => 'Kaamera pole valmis või pole saadaval';
@override
String get cameraNotAvailable => 'Kaamera pole saadaval';
@override
String get cameraNotSupportedMessage => 'Kaamerat ei toetata sellel platvormil.';
@override
String get cameraNotSupportedOnPlatform => 'Sellel platvormil ei toetata';
@override
String get maxPhotosReached => 'Maksimaalne arv saavutatud';
@override
String get cameraReadyNoPreview => 'Kaamera valmis (eelvaade puudub)';
@override
String get cameraLoading => 'Kaamera laadib...';
@override
String get cameraInitializing => 'Kaamera initsialiseerimine...';
@override
String get cameraLoadingMessage => 'Palun oodake, kuni kaamera laadib';
@override
String get addPhotos => 'Lisa fotod';
@override
String get addPhotosInstruction => 'Kasutage nuppu "Vali foto", et lisada pilte kaamerast või kõvakettalt.';
@override
String get photoOf => '/';
// ==================== CHAT ====================
@override
String get typeMessage => 'Sisesta sõnum...';
@override
String get send => 'Saada';
@override
String get noSender => 'Saatja pole saadaval';
@override
String get noSenderMessage => 'Saatja pole saadaval. Palun logige uuesti sisse.';
@override
String get noRecipient => 'Vastuvõtjat pole konfigureeritud';
@override
String get noRecipientMessage => 'Selle vestluse jaoks pole vastuvõtjat konfigureeritud.';
@override
String get messageSendError => 'Sõnumi saatmine ebaõnnestus.';
@override
String get photoSendError => 'Foto saatmine ebaõnnestus.';
@override
String get photoProcessError => 'Foto töötlemine ebaõnnestus.';
@override
String get imageSendError => 'Pildi saatmine ebaõnnestus.';
@override
String get chatTypeJob => 'Töö-spetsiifiline';
@override
String get chatTypeGeneral => 'Üldine';
@override
String get jobNumber => 'Töö number';
@override
String get messages => 'Sõnumid';
@override
String get selectPhoto => 'Vali foto';
@override
String get unreadMessages => 'Lugemata sõnumid';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Kauba detailid';
@override
String get itemName => 'Kirjeldus';
@override
String get itemNumber => 'Positsiooni nr';
@override
String get item => 'Positsioon';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Kaubaosi puuduvad';
@override
String get noCargoItemsMessage => 'Selle töö jaoks pole kaubaosi määratud.';
@override
String get article => 'Artikkel';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Tee pilte';
@override
String get photosCount => 'Fotod';
@override
String get checklistPoints => 'Punktid';
@override
String get signatureRequiredText => 'Allkiri nõutav';
@override
String get scanBarcodes => 'Skaneeri vöötkoode';
@override
String get barcodeCount => 'Koodid';
@override
String get commentOptional => 'Kommentaar';
@override
String get genericTask => 'Üldine ülesanne';
@override
String get complete => 'Lõpeta';
@override
String get abort => 'Tühista';
@override
String get optional => 'Valikuline';
@override
String get skipTask => 'Vahele jätta';
// ==================== SETTINGS ====================
@override
String get language => 'Keel';
@override
String get languageChanged => 'Keel muudetud:';
@override
String get appInfo => 'RAKENDUSE INFO';
// ==================== STATUS ====================
@override
String get statusCreated => 'Loodud';
@override
String get statusAssigned => 'Määratud';
@override
String get statusInProgress => 'Töös';
@override
String get statusCompleted => 'Lõpetatud';
@override
String get priorityLow => 'Madal';
@override
String get priorityMedium => 'Keskmine';
@override
String get priorityHigh => 'Kõrge';
@override
String get priorityUrgent => 'Kiire';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsFr extends AppLocalizations {
@override
String get languageName => 'Français';
@override
String get flagEmoji => '🇫🇷';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Annuler';
@override
String get save => 'Enregistrer';
@override
String get delete => 'Supprimer';
@override
String get close => 'Fermer';
@override
String get confirm => 'Confirmer';
@override
String get error => 'Erreur';
@override
String get success => 'Succès';
@override
String get loading => 'Chargement...';
@override
String get refresh => 'Actualiser';
@override
String get version => 'Version';
@override
String get unknown => 'Inconnu';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Emplois';
@override
String get availableJobs => 'Emplois Disponibles';
@override
String get chats => 'Discussions';
@override
String get settings => 'Paramètres';
@override
String get logout => 'Déconnexion';
@override
String get logoutConfirm => 'Déconnexion';
@override
String get logoutConfirmMessage => 'Voulez-vous vraiment vous déconnecter?';
@override
String get openChat => 'Ouvrir la discussion';
@override
String get chatInfo => 'Info discussion';
@override
String get routePlan => 'Planifier l\'itinéraire';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Bon retour';
@override
String get loginSubtitle => 'Connectez-vous à votre compte';
@override
String get email => 'E-mail';
@override
String get password => 'Mot de passe';
@override
String get login => 'Connexion';
@override
String get loggingIn => 'Connexion...';
@override
String get forgotPassword => 'Mot de passe oublié?';
@override
String get forgotPasswordMessage => 'Fonction mot de passe oublié pas encore implémentée';
@override
String get loginSuccess => 'Déconnexion réussie';
@override
String get loginFailed => 'Échec de la connexion';
@override
String get connectionFailed => 'Échec de la connexion au serveur (Délai dépassé).';
@override
String get connectionTimeout => 'Échec de la connexion au serveur (Délai dépassé).';
@override
String get connecting => 'Connexion au serveur...';
@override
String get connectionError => 'Erreur de connexion';
@override
String get loginError => 'Erreur lors de la connexion';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Aucun emploi assigné';
@override
String get noJobsMessage => 'Vos emplois assignés seront affichés ici.';
@override
String get pullToRefresh => 'Tirez vers le bas pour actualiser';
@override
String get newLabel => 'NOUVEAU';
@override
String get tasksToComplete => 'Tâches à accomplir';
@override
String get pickup => 'Ramassage';
@override
String get delivery => 'Livraison';
@override
String get created => 'Créé';
@override
String get status => 'Statut';
@override
String get priority => 'Priorité';
@override
String get dueDate => 'Date d\'échéance';
@override
String get location => 'Lieu';
@override
String get description => 'Description';
@override
String get cargo => 'Cargaison';
@override
String get quantity => 'Quantité';
@override
String get weight => 'Poids';
@override
String get dimensions => 'Dimensions';
@override
String get jobDeleted => 'Emploi supprimé';
@override
String get jobDeleteError => 'Erreur lors de la suppression de l\'emploi';
@override
String get jobCompleted => 'Emploi terminé';
@override
String get from => 'De';
@override
String get to => 'à';
@override
String get jobsUpdated => 'Emplois actualisés';
@override
String get connectionRestored => 'Connexion restaurée. Chargement des emplois...';
@override
String get connectionLost => 'Connexion perdue. Hors ligne.';
@override
String get offline => 'Hors ligne';
@override
String get deleteJob => 'Supprimer l\'emploi';
@override
String get jobRemoved => 'a été supprimé';
@override
String get newJobReceived => 'Nouvel emploi reçu';
// ==================== TASKS ====================
@override
String get tasks => 'Tâches';
@override
String get noTasks => 'Aucune tâche';
@override
String get noTasksMessage => 'Aucune tâche définie pour cet emploi.';
@override
String get taskOrder => 'Ordre';
@override
String get confirmationRequired => 'Confirmation requise';
@override
String get confirmationDescription => 'Cliquez sur le bouton pour terminer la tâche.';
@override
String get checklist => 'Liste de contrôle';
@override
String get checklistDescription => 'Veuillez cocher tous les éléments:';
@override
String get completeTask => 'Terminer la tâche';
@override
String get completeTaskConfirm => 'Voulez-vous marquer cette tâche comme terminée?';
@override
String get completeTaskNote => 'Note (optionnelle)';
@override
String get taskCompleted => 'Tâche terminée';
@override
String get comment => 'Commentaire';
@override
String get commentRequired => 'Commentaire (requis)';
@override
String get enterComment => 'Saisir un commentaire';
@override
String get commentDescription => 'Veuillez saisir un commentaire:';
@override
String get finish => 'Terminer';
@override
String get signature => 'Signature';
@override
String get signatureCapture => 'Capturer la signature';
@override
String get signatureRequired => 'Veuillez capturer une signature.';
@override
String get clear => 'Effacer';
@override
String get signatureError => 'Erreur lors de l\'enregistrement de la signature';
@override
String get signatureInstruction => 'Veuillez signer dans le champ ci-dessous (souris ou doigt).';
@override
String get photoCapture => 'Prendre des photos';
@override
String get requiredPhotos => 'Photos requises';
@override
String get photosTaken => 'Prises';
@override
String get photos => 'Photos';
@override
String get takePhoto => 'Prendre une photo';
@override
String get selectFromLibrary => 'Sélectionner depuis la bibliothèque';
@override
String get retakePhoto => 'Reprendre';
@override
String get photoRequired => 'Photo requise';
@override
String get minPhotos => 'Au moins';
@override
String get maxPhotos => 'Maximum';
@override
String get photoError => 'Erreur lors de la prise de photo';
@override
String get deletePhoto => 'Supprimer la photo';
@override
String get deletePhotoConfirm => 'Voulez-vous vraiment supprimer cette photo?';
@override
String get barcode => 'Code-barres';
@override
String get barcodeScan => 'Scanner le code-barres';
@override
String get scanBarcode => 'Scanner le code-barres';
@override
String get barcodeRequired => 'Code-barres requis';
@override
String get minBarcodes => 'Au moins';
@override
String get maxBarcodes => 'Maximum';
@override
String get scanned => 'Scanné';
@override
String get scannedBarcodes => 'Codes-barres scannés';
@override
String get barcodesRequired => 'Codes-barres requis';
@override
String get enterBarcode => 'Entrer le code-barres';
@override
String get barcodeEnterDescription => 'Veuillez entrer les codes-barres:';
@override
String barcodeNumberRequired(int number) => 'Code-barres $number (requis)';
@override
String barcodeNumberOptional(int number) => 'Code-barres $number (optionnel)';
@override
String get barcodeError => 'Erreur lors du scan du code-barres';
@override
String get cameraError => 'Erreur lors de l\'initialisation de la caméra';
@override
String get cameraNotReady => 'La caméra n\'est pas prête ou non disponible';
@override
String get cameraNotAvailable => 'Caméra non disponible';
@override
String get cameraNotSupportedMessage => 'La caméra n\'est pas prise en charge sur cette plateforme.';
@override
String get cameraNotSupportedOnPlatform => 'Non supporté sur cette plateforme';
@override
String get maxPhotosReached => 'Maximum atteint';
@override
String get cameraReadyNoPreview => 'Caméra prête (sans aperçu)';
@override
String get cameraLoading => 'Chargement de la caméra...';
@override
String get cameraInitializing => 'Initialisation de la caméra...';
@override
String get cameraLoadingMessage => 'Veuillez patienter pendant le chargement de la caméra';
@override
String get addPhotos => 'Ajouter des photos';
@override
String get addPhotosInstruction => 'Utilisez le bouton "Sélectionner une photo" pour ajouter des images depuis votre appareil photo ou disque dur.';
@override
String get photoOf => 'sur';
// ==================== CHAT ====================
@override
String get typeMessage => 'Tapez un message...';
@override
String get send => 'Envoyer';
@override
String get noSender => 'Aucun expéditeur disponible';
@override
String get noSenderMessage => 'Aucun expéditeur disponible. Veuillez vous reconnecter.';
@override
String get noRecipient => 'Aucun destinataire configuré';
@override
String get noRecipientMessage => 'Aucun destinataire configuré pour cette discussion.';
@override
String get messageSendError => 'Le message n\'a pas pu être envoyé.';
@override
String get photoSendError => 'La photo n\'a pas pu être envoyée.';
@override
String get photoProcessError => 'La photo n\'a pas pu être traitée.';
@override
String get imageSendError => 'L\'image n\'a pas pu être envoyée.';
@override
String get chatTypeJob => 'Spécifique à l\'emploi';
@override
String get chatTypeGeneral => 'Général';
@override
String get jobNumber => 'Numéro d\'emploi';
@override
String get messages => 'Messages';
@override
String get selectPhoto => 'Sélectionner une photo';
@override
String get unreadMessages => 'Messages non lus';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Détails de cargaison';
@override
String get itemName => 'Description';
@override
String get itemNumber => 'N° de position';
@override
String get item => 'Position';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Aucun article de cargaison';
@override
String get noCargoItemsMessage => 'Aucun article de cargaison défini pour cet emploi.';
@override
String get article => 'Article';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Prendre des photos';
@override
String get photosCount => 'Photos';
@override
String get checklistPoints => 'Points';
@override
String get signatureRequiredText => 'Signature requise';
@override
String get scanBarcodes => 'Scanner les codes-barres';
@override
String get barcodeCount => 'Codes';
@override
String get commentOptional => 'Commentaire';
@override
String get genericTask => 'Tâche générique';
@override
String get complete => 'Terminer';
@override
String get abort => 'Annuler';
@override
String get optional => 'Facultatif';
@override
String get skipTask => 'Ignorer';
// ==================== SETTINGS ====================
@override
String get language => 'Langue';
@override
String get languageChanged => 'Langue changée en';
@override
String get appInfo => 'INFO APP';
// ==================== STATUS ====================
@override
String get statusCreated => 'Créé';
@override
String get statusAssigned => 'Assigné';
@override
String get statusInProgress => 'En cours';
@override
String get statusCompleted => 'Terminé';
@override
String get priorityLow => 'Basse';
@override
String get priorityMedium => 'Moyenne';
@override
String get priorityHigh => 'Haute';
@override
String get priorityUrgent => 'Urgente';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsLt extends AppLocalizations {
@override
String get languageName => 'Lietuvių';
@override
String get flagEmoji => '🇱🇹';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'Gerai';
@override
String get cancel => 'Atšaukti';
@override
String get save => 'Išsaugoti';
@override
String get delete => 'Ištrinti';
@override
String get close => 'Uždaryti';
@override
String get confirm => 'Patvirtinti';
@override
String get error => 'Klaida';
@override
String get success => 'Sėkmė';
@override
String get loading => 'Kraunama...';
@override
String get refresh => 'Atnaujinti';
@override
String get version => 'Versija';
@override
String get unknown => 'Nežinoma';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Darbai';
@override
String get availableJobs => 'Galimi darbai';
@override
String get chats => 'Pokalbiai';
@override
String get settings => 'Nustatymai';
@override
String get logout => 'Atsijungti';
@override
String get logoutConfirm => 'Atsijungti';
@override
String get logoutConfirmMessage => 'Ar tikrai norite atsijungti?';
@override
String get openChat => 'Atidaryti pokalbį';
@override
String get chatInfo => 'Pokalbio info';
@override
String get routePlan => 'Planuoti maršrutą';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Sveiki sugrįžę';
@override
String get loginSubtitle => 'Prisijunkite prie savo paskyros';
@override
String get email => 'El. paštas';
@override
String get password => 'Slaptažodis';
@override
String get login => 'Prisijungti';
@override
String get loggingIn => 'Jungiamasi...';
@override
String get forgotPassword => 'Pamiršote slaptažodį?';
@override
String get forgotPasswordMessage => 'Pamiršto slaptažodžio funkcija dar neįdiegta';
@override
String get loginSuccess => 'Sėkmingai atsijungta';
@override
String get loginFailed => 'Prisijungimas nepavyko';
@override
String get connectionFailed => 'Nepavyko prisijungti prie serverio (Laikas baigėsi).';
@override
String get connectionTimeout => 'Nepavyko prisijungti prie serverio (Laikas baigėsi).';
@override
String get connecting => 'Jungiamasi prie serverio...';
@override
String get connectionError => 'Ryšio klaida';
@override
String get loginError => 'Klaida prisijungiant';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Nėra priskirtų darbų';
@override
String get noJobsMessage => 'Jūsų priskirti darbai bus rodomi čia.';
@override
String get pullToRefresh => 'Patraukite žemyn, kad atnaujintumėte';
@override
String get newLabel => 'NAUJAS';
@override
String get tasksToComplete => 'Užduotys, kurias reikia atlikti';
@override
String get pickup => 'Paėmimas';
@override
String get delivery => 'Pristatymas';
@override
String get created => 'Sukurta';
@override
String get status => 'Būsena';
@override
String get priority => 'Prioritetas';
@override
String get dueDate => 'Terminas';
@override
String get location => 'Vieta';
@override
String get description => 'Aprašymas';
@override
String get cargo => 'Krovinys';
@override
String get quantity => 'Kiekis';
@override
String get weight => 'Svoris';
@override
String get dimensions => 'Matmenys';
@override
String get jobDeleted => 'Darbas ištrintas';
@override
String get jobDeleteError => 'Klaida ištrinant darbą';
@override
String get jobCompleted => 'Darbas baigtas';
@override
String get from => '';
@override
String get to => 'į';
@override
String get jobsUpdated => 'Darbai atnaujinti';
@override
String get connectionRestored => 'Ryšys atkurtas. Kraunami darbai...';
@override
String get connectionLost => 'Ryšys prarastas. Neprisijungta.';
@override
String get offline => 'Neprisijungta';
@override
String get deleteJob => 'Ištrinti darbą';
@override
String get jobRemoved => 'buvo pašalintas';
@override
String get newJobReceived => 'Gautas naujas darbas';
// ==================== TASKS ====================
@override
String get tasks => 'Užduotys';
@override
String get noTasks => 'Nėra užduočių';
@override
String get noTasksMessage => 'Šiam darbui nėra apibrėžtų užduočių.';
@override
String get taskOrder => 'Eilės tvarka';
@override
String get confirmationRequired => 'Reikalingas patvirtinimas';
@override
String get confirmationDescription => 'Spustelėkite mygtuką, kad atliktumėte užduotį.';
@override
String get checklist => 'Patikros sąrašas';
@override
String get checklistDescription => 'Prašome pažymėti visus punktus:';
@override
String get completeTask => 'Baigti užduotį';
@override
String get completeTaskConfirm => 'Ar norite pažymėti šią užduotį kaip baigtą?';
@override
String get completeTaskNote => 'Pastaba (neprivaloma)';
@override
String get taskCompleted => 'Užduotis baigta';
@override
String get comment => 'Komentaras';
@override
String get commentRequired => 'Komentaras (būtinas)';
@override
String get enterComment => 'Įveskite komentarą';
@override
String get commentDescription => 'Prašome įvesti komentarą:';
@override
String get finish => 'Baigti';
@override
String get signature => 'Parašas';
@override
String get signatureCapture => 'Įrašyti parašą';
@override
String get signatureRequired => 'Prašome įrašyti parašą.';
@override
String get clear => 'Išvalyti';
@override
String get signatureError => 'Klaida išsaugant parašą';
@override
String get signatureInstruction => 'Prašome pasirašyti laukelyje žemiau (pele arba pirštu).';
@override
String get photoCapture => 'Daryti nuotraukas';
@override
String get requiredPhotos => 'Reikalingos nuotraukos';
@override
String get photosTaken => 'Padaryta';
@override
String get photos => 'Nuotraukos';
@override
String get takePhoto => 'Daryti nuotrauką';
@override
String get selectFromLibrary => 'Pasirinkti iš bibliotekos';
@override
String get retakePhoto => 'Perdaryti';
@override
String get photoRequired => 'Reikalinga nuotrauka';
@override
String get minPhotos => 'Mažiausiai';
@override
String get maxPhotos => 'Daugiausia';
@override
String get photoError => 'Klaida darant nuotrauką';
@override
String get deletePhoto => 'Ištrinti nuotrauką';
@override
String get deletePhotoConfirm => 'Ar tikrai norite ištrinti šią nuotrauką?';
@override
String get barcode => 'Brūkšninis kodas';
@override
String get barcodeScan => 'Skaityti brūkšninį kodą';
@override
String get scanBarcode => 'Skaityti brūkšninį kodą';
@override
String get barcodeRequired => 'Reikalingas brūkšninis kodas';
@override
String get minBarcodes => 'Mažiausiai';
@override
String get maxBarcodes => 'Daugiausia';
@override
String get scanned => 'Nuskaityta';
@override
String get scannedBarcodes => 'Nuskaityti brūkšniniai kodai';
@override
String get barcodesRequired => 'Reikalingi brūkšniniai kodai';
@override
String get enterBarcode => 'Įveskite brūkšninį kodą';
@override
String get barcodeEnterDescription => 'Prašome įvesti brūkšninius kodus:';
@override
String barcodeNumberRequired(int number) => 'Brūkšninis kodas $number (būtinas)';
@override
String barcodeNumberOptional(int number) => 'Brūkšninis kodas $number (neprivalomas)';
@override
String get barcodeError => 'Klaida skaitant brūkšninį kodą';
@override
String get cameraError => 'Klaida inicializuojant kamerą';
@override
String get cameraNotReady => 'Kamera nėra pasiruošusi arba nepasiekiama';
@override
String get cameraNotAvailable => 'Kamera nepasiekiama';
@override
String get cameraNotSupportedMessage => 'Šioje platformoje kamera nepalaikoma.';
@override
String get cameraNotSupportedOnPlatform => 'Nepalaikoma šioje platformoje';
@override
String get maxPhotosReached => 'Pasiektas maksimumas';
@override
String get cameraReadyNoPreview => 'Kamera paruošta (be peržiūros)';
@override
String get cameraLoading => 'Kamera kraunama...';
@override
String get cameraInitializing => 'Kamera inicializuojama...';
@override
String get cameraLoadingMessage => 'Palaukite, kol kamera įkraunama';
@override
String get addPhotos => 'Pridėti nuotraukas';
@override
String get addPhotosInstruction => 'Naudokite mygtuką "Pasirinkti nuotrauką", norėdami pridėti vaizdų iš fotoaparato ar standžiojo disko.';
@override
String get photoOf => '';
// ==================== CHAT ====================
@override
String get typeMessage => 'Įveskite žinutę...';
@override
String get send => 'Siųsti';
@override
String get noSender => 'Siuntėjas nepasiekiamas';
@override
String get noSenderMessage => 'Siuntėjas nepasiekiamas. Prašome prisijungti dar kartą.';
@override
String get noRecipient => 'Gavėjas nesukonfigūruotas';
@override
String get noRecipientMessage => 'Šiam pokalbiui nesukonfigūruotas gavėjas.';
@override
String get messageSendError => 'Žinutės išsiųsti nepavyko.';
@override
String get photoSendError => 'Nuotraukos išsiųsti nepavyko.';
@override
String get photoProcessError => 'Nuotraukos apdoroti nepavyko.';
@override
String get imageSendError => 'Vaizdo išsiųsti nepavyko.';
@override
String get chatTypeJob => 'Specifinis darbui';
@override
String get chatTypeGeneral => 'Bendras';
@override
String get jobNumber => 'Darbo numeris';
@override
String get messages => 'Žinutės';
@override
String get selectPhoto => 'Pasirinkti nuotrauką';
@override
String get unreadMessages => 'Neskaitytos žinutės';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Krovinio detalės';
@override
String get itemName => 'Aprašymas';
@override
String get itemNumber => 'Pozicijos Nr.';
@override
String get item => 'Pozicija';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Nėra krovinių pozicijų';
@override
String get noCargoItemsMessage => 'Šiam darbui nėra apibrėžtų krovinių pozicijų.';
@override
String get article => 'Pozicija';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Daryti nuotraukas';
@override
String get photosCount => 'Nuotraukos';
@override
String get checklistPoints => 'Taškai';
@override
String get signatureRequiredText => 'Parašas būtinas';
@override
String get scanBarcodes => 'Skaityti brūkšninius kodus';
@override
String get barcodeCount => 'Kodai';
@override
String get commentOptional => 'Komentaras';
@override
String get genericTask => 'Bendra užduotis';
@override
String get complete => 'Baigti';
@override
String get abort => 'Atšaukti';
@override
String get optional => 'Neprivaloma';
@override
String get skipTask => 'Praleisti';
// ==================== SETTINGS ====================
@override
String get language => 'Kalba';
@override
String get languageChanged => 'Kalba pakeista į';
@override
String get appInfo => 'PROGRAMĖLĖS INFO';
// ==================== STATUS ====================
@override
String get statusCreated => 'Sukurta';
@override
String get statusAssigned => 'Priskirta';
@override
String get statusInProgress => 'Vykdoma';
@override
String get statusCompleted => 'Baigta';
@override
String get priorityLow => 'Žemas';
@override
String get priorityMedium => 'Vidutinis';
@override
String get priorityHigh => 'Aukštas';
@override
String get priorityUrgent => 'Skubus';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsLv extends AppLocalizations {
@override
String get languageName => 'Latviešu';
@override
String get flagEmoji => '🇱🇻';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'Labi';
@override
String get cancel => 'Atcelt';
@override
String get save => 'Saglabāt';
@override
String get delete => 'Dzēst';
@override
String get close => 'Aizvērt';
@override
String get confirm => 'Apstiprināt';
@override
String get error => 'Kļūda';
@override
String get success => 'Veiksmīgi';
@override
String get loading => 'Ielādē...';
@override
String get refresh => 'Atsvaidzināt';
@override
String get version => 'Versija';
@override
String get unknown => 'Nezināms';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Darbi';
@override
String get availableJobs => 'Pieejamie darbi';
@override
String get chats => 'Tērzēšanas';
@override
String get settings => 'Iestatījumi';
@override
String get logout => 'Iziet';
@override
String get logoutConfirm => 'Iziet';
@override
String get logoutConfirmMessage => 'Vai tiešām vēlaties iziet?';
@override
String get openChat => 'Atvērt tērzēšanu';
@override
String get chatInfo => 'Tērzēšanas info';
@override
String get routePlan => 'Plānot maršrutu';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Laipni lūgti atpakaļ';
@override
String get loginSubtitle => 'Pierakstieties savā kontā';
@override
String get email => 'E-pasts';
@override
String get password => 'Parole';
@override
String get login => 'Pierakstīties';
@override
String get loggingIn => 'Savienojas...';
@override
String get forgotPassword => 'Aizmirsāt paroli?';
@override
String get forgotPasswordMessage => 'Aizmirstās paroles funkcija vēl nav ieviesta';
@override
String get loginSuccess => 'Veiksmīgi izrakstījās';
@override
String get loginFailed => 'Pierakstīšanās neizdevās';
@override
String get connectionFailed => 'Savienojuma kļūda ar serveri (Noildze).';
@override
String get connectionTimeout => 'Savienojuma kļūda ar serveri (Noildze).';
@override
String get connecting => 'Savienojas ar serveri...';
@override
String get connectionError => 'Savienojuma kļūda';
@override
String get loginError => 'Kļūda pierakstīšanās laikā';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Nav piešķirtu darbu';
@override
String get noJobsMessage => 'Jūsu piešķirtie darbi tiks parādīti šeit.';
@override
String get pullToRefresh => 'Velciet uz leju, lai atsvaidzinātu';
@override
String get newLabel => 'JAUNS';
@override
String get tasksToComplete => 'Uzdevumi, kas jāveic';
@override
String get pickup => 'Saņemšana';
@override
String get delivery => 'Piegāde';
@override
String get created => 'Izveidots';
@override
String get status => 'Statuss';
@override
String get priority => 'Prioritāte';
@override
String get dueDate => 'Izpildes termiņš';
@override
String get location => 'Atrašanās vieta';
@override
String get description => 'Apraksts';
@override
String get cargo => 'Krava';
@override
String get quantity => 'Daudzums';
@override
String get weight => 'Svars';
@override
String get dimensions => 'Izmēri';
@override
String get jobDeleted => 'Darbs izdzēsts';
@override
String get jobDeleteError => 'Kļūda dzēšot darbu';
@override
String get jobCompleted => 'Darbs pabeigts';
@override
String get from => 'No';
@override
String get to => 'uz';
@override
String get jobsUpdated => 'Darbi atsvaidzināti';
@override
String get connectionRestored => 'Savienojums atjaunots. Ielādē darbus...';
@override
String get connectionLost => 'Savienojums pazaudēts. Bezsaistē.';
@override
String get offline => 'Bezsaistē';
@override
String get deleteJob => 'Dzēst darbu';
@override
String get jobRemoved => 'tika noņemts';
@override
String get newJobReceived => 'Saņemts jauns darbs';
// ==================== TASKS ====================
@override
String get tasks => 'Uzdevumi';
@override
String get noTasks => 'Nav uzdevumu';
@override
String get noTasksMessage => 'Šim darbam nav definētu uzdevumu.';
@override
String get taskOrder => 'Secība';
@override
String get confirmationRequired => 'Nepieciešams apstiprinājums';
@override
String get confirmationDescription => 'Noklikšķiniet uz pogas, lai pabeigtu uzdevumu.';
@override
String get checklist => 'Pārbaudes saraksts';
@override
String get checklistDescription => 'Lūdzu, atzīmējiet visus punktus:';
@override
String get completeTask => 'Pabeigt uzdevumu';
@override
String get completeTaskConfirm => 'Vai vēlaties atzīmēt šo uzdevumu kā pabeigtu?';
@override
String get completeTaskNote => 'Piezīme (neobligāta)';
@override
String get taskCompleted => 'Uzdevums pabeigts';
@override
String get comment => 'Komentārs';
@override
String get commentRequired => 'Komentārs (obligāts)';
@override
String get enterComment => 'Ievadiet komentāru';
@override
String get commentDescription => 'Lūdzu, ievadiet komentāru:';
@override
String get finish => 'Pabeigt';
@override
String get signature => 'Paraksts';
@override
String get signatureCapture => 'Uzņemt parakstu';
@override
String get signatureRequired => 'Lūdzu, uzņemiet parakstu.';
@override
String get clear => 'Notīrīt';
@override
String get signatureError => 'Kļūda saglabājot parakstu';
@override
String get signatureInstruction => 'Lūdzu parakstieties zemāk esošajā laukā (pele vai pirksts).';
@override
String get photoCapture => 'Uzņemt fotogrāfijas';
@override
String get requiredPhotos => 'Nepieciešamās fotogrāfijas';
@override
String get photosTaken => 'Uzņemtas';
@override
String get photos => 'Fotogrāfijas';
@override
String get takePhoto => 'Uzņemt fotogrāfiju';
@override
String get selectFromLibrary => 'Izvēlēties no bibliotēkas';
@override
String get retakePhoto => 'Uzņemt vēlreiz';
@override
String get photoRequired => 'Nepieciešama fotogrāfija';
@override
String get minPhotos => 'Vismaz';
@override
String get maxPhotos => 'Maksimums';
@override
String get photoError => 'Kļūda uzņemot fotogrāfiju';
@override
String get deletePhoto => 'Dzēst fotogrāfiju';
@override
String get deletePhotoConfirm => 'Vai tiešām vēlaties dzēst šo fotogrāfiju?';
@override
String get barcode => 'Svītrkods';
@override
String get barcodeScan => 'Skenēt svītrkodu';
@override
String get scanBarcode => 'Skenēt svītrkodu';
@override
String get barcodeRequired => 'Nepieciešams svītrkods';
@override
String get minBarcodes => 'Vismaz';
@override
String get maxBarcodes => 'Maksimums';
@override
String get scanned => 'Skenēts';
@override
String get scannedBarcodes => 'Skenēti svītrkodi';
@override
String get barcodesRequired => 'Nepieciešami svītrkodi';
@override
String get enterBarcode => 'Ievadiet svītrkodu';
@override
String get barcodeEnterDescription => 'Lūdzu, ievadiet svītrkodus:';
@override
String barcodeNumberRequired(int number) => 'Svītrkods $number (obligāts)';
@override
String barcodeNumberOptional(int number) => 'Svītrkods $number (neobligāts)';
@override
String get barcodeError => 'Kļūda skenējot svītrkodu';
@override
String get cameraError => 'Kļūda inicializējot kameru';
@override
String get cameraNotReady => 'Kamera nav gatava vai nav pieejama';
@override
String get cameraNotAvailable => 'Kamera nav pieejama';
@override
String get cameraNotSupportedMessage => 'Šajā platformā kamera netiek atbalstīta.';
@override
String get cameraNotSupportedOnPlatform => 'Šajā platformā netiek atbalstīts';
@override
String get maxPhotosReached => 'Maksimums sasniegts';
@override
String get cameraReadyNoPreview => 'Kamera gatava (bez priekšskatījuma)';
@override
String get cameraLoading => 'Kamera ielādē...';
@override
String get cameraInitializing => 'Kamera tiek inicializēta...';
@override
String get cameraLoadingMessage => 'Lūdzu, uzgaidiet, kamēr kamera tiek ielādēta';
@override
String get addPhotos => 'Pievienot fotogrāfijas';
@override
String get addPhotosInstruction => 'Izmantojiet pogu "Izvēlēties fotogrāfiju", lai pievienotu attēlus no kameras vai cietā diska.';
@override
String get photoOf => 'no';
// ==================== CHAT ====================
@override
String get typeMessage => 'Ierakstiet ziņojumu...';
@override
String get send => 'Sūtīt';
@override
String get noSender => 'Sūtītājs nav pieejams';
@override
String get noSenderMessage => 'Sūtītājs nav pieejams. Lūdzu, piesakieties vēlreiz.';
@override
String get noRecipient => 'Saņēmējs nav konfigurēts';
@override
String get noRecipientMessage => 'Šai tērzēšanai nav konfigurēts saņēmējs.';
@override
String get messageSendError => 'Ziņojumu neizdevās nosūtīt.';
@override
String get photoSendError => 'Fotogrāfiju neizdevās nosūtīt.';
@override
String get photoProcessError => 'Fotogrāfiju neizdevās apstrādāt.';
@override
String get imageSendError => 'Attēlu neizdevās nosūtīt.';
@override
String get chatTypeJob => 'Darba specifisks';
@override
String get chatTypeGeneral => 'Vispārējs';
@override
String get jobNumber => 'Darba numurs';
@override
String get messages => 'Ziņojumi';
@override
String get selectPhoto => 'Izvēlēties fotogrāfiju';
@override
String get unreadMessages => 'Nelasīti ziņojumi';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Kravas detaļas';
@override
String get itemName => 'Apraksts';
@override
String get itemNumber => 'Pozīcijas Nr.';
@override
String get item => 'Pozīcija';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Nav kravas pozīciju';
@override
String get noCargoItemsMessage => 'Šim darbam nav definētu kravas pozīciju.';
@override
String get article => 'Pozīcija';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Uzņemt fotogrāfijas';
@override
String get photosCount => 'Fotogrāfijas';
@override
String get checklistPoints => 'Punkti';
@override
String get signatureRequiredText => 'Paraksts nepieciešams';
@override
String get scanBarcodes => 'Skenēt svītrkodus';
@override
String get barcodeCount => 'Kodi';
@override
String get commentOptional => 'Komentārs';
@override
String get genericTask => 'Vispārējs uzdevums';
@override
String get complete => 'Pabeigt';
@override
String get abort => 'Atcelt';
@override
String get optional => 'Neobligāts';
@override
String get skipTask => 'Izlaist';
// ==================== SETTINGS ====================
@override
String get language => 'Valoda';
@override
String get languageChanged => 'Valoda mainīta uz';
@override
String get appInfo => 'LIETOTNES INFO';
// ==================== STATUS ====================
@override
String get statusCreated => 'Izveidots';
@override
String get statusAssigned => 'Piešķirts';
@override
String get statusInProgress => 'Procesā';
@override
String get statusCompleted => 'Pabeigts';
@override
String get priorityLow => 'Zema';
@override
String get priorityMedium => 'Vidēja';
@override
String get priorityHigh => 'Augsta';
@override
String get priorityUrgent => 'Steidzama';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsPl extends AppLocalizations {
@override
String get languageName => 'Polski';
@override
String get flagEmoji => '🇵🇱';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Anuluj';
@override
String get save => 'Zapisz';
@override
String get delete => 'Usuń';
@override
String get close => 'Zamknij';
@override
String get confirm => 'Potwierdź';
@override
String get error => 'Błąd';
@override
String get success => 'Sukces';
@override
String get loading => 'Ładowanie...';
@override
String get refresh => 'Odśwież';
@override
String get version => 'Wersja';
@override
String get unknown => 'Nieznany';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Zadania';
@override
String get availableJobs => 'Dostępne Zadania';
@override
String get chats => 'Czaty';
@override
String get settings => 'Ustawienia';
@override
String get logout => 'Wyloguj';
@override
String get logoutConfirm => 'Wyloguj';
@override
String get logoutConfirmMessage => 'Czy na pewno chcesz się wylogować?';
@override
String get openChat => 'Otwórz czat';
@override
String get chatInfo => 'Info o czacie';
@override
String get routePlan => 'Planuj trasę';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Witaj ponownie';
@override
String get loginSubtitle => 'Zaloguj się do swojego konta';
@override
String get email => 'E-mail';
@override
String get password => 'Hasło';
@override
String get login => 'Zaloguj';
@override
String get loggingIn => 'Łączenie...';
@override
String get forgotPassword => 'Zapomniałeś hasła?';
@override
String get forgotPasswordMessage => 'Funkcja zapomnianego hasła jeszcze nie zaimplementowana';
@override
String get loginSuccess => 'Pomyślnie wylogowano';
@override
String get loginFailed => 'Logowanie nie powiodło się';
@override
String get connectionFailed => 'Błąd połączenia z serwerem (Upłynął czas).';
@override
String get connectionTimeout => 'Błąd połączenia z serwerem (Upłynął czas).';
@override
String get connecting => 'Łączenie z serwerem...';
@override
String get connectionError => 'Błąd połączenia';
@override
String get loginError => 'Błąd podczas logowania';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Brak przypisanych zadań';
@override
String get noJobsMessage => 'Twoje przypisane zadania będą wyświetlane tutaj.';
@override
String get pullToRefresh => 'Przeciągnij w dół, aby odświeżyć';
@override
String get newLabel => 'NOWE';
@override
String get tasksToComplete => 'Zadania do wykonania';
@override
String get pickup => 'Odbiór';
@override
String get delivery => 'Dostawa';
@override
String get created => 'Utworzono';
@override
String get status => 'Status';
@override
String get priority => 'Priorytet';
@override
String get dueDate => 'Termin';
@override
String get location => 'Lokalizacja';
@override
String get description => 'Opis';
@override
String get cargo => 'Ładunek';
@override
String get quantity => 'Ilość';
@override
String get weight => 'Waga';
@override
String get dimensions => 'Wymiary';
@override
String get jobDeleted => 'Zadanie usunięte';
@override
String get jobDeleteError => 'Błąd podczas usuwania zadania';
@override
String get jobCompleted => 'Zadanie ukończone';
@override
String get from => 'Z';
@override
String get to => 'do';
@override
String get jobsUpdated => 'Zadania zaktualizowane';
@override
String get connectionRestored => 'Połączenie przywrócone. Ładowanie zadań...';
@override
String get connectionLost => 'Utracono połączenie. Offline.';
@override
String get offline => 'Offline';
@override
String get deleteJob => 'Usuń zadanie';
@override
String get jobRemoved => 'zostało usunięte';
@override
String get newJobReceived => 'Otrzymano nowe zadanie';
// ==================== TASKS ====================
@override
String get tasks => 'Zadania';
@override
String get noTasks => 'Brak zadań';
@override
String get noTasksMessage => 'Brak zdefiniowanych zadań dla tego zadania.';
@override
String get taskOrder => 'Kolejność';
@override
String get confirmationRequired => 'Wymagane potwierdzenie';
@override
String get confirmationDescription => 'Kliknij przycisk, aby ukończyć zadanie.';
@override
String get checklist => 'Lista kontrolna';
@override
String get checklistDescription => 'Proszę zaznaczyć wszystkie punkty:';
@override
String get completeTask => 'Ukończ zadanie';
@override
String get completeTaskConfirm => 'Czy chcesz oznaczyć to zadanie jako ukończone?';
@override
String get completeTaskNote => 'Notatka (opcjonalnie)';
@override
String get taskCompleted => 'Zadanie ukończone';
@override
String get comment => 'Komentarz';
@override
String get commentRequired => 'Komentarz (wymagany)';
@override
String get enterComment => 'Wprowadź komentarz';
@override
String get commentDescription => 'Proszę wprowadzić komentarz:';
@override
String get finish => 'Zakończ';
@override
String get signature => 'Podpis';
@override
String get signatureCapture => 'Przechwyć podpis';
@override
String get signatureRequired => 'Proszę przechwycić podpis.';
@override
String get clear => 'Wyczyść';
@override
String get signatureError => 'Błąd podczas zapisywania podpisu';
@override
String get signatureInstruction => 'Proszę podpisać się w polu poniżej (mysz lub palec).';
@override
String get photoCapture => 'Zrób zdjęcia';
@override
String get requiredPhotos => 'Wymagane zdjęcia';
@override
String get photosTaken => 'Wykonane';
@override
String get photos => 'Zdjęcia';
@override
String get takePhoto => 'Zrób zdjęcie';
@override
String get selectFromLibrary => 'Wybierz z biblioteki';
@override
String get retakePhoto => 'Ponów';
@override
String get photoRequired => 'Zdjęcie wymagane';
@override
String get minPhotos => 'Co najmniej';
@override
String get maxPhotos => 'Maksimum';
@override
String get photoError => 'Błąd podczas robienia zdjęcia';
@override
String get deletePhoto => 'Usuń zdjęcie';
@override
String get deletePhotoConfirm => 'Czy na pewno chcesz usunąć to zdjęcie?';
@override
String get barcode => 'Kod kreskowy';
@override
String get barcodeScan => 'Skanuj kod kreskowy';
@override
String get scanBarcode => 'Skanuj kod kreskowy';
@override
String get barcodeRequired => 'Kod kreskowy wymagany';
@override
String get minBarcodes => 'Co najmniej';
@override
String get maxBarcodes => 'Maksimum';
@override
String get scanned => 'Zeskanowano';
@override
String get scannedBarcodes => 'Zeskanowane kody kreskowe';
@override
String get barcodesRequired => 'Wymagane kody kreskowe';
@override
String get enterBarcode => 'Wprowadź kod kreskowy';
@override
String get barcodeEnterDescription => 'Proszę wprowadzić kody kreskowe:';
@override
String barcodeNumberRequired(int number) => 'Kod kreskowy $number (wymagany)';
@override
String barcodeNumberOptional(int number) => 'Kod kreskowy $number (opcjonalny)';
@override
String get barcodeError => 'Błąd podczas skanowania kodu kreskowego';
@override
String get cameraError => 'Błąd podczas inicjalizacji kamery';
@override
String get cameraNotReady => 'Kamera nie jest gotowa lub niedostępna';
@override
String get cameraNotAvailable => 'Kamera niedostępna';
@override
String get cameraNotSupportedMessage => 'Kamera nie jest obsługiwana na tej platformie.';
@override
String get cameraNotSupportedOnPlatform => 'Nieobsługiwane na tej platformie';
@override
String get maxPhotosReached => 'Maksimum osiągnięte';
@override
String get cameraReadyNoPreview => 'Kamera gotowa (bez podglądu)';
@override
String get cameraLoading => 'Kamera ładuje się...';
@override
String get cameraInitializing => 'Inicjalizacja kamery...';
@override
String get cameraLoadingMessage => 'Proszę czekać, trwa ładowanie kamery';
@override
String get addPhotos => 'Dodaj zdjęcia';
@override
String get addPhotosInstruction => 'Użyj przycisku "Wybierz zdjęcie", aby dodać obrazy z kamery lub dysku twardego.';
@override
String get photoOf => 'z';
// ==================== CHAT ====================
@override
String get typeMessage => 'Wpisz wiadomość...';
@override
String get send => 'Wyślij';
@override
String get noSender => 'Brak dostępnego nadawcy';
@override
String get noSenderMessage => 'Brak dostępnego nadawcy. Proszę zalogować się ponownie.';
@override
String get noRecipient => 'Brak skonfigurowanego odbiorcy';
@override
String get noRecipientMessage => 'Brak skonfigurowanego odbiorcy dla tego czatu.';
@override
String get messageSendError => 'Wiadomość nie mogła zostać wysłana.';
@override
String get photoSendError => 'Zdjęcie nie mogło zostać wysłane.';
@override
String get photoProcessError => 'Zdjęcie nie mogło zostać przetworzone.';
@override
String get imageSendError => 'Obraz nie mógł zostać wysłany.';
@override
String get chatTypeJob => 'Specyficzne dla zadania';
@override
String get chatTypeGeneral => 'Ogólny';
@override
String get jobNumber => 'Numer zadania';
@override
String get messages => 'Wiadomości';
@override
String get selectPhoto => 'Wybierz zdjęcie';
@override
String get unreadMessages => 'Nieprzeczytane wiadomości';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Szczegóły ładunku';
@override
String get itemName => 'Opis';
@override
String get itemNumber => 'Nr pozycji';
@override
String get item => 'Pozycja';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Brak pozycji ładunku';
@override
String get noCargoItemsMessage => 'Brak pozycji ładunku zdefiniowanych dla tego zadania.';
@override
String get article => 'Pozycja';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Zrób zdjęcia';
@override
String get photosCount => 'Zdjęcia';
@override
String get checklistPoints => 'Punkty';
@override
String get signatureRequiredText => 'Wymagany podpis';
@override
String get scanBarcodes => 'Skanuj kody kreskowe';
@override
String get barcodeCount => 'Kody';
@override
String get commentOptional => 'Komentarz';
@override
String get genericTask => 'Zadanie ogólne';
@override
String get complete => 'Zakończ';
@override
String get abort => 'Anuluj';
@override
String get optional => 'Opcjonalny';
@override
String get skipTask => 'Pomiń';
// ==================== SETTINGS ====================
@override
String get language => 'Język';
@override
String get languageChanged => 'Język zmieniony na';
@override
String get appInfo => 'INFO O APLIKACJI';
// ==================== STATUS ====================
@override
String get statusCreated => 'Utworzono';
@override
String get statusAssigned => 'Przypisano';
@override
String get statusInProgress => 'W trakcie';
@override
String get statusCompleted => 'Ukończono';
@override
String get priorityLow => 'Niski';
@override
String get priorityMedium => 'Średni';
@override
String get priorityHigh => 'Wysoki';
@override
String get priorityUrgent => 'Pilny';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsRu extends AppLocalizations {
@override
String get languageName => 'Русский';
@override
String get flagEmoji => '🇷🇺';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'OK';
@override
String get cancel => 'Отмена';
@override
String get save => 'Сохранить';
@override
String get delete => 'Удалить';
@override
String get close => 'Закрыть';
@override
String get confirm => 'Подтвердить';
@override
String get error => 'Ошибка';
@override
String get success => 'Успех';
@override
String get loading => 'Загрузка...';
@override
String get refresh => 'Обновить';
@override
String get version => 'Версия';
@override
String get unknown => 'Неизвестно';
// ==================== NAVIGATION ====================
@override
String get jobs => 'Задания';
@override
String get availableJobs => 'Доступные задания';
@override
String get chats => 'Чаты';
@override
String get settings => 'Настройки';
@override
String get logout => 'Выход';
@override
String get logoutConfirm => 'Выход';
@override
String get logoutConfirmMessage => 'Вы действительно хотите выйти?';
@override
String get openChat => 'Открыть чат';
@override
String get chatInfo => 'Информация о чате';
@override
String get routePlan => 'Планировать маршрут';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'С возвращением';
@override
String get loginSubtitle => 'Войдите в свою учетную запись';
@override
String get email => 'Эл. почта';
@override
String get password => 'Пароль';
@override
String get login => 'Войти';
@override
String get loggingIn => 'Подключение...';
@override
String get forgotPassword => 'Забыли пароль?';
@override
String get forgotPasswordMessage => 'Функция восстановления пароля еще не реализована';
@override
String get loginSuccess => 'Успешный выход из системы';
@override
String get loginFailed => 'Ошибка входа';
@override
String get connectionFailed => 'Ошибка подключения к серверу (Таймаут).';
@override
String get connectionTimeout => 'Ошибка подключения к серверу (Таймаут).';
@override
String get connecting => 'Подключение к серверу...';
@override
String get connectionError => 'Ошибка подключения';
@override
String get loginError => 'Ошибка при входе';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Нет назначенных заданий';
@override
String get noJobsMessage => 'Ваши назначенные задания будут отображаться здесь.';
@override
String get pullToRefresh => 'Потяните вниз, чтобы обновить';
@override
String get newLabel => 'НОВОЕ';
@override
String get tasksToComplete => 'Задачи для выполнения';
@override
String get pickup => 'Забор';
@override
String get delivery => 'Доставка';
@override
String get created => 'Создано';
@override
String get status => 'Статус';
@override
String get priority => 'Приоритет';
@override
String get dueDate => 'Срок выполнения';
@override
String get location => 'Местоположение';
@override
String get description => 'Описание';
@override
String get cargo => 'Груз';
@override
String get quantity => 'Количество';
@override
String get weight => 'Вес';
@override
String get dimensions => 'Размеры';
@override
String get jobDeleted => 'Задание удалено';
@override
String get jobDeleteError => 'Ошибка при удалении задания';
@override
String get jobCompleted => 'Задание завершено';
@override
String get from => 'Из';
@override
String get to => 'в';
@override
String get jobsUpdated => 'Задания обновлены';
@override
String get connectionRestored => 'Соединение восстановлено. Загрузка заданий...';
@override
String get connectionLost => 'Соединение потеряно. Офлайн.';
@override
String get offline => 'Офлайн';
@override
String get deleteJob => 'Удалить задание';
@override
String get jobRemoved => 'было удалено';
@override
String get newJobReceived => 'Получено новое задание';
// ==================== TASKS ====================
@override
String get tasks => 'Задачи';
@override
String get noTasks => 'Нет задач';
@override
String get noTasksMessage => 'Для этого задания не определены задачи.';
@override
String get taskOrder => 'Порядок';
@override
String get confirmationRequired => 'Требуется подтверждение';
@override
String get confirmationDescription => 'Нажмите кнопку, чтобы выполнить задачу.';
@override
String get checklist => 'Контрольный список';
@override
String get checklistDescription => 'Пожалуйста, отметьте все пункты:';
@override
String get completeTask => 'Завершить задачу';
@override
String get completeTaskConfirm => 'Хотите отметить эту задачу как выполненную?';
@override
String get completeTaskNote => 'Примечание (необязательно)';
@override
String get taskCompleted => 'Задача выполнена';
@override
String get comment => 'Комментарий';
@override
String get commentRequired => 'Комментарий (обязательно)';
@override
String get enterComment => 'Введите комментарий';
@override
String get commentDescription => 'Пожалуйста, введите комментарий:';
@override
String get finish => 'Готово';
@override
String get signature => 'Подпись';
@override
String get signatureCapture => 'Захватить подпись';
@override
String get signatureRequired => 'Пожалуйста, сделайте подпись.';
@override
String get clear => 'Очистить';
@override
String get signatureError => 'Ошибка при сохранении подписи';
@override
String get signatureInstruction => 'Пожалуйста, подпишитесь в поле ниже (мышь или палец).';
@override
String get photoCapture => 'Сделать фото';
@override
String get requiredPhotos => 'Необходимые фото';
@override
String get photosTaken => 'Сделано';
@override
String get photos => 'Фото';
@override
String get takePhoto => 'Сделать фото';
@override
String get selectFromLibrary => 'Выбрать из библиотеки';
@override
String get retakePhoto => 'Переснять';
@override
String get photoRequired => 'Требуется фото';
@override
String get minPhotos => 'Минимум';
@override
String get maxPhotos => 'Максимум';
@override
String get photoError => 'Ошибка при съемке фото';
@override
String get deletePhoto => 'Удалить фото';
@override
String get deletePhotoConfirm => 'Вы действительно хотите удалить это фото?';
@override
String get barcode => 'Штрих-код';
@override
String get barcodeScan => 'Сканировать штрих-код';
@override
String get scanBarcode => 'Сканировать штрих-код';
@override
String get barcodeRequired => 'Требуется штрих-код';
@override
String get minBarcodes => 'Минимум';
@override
String get maxBarcodes => 'Максимум';
@override
String get scanned => 'Отсканировано';
@override
String get scannedBarcodes => 'Отсканированные штрих-коды';
@override
String get barcodesRequired => 'Требуются штрих-коды';
@override
String get enterBarcode => 'Введите штрих-код';
@override
String get barcodeEnterDescription => 'Пожалуйста, введите штрих-коды:';
@override
String barcodeNumberRequired(int number) => 'Штрих-код $number (обязательно)';
@override
String barcodeNumberOptional(int number) => 'Штрих-код $number (необязательно)';
@override
String get barcodeError => 'Ошибка при сканировании штрих-кода';
@override
String get cameraError => 'Ошибка инициализации камеры';
@override
String get cameraNotReady => 'Камера не готова или недоступна';
@override
String get cameraNotAvailable => 'Камера недоступна';
@override
String get cameraNotSupportedMessage => 'Камера не поддерживается на этой платформе.';
@override
String get cameraNotSupportedOnPlatform => 'Не поддерживается на этой платформе';
@override
String get maxPhotosReached => 'Максимум достигнут';
@override
String get cameraReadyNoPreview => 'Камера готова (без предпросмотра)';
@override
String get cameraLoading => 'Камера загружается...';
@override
String get cameraInitializing => 'Инициализация камеры...';
@override
String get cameraLoadingMessage => 'Пожалуйста, подождите, пока загружается камера';
@override
String get addPhotos => 'Добавить фото';
@override
String get addPhotosInstruction => 'Используйте кнопку "Выбрать фото", чтобы добавить изображения с камеры или жёсткого диска.';
@override
String get photoOf => 'из';
// ==================== CHAT ====================
@override
String get typeMessage => 'Введите сообщение...';
@override
String get send => 'Отправить';
@override
String get noSender => 'Отправитель недоступен';
@override
String get noSenderMessage => 'Отправитель недоступен. Пожалуйста, войдите снова.';
@override
String get noRecipient => 'Получатель не настроен';
@override
String get noRecipientMessage => 'Получатель не настроен для этого чата.';
@override
String get messageSendError => 'Сообщение не удалось отправить.';
@override
String get photoSendError => 'Фото не удалось отправить.';
@override
String get photoProcessError => 'Фото не удалось обработать.';
@override
String get imageSendError => 'Изображение не удалось отправить.';
@override
String get chatTypeJob => 'Специфичный для задания';
@override
String get chatTypeGeneral => 'Общий';
@override
String get jobNumber => 'Номер задания';
@override
String get messages => 'Сообщения';
@override
String get selectPhoto => 'Выбрать фото';
@override
String get unreadMessages => 'Непрочитанные сообщения';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Детали груза';
@override
String get itemName => 'Описание';
@override
String get itemNumber => 'Номер позиции';
@override
String get item => 'Позиция';
@override
String get weightUnit => 'кг';
@override
String get dimensionUnit => 'см';
@override
String get noCargoItems => 'Нет позиций груза';
@override
String get noCargoItemsMessage => 'Для этого задания не определены позиции груза.';
@override
String get article => 'Позиция';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Сделать фото';
@override
String get photosCount => 'Фото';
@override
String get checklistPoints => 'Пункты';
@override
String get signatureRequiredText => 'Требуется подпись';
@override
String get scanBarcodes => 'Сканировать штрих-коды';
@override
String get barcodeCount => 'Коды';
@override
String get commentOptional => 'Комментарий';
@override
String get genericTask => 'Общая задача';
@override
String get complete => 'Завершить';
@override
String get abort => 'Отмена';
@override
String get optional => 'Необязательно';
@override
String get skipTask => 'Пропустить';
// ==================== SETTINGS ====================
@override
String get language => 'Язык';
@override
String get languageChanged => 'Язык изменен на';
@override
String get appInfo => 'ИНФОРМАЦИЯ О ПРИЛОЖЕНИИ';
// ==================== STATUS ====================
@override
String get statusCreated => 'Создано';
@override
String get statusAssigned => 'Назначено';
@override
String get statusInProgress => 'В процессе';
@override
String get statusCompleted => 'Завершено';
@override
String get priorityLow => 'Низкий';
@override
String get priorityMedium => 'Средний';
@override
String get priorityHigh => 'Высокий';
@override
String get priorityUrgent => 'Срочный';
}

View File

@@ -0,0 +1,385 @@
import 'app_localizations.dart';
class AppLocalizationsTr extends AppLocalizations {
@override
String get languageName => 'Türkçe';
@override
String get flagEmoji => '🇹🇷';
// ==================== GENERAL ====================
@override
String get appTitle => 'VotianLT App';
@override
String get ok => 'Tamam';
@override
String get cancel => 'İptal';
@override
String get save => 'Kaydet';
@override
String get delete => 'Sil';
@override
String get close => 'Kapat';
@override
String get confirm => 'Onayla';
@override
String get error => 'Hata';
@override
String get success => 'Başarılı';
@override
String get loading => 'Yükleniyor...';
@override
String get refresh => 'Yenile';
@override
String get version => 'Versiyon';
@override
String get unknown => 'Bilinmiyor';
// ==================== NAVIGATION ====================
@override
String get jobs => 'İşler';
@override
String get availableJobs => 'Mevcut İşler';
@override
String get chats => 'Sohbetler';
@override
String get settings => 'Ayarlar';
@override
String get logout => 'Çıkış';
@override
String get logoutConfirm => 'Çıkış';
@override
String get logoutConfirmMessage => 'Gerçekten çıkış yapmak istiyor musunuz?';
@override
String get openChat => 'Sohbeti aç';
@override
String get chatInfo => 'Sohbet bilgisi';
@override
String get routePlan => 'Rota planla';
// ==================== LOGIN ====================
@override
String get welcomeBack => 'Tekrar hoş geldiniz';
@override
String get loginSubtitle => 'Hesabınıza giriş yapın';
@override
String get email => 'E-posta';
@override
String get password => 'Şifre';
@override
String get login => 'Giriş';
@override
String get loggingIn => 'Bağlanıyor...';
@override
String get forgotPassword => 'Şifrenizi mi unuttunuz?';
@override
String get forgotPasswordMessage => 'Şifremi unuttum özelliği henüz uygulanmadı';
@override
String get loginSuccess => 'Başarıyla çıkış yapıldı';
@override
String get loginFailed => 'Giriş başarısız';
@override
String get connectionFailed => 'Sunucu bağlantısı başarısız (Zaman aşımı).';
@override
String get connectionTimeout => 'Sunucu bağlantısı başarısız (Zaman aşımı).';
@override
String get connecting => 'Sunucuya bağlanılıyor...';
@override
String get connectionError => 'Bağlantı hatası';
@override
String get loginError => 'Giriş sırasında hata';
// ==================== JOBS ====================
@override
String get noJobsAssigned => 'Atanmış iş yok';
@override
String get noJobsMessage => 'Atanmış işleriniz burada görüntülenecek.';
@override
String get pullToRefresh => 'Yenilemek için aşağı çekin';
@override
String get newLabel => 'YENİ';
@override
String get tasksToComplete => 'Tamamlanacak görevler';
@override
String get pickup => 'Alım';
@override
String get delivery => 'Teslimat';
@override
String get created => 'Oluşturuldu';
@override
String get status => 'Durum';
@override
String get priority => 'Öncelik';
@override
String get dueDate => 'Bitiş tarihi';
@override
String get location => 'Konum';
@override
String get description => 'ıklama';
@override
String get cargo => 'Yük';
@override
String get quantity => 'Miktar';
@override
String get weight => 'ırlık';
@override
String get dimensions => 'Boyutlar';
@override
String get jobDeleted => 'İş silindi';
@override
String get jobDeleteError => 'İş silinirken hata oluştu';
@override
String get jobCompleted => 'İş tamamlandı';
@override
String get from => 'Kimden';
@override
String get to => 'den';
@override
String get jobsUpdated => 'İşler güncellendi';
@override
String get connectionRestored => 'Bağlantı geri yüklendi. İşler yükleniyor...';
@override
String get connectionLost => 'Bağlantı kesildi. Çevrimdışı.';
@override
String get offline => 'Çevrimdışı';
@override
String get deleteJob => 'İşi sil';
@override
String get jobRemoved => 'kaldırıldı';
@override
String get newJobReceived => 'Yeni iş alındı';
// ==================== TASKS ====================
@override
String get tasks => 'Görevler';
@override
String get noTasks => 'Görev yok';
@override
String get noTasksMessage => 'Bu iş için tanımlanmış görev yok.';
@override
String get taskOrder => 'Sıra';
@override
String get confirmationRequired => 'Onay gerekli';
@override
String get confirmationDescription => 'Görevi tamamlamak için butona tıklayın.';
@override
String get checklist => 'Kontrol listesi';
@override
String get checklistDescription => 'Lütfen tüm maddeleri işaretleyin:';
@override
String get completeTask => 'Görevi tamamla';
@override
String get completeTaskConfirm => 'Bu görevi tamamlandı olarak işaretlemek istiyor musunuz?';
@override
String get completeTaskNote => 'Not (isteğe bağlı)';
@override
String get taskCompleted => 'Görev tamamlandı';
@override
String get comment => 'Yorum';
@override
String get commentRequired => 'Yorum (gerekli)';
@override
String get enterComment => 'Yorum gir';
@override
String get commentDescription => 'Lütfen bir yorum girin:';
@override
String get finish => 'Bitir';
@override
String get signature => 'İmza';
@override
String get signatureCapture => 'İmza yakalama';
@override
String get signatureRequired => 'Lütfen bir imza yakalayın.';
@override
String get clear => 'Temizle';
@override
String get signatureError => 'İmza kaydedilirken hata oluştu';
@override
String get signatureInstruction => 'Lütfen aşağıdaki alana imzanızı atın (fare veya parmak).';
@override
String get photoCapture => 'Fotoğraf çek';
@override
String get requiredPhotos => 'Gerekli fotoğraflar';
@override
String get photosTaken => 'Çekilen';
@override
String get photos => 'Fotoğraflar';
@override
String get takePhoto => 'Fotoğraf çek';
@override
String get selectFromLibrary => 'Kütüphaneden seç';
@override
String get retakePhoto => 'Tekrar çek';
@override
String get photoRequired => 'Fotoğraf gerekli';
@override
String get minPhotos => 'En az';
@override
String get maxPhotos => 'En fazla';
@override
String get photoError => 'Fotoğraf çekilirken hata oluştu';
@override
String get deletePhoto => 'Fotoğrafı sil';
@override
String get deletePhotoConfirm => 'Bu fotoğrafı gerçekten silmek istiyor musunuz?';
@override
String get barcode => 'Barkod';
@override
String get barcodeScan => 'Barkod tara';
@override
String get scanBarcode => 'Barkod tara';
@override
String get barcodeRequired => 'Barkod gerekli';
@override
String get minBarcodes => 'En az';
@override
String get maxBarcodes => 'En fazla';
@override
String get scanned => 'Tarandı';
@override
String get scannedBarcodes => 'Taranan barkodlar';
@override
String get barcodesRequired => 'Barkodlar gerekli';
@override
String get enterBarcode => 'Barkod gir';
@override
String get barcodeEnterDescription => 'Lütfen barkodları girin:';
@override
String barcodeNumberRequired(int number) => 'Barkod $number (gerekli)';
@override
String barcodeNumberOptional(int number) => 'Barkod $number (isteğe bağlı)';
@override
String get barcodeError => 'Barkod taranırken hata oluştu';
@override
String get cameraError => 'Kamera başlatılırken hata oluştu';
@override
String get cameraNotReady => 'Kamera hazır değil veya kullanılamıyor';
@override
String get cameraNotAvailable => 'Kamera kullanılamıyor';
@override
String get cameraNotSupportedMessage => 'Bu platformda kamera desteklenmiyor.';
@override
String get cameraNotSupportedOnPlatform => 'Bu platformda desteklenmiyor';
@override
String get maxPhotosReached => 'Maksimum ulaşıldı';
@override
String get cameraReadyNoPreview => 'Kamera hazır (önizleme yok)';
@override
String get cameraLoading => 'Kamera yükleniyor...';
@override
String get cameraInitializing => 'Kamera başlatılıyor...';
@override
String get cameraLoadingMessage => 'Kamera yüklenirken lütfen bekleyin';
@override
String get addPhotos => 'Fotoğraf ekle';
@override
String get addPhotosInstruction => 'Kamera veya sabit diskten görüntü eklemek için "Fotoğraf seç" düğmesini kullanın.';
@override
String get photoOf => '/';
// ==================== CHAT ====================
@override
String get typeMessage => 'Mesaj yazın...';
@override
String get send => 'Gönder';
@override
String get noSender => 'Gönderen mevcut değil';
@override
String get noSenderMessage => 'Gönderen mevcut değil. Lütfen tekrar giriş yapın.';
@override
String get noRecipient => 'Alıcı yapılandırılmamış';
@override
String get noRecipientMessage => 'Bu sohbet için alıcı yapılandırılmamış.';
@override
String get messageSendError => 'Mesaj gönderilemedi.';
@override
String get photoSendError => 'Fotoğraf gönderilemedi.';
@override
String get photoProcessError => 'Fotoğraf işlenemedi.';
@override
String get imageSendError => 'Görüntü gönderilemedi.';
@override
String get chatTypeJob => 'İşe özel';
@override
String get chatTypeGeneral => 'Genel';
@override
String get jobNumber => 'İş numarası';
@override
String get messages => 'Mesajlar';
@override
String get selectPhoto => 'Fotoğraf seç';
@override
String get unreadMessages => 'Okunmamış mesajlar';
// ==================== CARGO ====================
@override
String get cargoDetails => 'Yük Detayları';
@override
String get itemName => 'ıklama';
@override
String get itemNumber => 'Pozisyon No';
@override
String get item => 'Pozisyon';
@override
String get weightUnit => 'kg';
@override
String get dimensionUnit => 'cm';
@override
String get noCargoItems => 'Yük kalemi yok';
@override
String get noCargoItemsMessage => 'Bu iş için tanımlanmış yük kalemi yok.';
@override
String get article => 'Kalem';
// ==================== TASK TYPES ====================
@override
String get takePhotos => 'Fotoğraf çek';
@override
String get photosCount => 'Fotoğraflar';
@override
String get checklistPoints => 'Noktalar';
@override
String get signatureRequiredText => 'İmza gerekli';
@override
String get scanBarcodes => 'Barkodları tara';
@override
String get barcodeCount => 'Kodlar';
@override
String get commentOptional => 'Yorum';
@override
String get genericTask => 'Genel görev';
@override
String get complete => 'Tamamla';
@override
String get abort => 'İptal';
@override
String get optional => 'İsteğe bağlı';
@override
String get skipTask => 'Atla';
// ==================== SETTINGS ====================
@override
String get language => 'Dil';
@override
String get languageChanged => 'Dil değiştirildi:';
@override
String get appInfo => 'UYGULAMA BİLGİSİ';
// ==================== STATUS ====================
@override
String get statusCreated => 'Oluşturuldu';
@override
String get statusAssigned => 'Atandı';
@override
String get statusInProgress => 'Devam ediyor';
@override
String get statusCompleted => 'Tamamlandı';
@override
String get priorityLow => 'Düşük';
@override
String get priorityMedium => 'Orta';
@override
String get priorityHigh => 'Yüksek';
@override
String get priorityUrgent => 'Acil';
}

383
app/lib/login_view.dart Normal file
View File

@@ -0,0 +1,383 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:votianlt_app/services/developer.dart' as developer;
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'services/websocket_service.dart';
import 'services/dart_mq.dart';
import 'services/database_service.dart';
import 'app_state.dart';
import 'l10n/app_localizations.dart';
class LoginView extends StatefulWidget {
const LoginView({super.key, this.suppressConnectionSnack = false});
// If true, suppress connection-related SnackBars until the user attempts login
final bool suppressConnectionSnack;
@override
State<LoginView> createState() => _LoginViewState();
}
class _LoginViewState extends State<LoginView> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isPasswordVisible = false;
bool _isLoggingIn = false;
final StompService _stompService = StompService();
final AppState _appState = AppState();
// DartMQ subscriptions for proper cleanup
DartMQSubscription? _connectionStatusSubscription;
DartMQSubscription? _authResponseSubscription;
bool _logoutNoticeShown = false;
bool _hasNavigatedToJobs = false;
String _appVersion = '';
@override
void initState() {
super.initState();
// Pre-populate with test data
if (kDebugMode) {
_emailController.text = 'mail@svencarstensen.de';
_passwordController.text = 'test123';
}
_loadAppVersion();
_initializeStompService();
// If we came here due to logout, show only a success message and suppress other connection snacks
if (widget.suppressConnectionSnack && !_logoutNoticeShown) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_logoutNoticeShown = true;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).loginSuccess), backgroundColor: Colors.green, duration: const Duration(seconds: 1)));
});
}
}
@override
void dispose() {
// Cancel all stream subscriptions to prevent setState() after dispose
_connectionStatusSubscription?.cancel();
_authResponseSubscription?.cancel();
_emailController.dispose();
_passwordController.dispose();
// Don't dispose the singleton StompService as it may be used elsewhere
// _stompService.dispose();
super.dispose();
}
Future<void> _loadAppVersion() async {
try {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
setState(() {
_appVersion = packageInfo.version;
});
} catch (e) {
developer.log('Error loading app version: $e', name: 'LoginView');
}
}
void _initializeStompService() {
// Listen to connection status changes via dart_mq
// Note: Don't reset _isLoggingIn here - the login flow in _handleLogin
// manages button state through its own error/success handling.
_connectionStatusSubscription = DartMQ().subscribe<bool>(MQTopics.connectionStatus, (isConnected) {
if (mounted) {
setState(() {});
}
});
// Listen to authentication responses via dart_mq
_authResponseSubscription = DartMQ().subscribe<Map<String, dynamic>>(MQTopics.authResponse, (response) {
final responseTime = DateTime.now();
developer.log('=== AUTHENTICATION RESPONSE RECEIVED ===', name: 'LoginView');
developer.log('Timestamp: ${responseTime.toIso8601String()}', name: 'LoginView');
developer.log('Response data: $response', name: 'LoginView');
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() {
_isLoggingIn = false;
});
if (response['success'] == true) {
// Prevent duplicate navigation from multiple auth responses
if (_hasNavigatedToJobs) {
developer.log('Already navigated to jobs view - ignoring duplicate auth response', name: 'LoginView');
return;
}
_hasNavigatedToJobs = true;
final message = response['message'] ?? 'Anmeldung erfolgreich';
final email = _emailController.text.trim();
final password = _passwordController.text;
developer.log('=== LOGIN SUCCESS ===', name: 'LoginView');
developer.log('Email: $email', name: 'LoginView');
developer.log('Message: $message', name: 'LoginView');
// Store email as login identifier
_appState.setLoggedInEmail(email);
// Save credentials for auto-login on app restart
DatabaseService().saveCredentials(email, password);
// Navigate directly to jobs view - jobs will be loaded there
developer.log('Navigating to jobs view - jobs will be loaded there...', name: 'LoginView');
Navigator.of(context).pushReplacementNamed('/jobs');
} else {
final errorMessage = response['message'] ?? 'Unbekannter Fehler';
final errorCode = response['code'] ?? 'No code';
developer.log('=== LOGIN FAILURE ===', name: 'LoginView');
developer.log('Error message: $errorMessage', name: 'LoginView');
developer.log('Error code: $errorCode', name: 'LoginView');
developer.log('Full error response: $response', name: 'LoginView');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${AppLocalizations.of(context).loginFailed}: $errorMessage'), backgroundColor: Colors.red, duration: const Duration(seconds: 1)));
}
});
} else {
developer.log('Widget not mounted - skipping UI updates for auth response', name: 'LoginView');
}
developer.log('Authentication response processing completed', name: 'LoginView');
});
}
Future<void> _handleLogin() async {
final loginStartTime = DateTime.now();
final sessionId = loginStartTime.millisecondsSinceEpoch.toString();
developer.log('=== LOGIN ATTEMPT STARTED ===', name: 'LoginView');
developer.log('Session ID: $sessionId', name: 'LoginView');
developer.log('Timestamp: ${loginStartTime.toIso8601String()}', name: 'LoginView');
if (!_formKey.currentState!.validate()) {
developer.log('Login validation failed - form is invalid', name: 'LoginView');
return;
}
if (_isLoggingIn) {
developer.log('Login already in progress - ignoring duplicate request', name: 'LoginView');
return;
}
String email = _emailController.text.trim();
developer.log('Login attempt for email: $email', name: 'LoginView');
developer.log('Password length: ${_passwordController.text.length} characters', name: 'LoginView');
// Capture ScaffoldMessenger and localizations before any async operations
final scaffoldMessenger = ScaffoldMessenger.of(context);
final localizations = AppLocalizations.of(context);
if (!_stompService.isConnected) {
developer.log('Not connected to STOMP server - establishing connection first', name: 'LoginView');
developer.log('STOMP service connection state: ${_stompService.isConnected}', name: 'LoginView');
// Always attempt connection to fixed STOMP endpoint (no discovery gating)
// Show connecting message
if (!widget.suppressConnectionSnack) {
scaffoldMessenger.showSnackBar(SnackBar(content: Text(localizations.connecting), backgroundColor: Colors.blue, duration: const Duration(seconds: 1)));
}
// Set loading state
setState(() {
_isLoggingIn = true;
});
try {
// Start connection to STOMP server
await _stompService.connect();
// Check if already connected after connect returns
if (!_stompService.isConnected) {
// Wait for connection to be established with a timeout
try {
final completer = Completer<bool>();
final subscription = DartMQ().subscribe<bool>(MQTopics.connectionStatus, (isConnected) {
if (isConnected && !completer.isCompleted) {
completer.complete(true);
}
});
await completer.future.timeout(const Duration(seconds: 12));
subscription.cancel();
developer.log('STOMP connection established - proceeding with login', name: 'LoginView');
} on TimeoutException {
developer.log('STOMP connection timed out', name: 'LoginView');
}
} else {
developer.log('STOMP already connected after connect - proceeding with login', name: 'LoginView');
}
// Check if connection was successful
if (!_stompService.isConnected) {
setState(() {
_isLoggingIn = false;
});
scaffoldMessenger.showSnackBar(SnackBar(content: Text(localizations.connectionTimeout), backgroundColor: Colors.red, duration: const Duration(seconds: 2)));
return;
}
} catch (e, stackTrace) {
setState(() {
_isLoggingIn = false;
});
developer.log('Error connecting to STOMP server: $e', name: 'LoginView');
developer.log('Stack trace: $stackTrace', name: 'LoginView');
scaffoldMessenger.showSnackBar(SnackBar(content: Text('${localizations.connectionError}: $e'), backgroundColor: Colors.red, duration: const Duration(seconds: 1)));
return;
}
}
developer.log('Pre-login checks passed - initiating login request', name: 'LoginView');
developer.log('Connection status: connected=${_stompService.isConnected}', name: 'LoginView');
setState(() {
_isLoggingIn = true;
});
String password = _passwordController.text;
developer.log('Sending login request via STOMP service...', name: 'LoginView');
try {
// Send login request via STOMP
await _stompService.login(email, password);
final requestSentTime = DateTime.now();
final requestDuration = requestSentTime.difference(loginStartTime).inMilliseconds;
developer.log('Login request sent successfully after ${requestDuration}ms', name: 'LoginView');
} catch (e, stackTrace) {
final errorTime = DateTime.now();
final errorDuration = errorTime.difference(loginStartTime).inMilliseconds;
developer.log('LOGIN ERROR: Exception during login request after ${errorDuration}ms', name: 'LoginView');
developer.log('Error: $e', name: 'LoginView');
developer.log('Stack trace: $stackTrace', name: 'LoginView');
setState(() {
_isLoggingIn = false;
});
scaffoldMessenger.showSnackBar(SnackBar(content: Text('${localizations.loginError}: $e'), backgroundColor: Colors.red, duration: const Duration(seconds: 1)));
}
// The auth response will be handled by the stream listener
// _isLoggingIn will be set to false in the listener
developer.log('Login request phase completed - waiting for auth response', name: 'LoginView');
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: Column(
children: [
Expanded(
child: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo oder App-Name
Icon(Icons.account_circle, size: 100, color: Colors.deepPurple),
const SizedBox(height: 32),
Text(AppLocalizations.of(context).welcomeBack, style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.grey[800]), textAlign: TextAlign.center),
const SizedBox(height: 8),
Text(AppLocalizations.of(context).loginSubtitle, style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.grey[600]), textAlign: TextAlign.center),
const SizedBox(height: 32),
// E-Mail-Feld
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(labelText: 'E-Mail-Adresse', hintText: 'Geben Sie Ihre E-Mail-Adresse ein', prefixIcon: const Icon(Icons.email_outlined), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), filled: true, fillColor: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte geben Sie Ihre E-Mail-Adresse ein';
}
if (!RegExp(r'^[A-Za-z0-9_.+-]+@[A-Za-z0-9-]+\.[A-Za-z0-9.-]+$').hasMatch(value)) {
return 'Bitte geben Sie eine gültige E-Mail-Adresse ein';
}
return null;
},
),
const SizedBox(height: 16),
// Passwort-Feld
TextFormField(
controller: _passwordController,
obscureText: !_isPasswordVisible,
decoration: InputDecoration(
labelText: 'Passwort',
hintText: 'Geben Sie Ihr Passwort ein',
prefixIcon: const Icon(Icons.lock_outlined),
suffixIcon: IconButton(
icon: Icon(_isPasswordVisible ? Icons.visibility : Icons.visibility_off),
onPressed: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
filled: true,
fillColor: Colors.white,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte geben Sie Ihr Passwort ein';
}
if (value.length < 6) {
return 'Das Passwort muss mindestens 6 Zeichen lang sein';
}
return null;
},
),
const SizedBox(height: 24),
// Passwort vergessen Link
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
// Hier würde die "Passwort vergessen" Funktionalität implementiert werden
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).forgotPasswordMessage), duration: const Duration(seconds: 1)));
},
child: Text(AppLocalizations.of(context).forgotPassword, style: const TextStyle(color: Colors.deepPurple, fontWeight: FontWeight.w500)),
),
),
const SizedBox(height: 24),
// Verbindungsstatus
// Anmelden Button
ElevatedButton(onPressed: _isLoggingIn ? null : _handleLogin, style: ElevatedButton.styleFrom(backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 2), child: _isLoggingIn ? Row(mainAxisAlignment: MainAxisAlignment.center, children: const [SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2.5, valueColor: AlwaysStoppedAnimation<Color>(Colors.white))), SizedBox(width: 12), Text('Verbinden…', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600))]) : const Text('Anmelden', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600))),
const SizedBox(height: 24),
],
),
),
),
),
),
),
// Version number at the bottom
if (_appVersion.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16.0), child: Text('Version $_appVersion', style: TextStyle(fontSize: 12, color: Colors.grey[500]), textAlign: TextAlign.center)),
],
),
);
}
}

122
app/lib/main.dart Normal file
View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'login_view.dart';
import 'jobs_view.dart';
import 'cargo_items_view.dart';
import 'chats_view.dart';
import 'chat_details_view.dart';
import 'settings_view.dart';
import 'models/job.dart';
import 'models/chat.dart';
import 'services/database_service.dart';
import 'services/chat_service.dart';
import 'app_state.dart';
import 'navigation_observer.dart';
import 'services/notification_service.dart';
import 'l10n/app_localizations.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize SQLite database
await DatabaseService().initialize();
// Load data from database
await AppState().loadLoginFromDatabase();
// Load language preference
await AppState().loadLanguagePreference();
// Load jobs from database to trigger message type logging at startup
await AppState().refreshJobsFromDatabase();
// Prepare chat service before WebSocket events start flowing
await ChatService().initialize();
// Initialize notification service for local notifications with sound
await NotificationService().initialize();
// Note: WebSocket connection is initiated from the view that needs it:
// - If userId exists: JobsView initiates connection on startup
// - If no userId: LoginView initiates connection when login button is clicked
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Check if user is already logged in
final appState = AppState();
final initialRoute = appState.isLoggedIn ? '/jobs' : '/login';
return ValueListenableBuilder<Locale>(
valueListenable: localeNotifier,
builder: (context, locale, child) {
return MaterialApp(
title: 'VotianLT App',
debugShowCheckedModeBanner: false,
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true),
// Localization configuration
locale: locale,
localizationsDelegates: const [AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate],
supportedLocales: supportedLanguageCodes.map((code) => Locale(code)).toList(),
navigatorObservers: [routeObserver],
initialRoute: initialRoute,
onGenerateRoute: (settings) {
switch (settings.name) {
case '/login':
final arg = settings.arguments;
final suppress = (arg is bool) ? arg : false;
return MaterialPageRoute(builder: (_) => LoginView(suppressConnectionSnack: suppress));
case '/jobs':
return MaterialPageRoute(builder: (_) => const JobsView());
case '/cargo_items':
final job = settings.arguments as Job;
return MaterialPageRoute(builder: (_) => CargoItemsView(job: job));
case '/chats':
return MaterialPageRoute(builder: (_) => const ChatsView());
case '/chat_details':
final chat = settings.arguments as Chat;
return MaterialPageRoute(builder: (_) => ChatDetailsView(chat: chat));
case '/settings':
return MaterialPageRoute(builder: (_) => const SettingsView());
default:
return MaterialPageRoute(builder: (_) => const LoginView(suppressConnectionSnack: false));
}
},
);
},
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title)),
body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[const Text('You have pushed the button this many times:'), Text('$_counter', style: Theme.of(context).textTheme.headlineMedium)])),
floatingActionButton: FloatingActionButton(onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add)), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

View File

@@ -0,0 +1,85 @@
/// Acknowledgment message sent by client to confirm message receipt
class AcknowledgmentMessage {
/// ID of the message being acknowledged
final String messageId;
/// Status of the acknowledgment
final AcknowledgmentStatus status;
/// Timestamp when the acknowledgment was created
final DateTime timestamp;
/// Optional error message if status is FAILED
final String? errorMessage;
AcknowledgmentMessage({
required this.messageId,
required this.status,
required this.timestamp,
this.errorMessage,
});
/// Create AcknowledgmentMessage from JSON
factory AcknowledgmentMessage.fromJson(Map<String, dynamic> json) {
return AcknowledgmentMessage(
messageId: json['messageId'] as String,
status: AcknowledgmentStatus.fromString(json['status'] as String),
timestamp: DateTime.parse(json['timestamp'] as String),
errorMessage: json['errorMessage'] as String?,
);
}
/// Convert AcknowledgmentMessage to JSON
Map<String, dynamic> toJson() {
return {
'messageId': messageId,
'status': status.toString(),
'timestamp': timestamp.toIso8601String(),
if (errorMessage != null) 'errorMessage': errorMessage,
};
}
@override
String toString() {
return 'AcknowledgmentMessage(messageId: $messageId, status: $status)';
}
}
/// Status of an acknowledgment
enum AcknowledgmentStatus {
/// Message was received by the client
received,
/// Message was processed successfully
processed,
/// Message processing failed
failed;
/// Convert string to AcknowledgmentStatus
static AcknowledgmentStatus fromString(String value) {
switch (value.toUpperCase()) {
case 'RECEIVED':
return AcknowledgmentStatus.received;
case 'PROCESSED':
return AcknowledgmentStatus.processed;
case 'FAILED':
return AcknowledgmentStatus.failed;
default:
return AcknowledgmentStatus.received;
}
}
@override
String toString() {
switch (this) {
case AcknowledgmentStatus.received:
return 'RECEIVED';
case AcknowledgmentStatus.processed:
return 'PROCESSED';
case AcknowledgmentStatus.failed:
return 'FAILED';
}
}
}

View File

@@ -0,0 +1,99 @@
class CargoItem {
final String id; // Will store the timestamp as string
final String jobId; // Will store the timestamp as string
final String description;
final int quantity;
final double weightKg;
final double lengthCm;
final double widthCm;
final double heightCm;
CargoItem({
required this.id,
required this.jobId,
required this.description,
required this.quantity,
required this.weightKg,
required this.lengthCm,
required this.widthCm,
required this.heightCm,
});
static String _readString(dynamic value, {String fallback = ''}) {
if (value is String) {
return value;
}
if (value is num || value is bool) {
return value.toString();
}
return fallback;
}
factory CargoItem.fromJson(Map<String, dynamic> json) {
// Parse the complex id object - can be either a Map or a simple string
String idValue = '';
if (json['id'] is Map) {
final idMap = json['id'] as Map<String, dynamic>;
idValue = idMap['timestamp']?.toString() ?? '';
} else {
idValue = json['id']?.toString() ?? '';
}
// Parse the complex jobId object - can be either a Map or a simple string
String jobIdValue = '';
if (json['jobId'] is Map) {
final jobIdMap = json['jobId'] as Map<String, dynamic>;
jobIdValue = jobIdMap['timestamp']?.toString() ?? '';
} else {
jobIdValue = json['jobId']?.toString() ?? '';
}
return CargoItem(
id: idValue,
jobId: jobIdValue,
description: _readString(json['description']),
quantity: json['quantity'] is num ? json['quantity'].toInt() : 0,
weightKg: json['weightKg'] is num ? json['weightKg'].toDouble() : 0.0,
lengthCm: json['lengthMm'] is num ? json['lengthMm'].toDouble() : 0.0,
widthCm: json['widthMm'] is num ? json['widthMm'].toDouble() : 0.0,
heightCm: json['heightMm'] is num ? json['heightMm'].toDouble() : 0.0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'description': description,
'quantity': quantity,
'weightKg': weightKg,
'lengthMm': lengthCm,
'widthMm': widthCm,
'heightMm': heightCm,
};
}
@override
String toString() {
return 'CargoItem(id: $id, description: $description, quantity: $quantity)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CargoItem && other.id == id;
}
@override
int get hashCode => id.hashCode;
/// Get formatted dimensions string for display
String get formattedDimensions {
return '${lengthCm.toInt()} × ${widthCm.toInt()} × ${heightCm.toInt()} cm';
}
/// Get formatted weight string for display
String get formattedWeight {
return '${weightKg.toStringAsFixed(1)} kg';
}
}

162
app/lib/models/chat.dart Normal file
View File

@@ -0,0 +1,162 @@
import 'chat_message.dart';
enum ChatType { general, jobSpecific }
class Chat {
final String id;
final String title;
final String? receiver;
final ChatType type;
final String? jobId; // only for job-specific chats
final String? jobNumber; // only for job-specific chats
final List<ChatMessage> messages;
final DateTime lastMessageTime;
final String lastMessagePreview;
Chat({
required this.id,
required this.title,
this.receiver,
required this.type,
this.jobId,
this.jobNumber,
required this.messages,
required this.lastMessageTime,
required this.lastMessagePreview,
});
factory Chat.fromJson(Map<String, dynamic> json) {
final messagesList = json['messages'] as List? ?? [];
final messages =
messagesList
.map(
(messageJson) =>
ChatMessage.fromJson(messageJson as Map<String, dynamic>),
)
.toList();
return Chat(
id: json['id']?.toString() ?? '',
title: json['title']?.toString() ?? '',
receiver: json['receiver']?.toString(),
type:
json['type'] == 'jobSpecific'
? ChatType.jobSpecific
: ChatType.general,
jobId: json['jobId']?.toString(),
jobNumber: json['jobNumber']?.toString(),
messages: messages,
lastMessageTime: _resolveLastMessageTime(json, messages),
lastMessagePreview: _resolveLastMessagePreview(json, messages),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'receiver': receiver,
'type': type == ChatType.jobSpecific ? 'jobSpecific' : 'general',
'jobId': jobId,
'jobNumber': jobNumber,
'messages': messages.map((message) => message.toJson()).toList(),
'lastMessageTime': lastMessageTime.toIso8601String(),
'lastMessagePreview': lastMessagePreview,
};
}
// Factory constructor for general chat
factory Chat.general({
required String id,
required String title,
String? receiver,
required List<ChatMessage> messages,
}) {
final lastMessage = messages.isNotEmpty ? messages.last : null;
return Chat(
id: id,
title: title,
receiver: receiver,
type: ChatType.general,
messages: messages,
lastMessageTime: lastMessage?.createdAt ?? DateTime.now(),
lastMessagePreview:
lastMessage != null
? _previewForMessage(lastMessage)
: 'Noch keine Nachrichten',
);
}
// Factory constructor for job-specific chat
factory Chat.jobSpecific({
required String id,
required String jobId,
required String jobNumber,
String? receiver,
required List<ChatMessage> messages,
}) {
final lastMessage = messages.isNotEmpty ? messages.last : null;
return Chat(
id: id,
title: 'Job $jobNumber',
receiver: receiver,
type: ChatType.jobSpecific,
jobId: jobId,
jobNumber: jobNumber,
messages: messages,
lastMessageTime: lastMessage?.createdAt ?? DateTime.now(),
lastMessagePreview:
lastMessage != null
? _previewForMessage(lastMessage)
: 'Noch keine Nachrichten',
);
}
static DateTime _resolveLastMessageTime(
Map<String, dynamic> json,
List<ChatMessage> messages,
) {
if (messages.isNotEmpty) {
return messages.last.createdAt;
}
final raw = json['lastMessageTime']?.toString();
if (raw != null) {
final parsed = DateTime.tryParse(raw);
if (parsed != null) {
return parsed;
}
}
return DateTime.now();
}
static String _resolveLastMessagePreview(
Map<String, dynamic> json,
List<ChatMessage> messages,
) {
if (messages.isNotEmpty) {
return _previewForMessage(messages.last);
}
return json['lastMessagePreview']?.toString() ?? 'Noch keine Nachrichten';
}
static String _previewForMessage(ChatMessage message) {
if (message.contentType == ChatContentType.image) {
return '[Bild]';
}
return message.content;
}
@override
String toString() {
return 'Chat(id: $id, title: $title, type: $type, jobId: $jobId, messagesCount: ${messages.length})';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Chat && other.id == id;
}
@override
int get hashCode => id.hashCode;
}

View File

@@ -0,0 +1,211 @@
import 'package:votianlt_app/services/developer.dart' as developer;
enum ChatDirection { incoming, outgoing }
enum ChatMessageType { general, jobRelated }
enum ChatContentType { text, image }
ChatDirection chatDirectionFromString(
String? value, {
ChatDirection fallback = ChatDirection.incoming,
}) {
switch (value?.toUpperCase()) {
case 'CLIENT':
case 'OUTGOING':
return ChatDirection.outgoing;
case 'SERVER':
case 'INCOMING':
return ChatDirection.incoming;
default:
return fallback;
}
}
String chatDirectionToString(ChatDirection direction) {
return direction == ChatDirection.outgoing ? 'CLIENT' : 'SERVER';
}
ChatMessageType chatMessageTypeFromString(
String? value, {
ChatMessageType fallback = ChatMessageType.general,
}) {
switch (value?.toUpperCase()) {
case 'JOB_RELATED':
return ChatMessageType.jobRelated;
case 'GENERAL':
return ChatMessageType.general;
default:
return fallback;
}
}
String chatMessageTypeToString(ChatMessageType type) {
return type == ChatMessageType.jobRelated ? 'JOB_RELATED' : 'GENERAL';
}
ChatContentType chatContentTypeFromString(
String? value, {
ChatContentType fallback = ChatContentType.text,
}) {
switch (value?.toUpperCase()) {
case 'IMAGE':
return ChatContentType.image;
case 'TEXT':
return ChatContentType.text;
default:
return fallback;
}
}
String chatContentTypeToString(ChatContentType type) {
return type == ChatContentType.image ? 'IMAGE' : 'TEXT';
}
class ChatMessage {
final String id;
final String content;
final DateTime createdAt;
final ChatDirection direction;
final ChatMessageType messageType;
final ChatContentType contentType;
final String? jobId;
final String? jobNumber;
final bool read;
final bool pendingSync;
const ChatMessage({
required this.id,
required this.content,
required this.createdAt,
required this.direction,
required this.messageType,
this.contentType = ChatContentType.text,
this.jobId,
this.jobNumber,
this.read = false,
this.pendingSync = false,
});
factory ChatMessage.fromJson(Map<String, dynamic> json) {
final rawId = (json['messageId'] ?? json['id'] ?? '').toString();
final rawContent = (json['content'] ?? json['text'] ?? '').toString();
final rawContentType = json['contentType']?.toString();
DateTime createdAt;
final createdAtRaw = json['createdAt'] ?? json['timestamp'];
if (createdAtRaw is DateTime) {
createdAt = createdAtRaw;
} else {
createdAt =
DateTime.tryParse(createdAtRaw?.toString() ?? '') ?? DateTime.now();
}
var direction = chatDirectionFromString(
json['origin']?.toString() ?? json['direction']?.toString(),
fallback:
json['isOwn'] == true
? ChatDirection.outgoing
: ChatDirection.incoming,
);
var messageType = chatMessageTypeFromString(
json['messageType']?.toString(),
fallback:
((json['jobId']?.toString().isNotEmpty ?? false) ||
(json['jobNumber']?.toString().isNotEmpty ?? false))
? ChatMessageType.jobRelated
: ChatMessageType.general,
);
final jobIdRaw = json['jobId']?.toString();
final jobNumberRaw = json['jobNumber']?.toString();
final jobId = jobIdRaw?.trim();
final jobNumber = jobNumberRaw?.trim();
if (rawId.isEmpty || rawContent.isEmpty) {
developer.log(
'Invalid ChatMessage payload (one or more required fields missing): $json',
);
}
return ChatMessage(
id: rawId,
content: rawContent,
createdAt: createdAt,
direction: direction,
messageType: messageType,
contentType: chatContentTypeFromString(rawContentType),
jobId: jobId?.isEmpty ?? true ? null : jobId,
jobNumber: jobNumber?.isEmpty ?? true ? null : jobNumber,
read: json['read'] == true,
pendingSync: json['pendingSync'] == true,
);
}
Map<String, dynamic> toJson() {
final map = <String, dynamic>{
'messageId': id,
'content': content,
'origin': chatDirectionToString(direction),
'messageType': chatMessageTypeToString(messageType),
'contentType': chatContentTypeToString(contentType),
'createdAt': createdAt.toIso8601String(),
'read': read,
};
if (jobId != null && jobId!.isNotEmpty) {
map['jobId'] = jobId;
}
if (jobNumber != null && jobNumber!.isNotEmpty) {
map['jobNumber'] = jobNumber;
}
if (pendingSync) {
map['pendingSync'] = true;
}
return map;
}
bool get isOwn => direction == ChatDirection.outgoing;
ChatMessage copyWith({
String? id,
String? content,
DateTime? createdAt,
ChatDirection? direction,
ChatMessageType? messageType,
ChatContentType? contentType,
String? jobId,
String? jobNumber,
bool? read,
bool? pendingSync,
}) {
return ChatMessage(
id: id ?? this.id,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
direction: direction ?? this.direction,
messageType: messageType ?? this.messageType,
contentType: contentType ?? this.contentType,
jobId: jobId ?? this.jobId,
jobNumber: jobNumber ?? this.jobNumber,
read: read ?? this.read,
pendingSync: pendingSync ?? this.pendingSync,
);
}
@override
String toString() {
return 'ChatMessage(id: $id, direction: $direction, messageType: $messageType, contentType: $contentType, createdAt: $createdAt)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ChatMessage && other.id == id;
}
@override
int get hashCode => id.hashCode;
}

View File

@@ -0,0 +1,136 @@
import 'task.dart';
class DeliveryStation {
final int stationOrder;
final String company;
final String? salutation;
final String firstName;
final String lastName;
final String phone;
final String street;
final String houseNumber;
final String addressAddition;
final String zip;
final String city;
final String deliveryDate;
final String deliveryTime;
final List<Task> tasks;
DeliveryStation({
required this.stationOrder,
required this.company,
this.salutation,
required this.firstName,
required this.lastName,
required this.phone,
required this.street,
required this.houseNumber,
required this.addressAddition,
required this.zip,
required this.city,
required this.deliveryDate,
required this.deliveryTime,
required this.tasks,
});
static String _readString(dynamic value, {String fallback = ''}) {
if (value is String) {
return value;
}
if (value is num || value is bool) {
return value.toString();
}
return fallback;
}
factory DeliveryStation.fromJson(Map<String, dynamic> json) {
final stationOrder =
json['stationOrder'] is num
? (json['stationOrder'] as num).toInt()
: int.tryParse(json['stationOrder']?.toString() ?? '') ?? 0;
final tasks =
(json['tasks'] as List? ?? const []).map((rawTask) {
final taskJson = Map<String, dynamic>.from(rawTask as Map);
taskJson['stationOrder'] ??= stationOrder;
return Task.fromJson(taskJson);
}).toList()
..sort((a, b) => (a.taskOrder ?? 0).compareTo(b.taskOrder ?? 0));
return DeliveryStation(
stationOrder: stationOrder,
company: _readString(json['company']),
salutation: json['salutation']?.toString(),
firstName: _readString(json['firstName']),
lastName: _readString(json['lastName']),
phone: _readString(json['phone']),
street: _readString(json['street']),
houseNumber: _readString(json['houseNumber']),
addressAddition: _readString(json['addressAddition']),
zip: _readString(json['zip']),
city: _readString(json['city']),
deliveryDate: _readString(json['deliveryDate']),
deliveryTime: _readString(json['deliveryTime']),
tasks: tasks,
);
}
Map<String, dynamic> toJson() {
return {
'stationOrder': stationOrder,
'company': company,
'salutation': salutation,
'firstName': firstName,
'lastName': lastName,
'phone': phone,
'street': street,
'houseNumber': houseNumber,
'addressAddition': addressAddition,
'zip': zip,
'city': city,
'deliveryDate': deliveryDate,
'deliveryTime': deliveryTime,
'tasks': tasks.map((task) => task.toJson()).toList(),
};
}
DeliveryStation normalized() {
String t(String? value) => (value ?? '').trim();
return DeliveryStation(
stationOrder: stationOrder,
company: t(company),
salutation: t(salutation),
firstName: t(firstName),
lastName: t(lastName),
phone: t(phone),
street: t(street),
houseNumber: t(houseNumber),
addressAddition: t(addressAddition),
zip: t(zip),
city: t(city),
deliveryDate: t(deliveryDate),
deliveryTime: t(deliveryTime),
tasks: tasks,
);
}
String get displayName {
final name = [
firstName.trim(),
lastName.trim(),
].where((part) => part.isNotEmpty).join(' ');
return name.isNotEmpty ? name : company;
}
String get formattedAddress {
final streetPart = [
street.trim(),
houseNumber.trim(),
].where((part) => part.isNotEmpty).join(' ');
final cityPart = [
zip.trim(),
city.trim(),
].where((part) => part.isNotEmpty).join(' ');
return [streetPart, cityPart].where((part) => part.isNotEmpty).join(', ');
}
}

542
app/lib/models/job.dart Normal file
View File

@@ -0,0 +1,542 @@
import 'cargo_item.dart';
import 'delivery_station.dart';
import 'task.dart';
class Job {
final String id; // Will store the timestamp as string
final String jobNumber;
final String status;
final DateTime createdAt;
final DateTime updatedAt;
final String createdBy;
final String customerSelection;
final String pickupCompany;
final String? pickupSalutation;
final String pickupFirstName;
final String pickupLastName;
final String pickupPhone;
final String pickupStreet;
final String pickupHouseNumber;
final String pickupAddressAddition;
final String pickupZip;
final String pickupCity;
final String deliveryCompany;
final String? deliverySalutation;
final String deliveryFirstName;
final String deliveryLastName;
final String deliveryPhone;
final String deliveryStreet;
final String deliveryHouseNumber;
final String deliveryAddressAddition;
final String deliveryZip;
final String deliveryCity;
final bool digitalProcessing;
final String appUser;
final String pickupDate;
final String pickupTime;
final String deliveryDate;
final String deliveryTime;
final String remark;
final double price;
final bool draft;
// New fields for cargoItems and tasks
final List<CargoItem> cargoItems;
final List<DeliveryStation> deliveryStations;
final List<Task> tasks;
final String deliveryCitiesDisplay;
final String firstDeliveryCity;
final String lastDeliveryCity;
// Legacy fields for backward compatibility
final String title;
final String description;
final String priority;
final DateTime? dueDate;
final String? assignedTo;
final String? location;
final Map<String, dynamic>? additionalData;
Job({
required this.id,
required this.jobNumber,
required this.status,
required this.createdAt,
required this.updatedAt,
required this.createdBy,
required this.customerSelection,
required this.pickupCompany,
this.pickupSalutation,
required this.pickupFirstName,
required this.pickupLastName,
required this.pickupPhone,
required this.pickupStreet,
required this.pickupHouseNumber,
required this.pickupAddressAddition,
required this.pickupZip,
required this.pickupCity,
required this.deliveryCompany,
this.deliverySalutation,
required this.deliveryFirstName,
required this.deliveryLastName,
required this.deliveryPhone,
required this.deliveryStreet,
required this.deliveryHouseNumber,
required this.deliveryAddressAddition,
required this.deliveryZip,
required this.deliveryCity,
required this.digitalProcessing,
required this.appUser,
required this.pickupDate,
required this.pickupTime,
required this.deliveryDate,
required this.deliveryTime,
required this.remark,
required this.price,
required this.draft,
// New fields for cargoItems and tasks
required this.cargoItems,
required this.tasks,
this.deliveryStations = const [],
this.deliveryCitiesDisplay = '',
this.firstDeliveryCity = '',
this.lastDeliveryCity = '',
// Legacy fields
String? title,
String? description,
this.priority = 'normal',
this.dueDate,
this.assignedTo,
this.location,
this.additionalData,
}) : title = title ?? 'Job $jobNumber',
description =
description ?? 'Transport von $pickupCity nach $deliveryCity';
/// Parse DateTime from either string or array format
static DateTime? _parseDateTime(dynamic value) {
if (value == null) return null;
if (value is String) {
return DateTime.tryParse(value);
}
if (value is List && value.isNotEmpty) {
try {
// Array format: [year, month, day, hour, minute, second, nanosecond]
final year = value[0] as int;
final month = value.length > 1 ? value[1] as int : 1;
final day = value.length > 2 ? value[2] as int : 1;
final hour = value.length > 3 ? value[3] as int : 0;
final minute = value.length > 4 ? value[4] as int : 0;
final second = value.length > 5 ? value[5] as int : 0;
final nanosecond = value.length > 6 ? value[6] as int : 0;
// Convert nanoseconds to microseconds (divide by 1000)
final microsecond = nanosecond ~/ 1000;
return DateTime(year, month, day, hour, minute, second, microsecond);
} catch (e) {
return null;
}
}
return null;
}
/// Parse time string (for pickupTime/deliveryTime)
static String? _parseTimeString(dynamic value) {
if (value == null) return null;
if (value is String && value.isNotEmpty && value != 'null') {
return value;
}
return null;
}
/// Parse date string from either string or array format (for pickupDate/deliveryDate)
static String? _parseDateString(dynamic value) {
if (value == null) return null;
if (value is String) {
return value;
}
if (value is List && value.isNotEmpty) {
try {
// Array format: [year, month, day]
final year = value[0] as int;
final month = value.length > 1 ? value[1] as int : 1;
final day = value.length > 2 ? value[2] as int : 1;
// Format as ISO date string
return '${year.toString().padLeft(4, '0')}-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
} catch (e) {
return null;
}
}
return value.toString();
}
static String _readString(dynamic value, {String fallback = ''}) {
if (value is String) {
return value;
}
if (value is num || value is bool) {
return value.toString();
}
return fallback;
}
factory Job.fromJson(Map<String, dynamic> json) {
// Support both flat structure and { job: {...}, cargoItems: [...], tasks: [...] }
final jobJson =
(json['job'] is Map)
? Map<String, dynamic>.from(json['job'] as Map)
: json;
// Determine the id robustly. Prefer the inner job.id if present.
String idValue = '';
final dynamic innerId = jobJson['id'];
if (innerId is Map<String, dynamic>) {
// Some backends send an object; try common fields
idValue =
innerId['timestamp']?.toString() ??
innerId[r'$oid']?.toString() ??
'';
} else if (innerId != null) {
idValue = innerId.toString();
}
if (idValue.isEmpty) {
// Fallback to outer json['id'] if provided
final dynamic outerId = json['id'];
if (outerId is Map<String, dynamic>) {
idValue =
outerId['timestamp']?.toString() ??
outerId[r'$oid']?.toString() ??
'';
} else if (outerId != null) {
idValue = outerId.toString();
}
}
// Parse cargoItems array
List<CargoItem> cargoItems = [];
if (json['cargoItems'] is List) {
cargoItems =
(json['cargoItems'] as List)
.map(
(item) =>
CargoItem.fromJson(Map<String, dynamic>.from(item as Map)),
)
.toList();
}
// Parse delivery stations and prefer their tasks over the legacy top-level tasks.
List<DeliveryStation> deliveryStations = [];
final deliveryStationsRaw =
jobJson['deliveryStations'] ?? json['deliveryStations'];
if (deliveryStationsRaw is List) {
deliveryStations =
deliveryStationsRaw
.map(
(station) => DeliveryStation.fromJson(
Map<String, dynamic>.from(station as Map),
),
)
.toList()
..sort((a, b) => a.stationOrder.compareTo(b.stationOrder));
}
int compareTasks(Task a, Task b) {
final stationCompare = (a.stationOrder ?? -1).compareTo(
b.stationOrder ?? -1,
);
if (stationCompare != 0) {
return stationCompare;
}
return (a.taskOrder ?? 0).compareTo(b.taskOrder ?? 0);
}
// Parse tasks array
List<Task> tasks = [];
if (deliveryStations.isNotEmpty) {
tasks =
deliveryStations.expand((station) => station.tasks).toList()
..sort(compareTasks);
} else if (json['tasks'] is List) {
tasks =
(json['tasks'] as List)
.map(
(task) => Task.fromJson(Map<String, dynamic>.from(task as Map)),
)
.toList()
..sort(compareTasks);
} else if (jobJson['tasks'] is List) {
tasks =
(jobJson['tasks'] as List)
.map(
(task) => Task.fromJson(Map<String, dynamic>.from(task as Map)),
)
.toList()
..sort(compareTasks);
}
// As a last resort, derive a deterministic id if still empty (avoid UNIQUE '' collisions)
if (idValue.isEmpty) {
final jobNumber = jobJson['jobNumber']?.toString();
final createdAt = jobJson['createdAt']?.toString();
idValue =
(jobNumber?.isNotEmpty == true)
? 'jobnum:$jobNumber'
: (createdAt?.isNotEmpty == true
? 'ts:${createdAt!}'
: DateTime.now().millisecondsSinceEpoch.toString());
}
return Job(
id: idValue,
jobNumber: jobJson['jobNumber']?.toString() ?? '',
status: jobJson['status']?.toString() ?? 'UNKNOWN',
createdAt: _parseDateTime(jobJson['createdAt']) ?? DateTime.now(),
updatedAt: _parseDateTime(jobJson['updatedAt']) ?? DateTime.now(),
createdBy: jobJson['createdBy']?.toString() ?? '',
customerSelection: jobJson['customerSelection']?.toString() ?? '',
pickupCompany: jobJson['pickupCompany']?.toString() ?? '',
pickupSalutation: jobJson['pickupSalutation']?.toString(),
pickupFirstName: jobJson['pickupFirstName']?.toString() ?? '',
pickupLastName: jobJson['pickupLastName']?.toString() ?? '',
pickupPhone: jobJson['pickupPhone']?.toString() ?? '',
pickupStreet: jobJson['pickupStreet']?.toString() ?? '',
pickupHouseNumber: jobJson['pickupHouseNumber']?.toString() ?? '',
pickupAddressAddition: jobJson['pickupAddressAddition']?.toString() ?? '',
pickupZip: jobJson['pickupZip']?.toString() ?? '',
pickupCity: jobJson['pickupCity']?.toString() ?? '',
deliveryCompany: jobJson['deliveryCompany']?.toString() ?? '',
deliverySalutation: jobJson['deliverySalutation']?.toString(),
deliveryFirstName: jobJson['deliveryFirstName']?.toString() ?? '',
deliveryLastName: jobJson['deliveryLastName']?.toString() ?? '',
deliveryPhone: jobJson['deliveryPhone']?.toString() ?? '',
deliveryStreet: jobJson['deliveryStreet']?.toString() ?? '',
deliveryHouseNumber: jobJson['deliveryHouseNumber']?.toString() ?? '',
deliveryAddressAddition:
jobJson['deliveryAddressAddition']?.toString() ?? '',
deliveryZip: jobJson['deliveryZip']?.toString() ?? '',
deliveryCity: jobJson['deliveryCity']?.toString() ?? '',
digitalProcessing: jobJson['digitalProcessing'] == true,
appUser: jobJson['appUser']?.toString() ?? '',
pickupDate: _parseDateString(jobJson['pickupDate']) ?? '',
pickupTime: _parseTimeString(jobJson['pickupTime']) ?? '',
deliveryDate: _parseDateString(jobJson['deliveryDate']) ?? '',
deliveryTime: _parseTimeString(jobJson['deliveryTime']) ?? '',
remark: _readString(jobJson['remark']),
price: (jobJson['price'] is num) ? jobJson['price'].toDouble() : 0.0,
draft: jobJson['draft'] == true,
// New fields for cargoItems and tasks
cargoItems: cargoItems,
deliveryStations: deliveryStations,
tasks: tasks,
deliveryCitiesDisplay: _readString(jobJson['deliveryCitiesDisplay']),
firstDeliveryCity: _readString(jobJson['firstDeliveryCity']),
lastDeliveryCity: _readString(jobJson['lastDeliveryCity']),
// Legacy fields for backward compatibility
title: jobJson['title']?.toString(),
description: jobJson['description']?.toString(),
priority: jobJson['priority']?.toString() ?? 'normal',
dueDate: _parseDateTime(jobJson['dueDate']),
assignedTo: jobJson['assignedTo']?.toString(),
location: jobJson['location']?.toString(),
additionalData: jobJson['additionalData'] as Map<String, dynamic>?,
);
}
/// Return a normalized copy (trim strings, null→'')
Job normalized() {
String t(String? s) => (s ?? '').trim();
return Job(
id: id,
jobNumber: t(jobNumber),
status: t(status),
createdAt: createdAt,
updatedAt: updatedAt,
createdBy: t(createdBy),
customerSelection: t(customerSelection),
pickupCompany: t(pickupCompany),
pickupSalutation: t(pickupSalutation),
pickupFirstName: t(pickupFirstName),
pickupLastName: t(pickupLastName),
pickupPhone: t(pickupPhone),
pickupStreet: t(pickupStreet),
pickupHouseNumber: t(pickupHouseNumber),
pickupAddressAddition: t(pickupAddressAddition),
pickupZip: t(pickupZip),
pickupCity: t(pickupCity),
deliveryCompany: t(deliveryCompany),
deliverySalutation: t(deliverySalutation),
deliveryFirstName: t(deliveryFirstName),
deliveryLastName: t(deliveryLastName),
deliveryPhone: t(deliveryPhone),
deliveryStreet: t(deliveryStreet),
deliveryHouseNumber: t(deliveryHouseNumber),
deliveryAddressAddition: t(deliveryAddressAddition),
deliveryZip: t(deliveryZip),
deliveryCity: t(deliveryCity),
digitalProcessing: digitalProcessing,
appUser: t(appUser),
pickupDate: t(pickupDate),
pickupTime: t(pickupTime),
deliveryDate: t(deliveryDate),
deliveryTime: t(deliveryTime),
remark: t(remark),
price: price,
draft: draft,
cargoItems: cargoItems,
deliveryStations:
deliveryStations.map((station) => station.normalized()).toList(),
tasks: tasks,
deliveryCitiesDisplay: t(deliveryCitiesDisplay),
firstDeliveryCity: t(firstDeliveryCity),
lastDeliveryCity: t(lastDeliveryCity),
title: t(title),
description: t(description),
priority: t(priority),
dueDate: dueDate,
assignedTo: t(assignedTo),
location: t(location),
additionalData: additionalData,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'jobNumber': jobNumber,
'status': status,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'createdBy': createdBy,
'customerSelection': customerSelection,
'pickupCompany': pickupCompany,
'pickupSalutation': pickupSalutation,
'pickupFirstName': pickupFirstName,
'pickupLastName': pickupLastName,
'pickupPhone': pickupPhone,
'pickupStreet': pickupStreet,
'pickupHouseNumber': pickupHouseNumber,
'pickupAddressAddition': pickupAddressAddition,
'pickupZip': pickupZip,
'pickupCity': pickupCity,
'deliveryCompany': deliveryCompany,
'deliverySalutation': deliverySalutation,
'deliveryFirstName': deliveryFirstName,
'deliveryLastName': deliveryLastName,
'deliveryPhone': deliveryPhone,
'deliveryStreet': deliveryStreet,
'deliveryHouseNumber': deliveryHouseNumber,
'deliveryAddressAddition': deliveryAddressAddition,
'deliveryZip': deliveryZip,
'deliveryCity': deliveryCity,
'digitalProcessing': digitalProcessing,
'appUser': appUser,
'pickupDate': pickupDate,
'pickupTime': pickupTime,
'deliveryDate': deliveryDate,
'deliveryTime': deliveryTime,
'remark': remark,
'price': price,
'draft': draft,
// New fields for cargoItems and tasks
'cargoItems': cargoItems.map((item) => item.toJson()).toList(),
'deliveryStations':
deliveryStations.map((station) => station.toJson()).toList(),
'tasks': tasks.map((task) => task.toJson()).toList(),
'deliveryCitiesDisplay': deliveryCitiesDisplay,
'firstDeliveryCity': firstDeliveryCity,
'lastDeliveryCity': lastDeliveryCity,
// Legacy fields
'title': title,
'description': description,
'priority': priority,
'dueDate': dueDate?.toIso8601String(),
'assignedTo': assignedTo,
'location': location,
'additionalData': additionalData,
};
}
@override
String toString() {
return 'Job(id: $id, title: $title, status: $status, priority: $priority)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Job && other.id == id;
}
@override
int get hashCode => id.hashCode;
/// Get status color for UI display
String get statusColor {
switch (status.toLowerCase()) {
case 'created':
return 'orange';
case 'pending':
case 'assigned':
return 'orange';
case 'in_progress':
case 'started':
return 'blue';
case 'completed':
case 'done':
return 'green';
case 'cancelled':
case 'failed':
return 'red';
default:
return 'grey';
}
}
/// Get status display text in German
String get statusDisplayText {
switch (status.toLowerCase()) {
case 'created':
return 'Erstellt';
case 'pending':
return 'Wartend';
case 'assigned':
return 'Zugewiesen';
case 'in_progress':
case 'started':
return 'In Bearbeitung';
case 'completed':
case 'done':
return 'Abgeschlossen';
case 'cancelled':
return 'Abgebrochen';
case 'failed':
return 'Fehlgeschlagen';
default:
return status; // Show the original status if not mapped
}
}
/// Get priority display text in German
String get priorityDisplayText {
switch (priority.toLowerCase()) {
case 'low':
return 'Niedrig';
case 'normal':
return 'Normal';
case 'high':
return 'Hoch';
case 'urgent':
return 'Dringend';
default:
return 'Normal';
}
}
}

View File

@@ -0,0 +1,90 @@
/// Message Envelope for wrapping all WebSocket messages with metadata
///
/// This provides reliable message delivery with acknowledgment mechanism
class MessageEnvelope {
/// Unique message identifier (UUID)
final String messageId;
/// Timestamp when the message was created
final DateTime timestamp;
/// Target topic
final String topic;
/// Original message payload (can be Map or List)
final dynamic payload;
/// Whether this message requires acknowledgment
final bool requiresAck;
/// Number of retry attempts (for tracking)
final int retryCount;
/// Optional expiration timestamp
final DateTime? expiresAt;
MessageEnvelope({
required this.messageId,
required this.timestamp,
required this.topic,
required this.payload,
this.requiresAck = true,
this.retryCount = 0,
this.expiresAt,
});
/// Create MessageEnvelope from JSON
factory MessageEnvelope.fromJson(Map<String, dynamic> json) {
return MessageEnvelope(
messageId: json['messageId'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
topic: json['topic'] as String,
payload: json['payload'],
requiresAck: json['requiresAck'] as bool? ?? true,
retryCount: json['retryCount'] as int? ?? 0,
expiresAt: json['expiresAt'] != null
? DateTime.parse(json['expiresAt'] as String)
: null,
);
}
/// Convert MessageEnvelope to JSON
Map<String, dynamic> toJson() {
return {
'messageId': messageId,
'timestamp': timestamp.toIso8601String(),
'topic': topic,
'payload': payload,
'requiresAck': requiresAck,
'retryCount': retryCount,
if (expiresAt != null) 'expiresAt': expiresAt!.toIso8601String(),
};
}
/// Create a copy with updated fields
MessageEnvelope copyWith({
String? messageId,
DateTime? timestamp,
String? topic,
dynamic payload,
bool? requiresAck,
int? retryCount,
DateTime? expiresAt,
}) {
return MessageEnvelope(
messageId: messageId ?? this.messageId,
timestamp: timestamp ?? this.timestamp,
topic: topic ?? this.topic,
payload: payload ?? this.payload,
requiresAck: requiresAck ?? this.requiresAck,
retryCount: retryCount ?? this.retryCount,
expiresAt: expiresAt ?? this.expiresAt,
);
}
@override
String toString() {
return 'MessageEnvelope(messageId: $messageId, topic: $topic, requiresAck: $requiresAck, retryCount: $retryCount)';
}
}

View File

@@ -0,0 +1,51 @@
class QueuedMessage {
final String id;
final String topic;
final Map<String, dynamic> payload;
final DateTime createdAt;
final int retryCount;
QueuedMessage({
required this.id,
required this.topic,
required this.payload,
required this.createdAt,
this.retryCount = 0,
});
factory QueuedMessage.fromJson(Map<String, dynamic> json) {
return QueuedMessage(
id: json['id'],
topic: json['topic'],
payload: Map<String, dynamic>.from(json['payload']),
createdAt: DateTime.parse(json['createdAt']),
retryCount: json['retryCount'] ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'topic': topic,
'payload': payload,
'createdAt': createdAt.toIso8601String(),
'retryCount': retryCount,
};
}
QueuedMessage copyWith({
String? id,
String? topic,
Map<String, dynamic>? payload,
DateTime? createdAt,
int? retryCount,
}) {
return QueuedMessage(
id: id ?? this.id,
topic: topic ?? this.topic,
payload: payload ?? this.payload,
createdAt: createdAt ?? this.createdAt,
retryCount: retryCount ?? this.retryCount,
);
}
}

View File

@@ -0,0 +1,20 @@
/// Represents a translated remark in a specific language
class RemarkTranslation {
final String language;
final String text;
RemarkTranslation({required this.language, required this.text});
factory RemarkTranslation.fromJson(Map<String, dynamic> json) {
return RemarkTranslation(language: json['language']?.toString() ?? '', text: json['text']?.toString() ?? '');
}
Map<String, dynamic> toJson() {
return {'language': language, 'text': text};
}
@override
String toString() {
return 'RemarkTranslation(language: $language, text: $text)';
}
}

198
app/lib/models/task.dart Normal file
View File

@@ -0,0 +1,198 @@
// Import all task types
import 'tasks/generic_task.dart';
import 'tasks/confirmation_task.dart';
import 'tasks/photo_task.dart';
import 'tasks/todolist_task.dart';
import 'tasks/signature_task.dart';
import 'tasks/barcode_task.dart';
import 'tasks/comment_task.dart';
abstract class Task {
final String id;
final String jobId;
final int? stationOrder;
final bool completed;
final bool optional;
final DateTime? completedAt;
final String? completedBy;
final int? taskOrder;
final String? title;
final String? description;
final String? displayName;
Task({
required this.id,
required this.jobId,
this.stationOrder,
this.completed = false,
this.optional = false,
this.completedAt,
this.completedBy,
this.taskOrder,
this.title,
this.description,
this.displayName,
});
factory Task.fromJson(Map<String, dynamic> json) {
// Get task specific data to determine task type
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>?;
final taskType =
(taskSpecificData?['taskType'] ?? json['taskType'])?.toString();
// Create specific task type based on taskType
switch (taskType) {
case 'CONFIRMATION':
return ConfirmationTask.fromJson(json);
case 'PHOTO':
return PhotoTask.fromJson(json);
case 'TODOLIST':
return TodoListTask.fromJson(json);
case 'SIGNATURE':
return SignatureTask.fromJson(json);
case 'BARCODE':
return BarcodeTask.fromJson(json);
case 'COMMENT':
return CommentTask.fromJson(json);
case 'GENERIC':
return GenericTask.fromJson(json);
default:
// Fallback to a generic task if no specific type is found
return GenericTask.fromJson(json);
}
}
Map<String, dynamic> toJson();
Task copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
});
@override
String toString() {
return 'Task(id: $id, jobId: $jobId, stationOrder: $stationOrder, completed: $completed, taskOrder: $taskOrder)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Task && other.id == id;
}
@override
int get hashCode => id.hashCode;
/// Parse DateTime from either string or array format
static DateTime? _parseDateTime(dynamic value) {
if (value == null) return null;
if (value is String) {
return DateTime.tryParse(value);
}
if (value is List && value.isNotEmpty) {
try {
// Array format: [year, month, day, hour, minute, second, microsecond]
final year = value[0] as int;
final month = value.length > 1 ? value[1] as int : 1;
final day = value.length > 2 ? value[2] as int : 1;
final hour = value.length > 3 ? value[3] as int : 0;
final minute = value.length > 4 ? value[4] as int : 0;
final second = value.length > 5 ? value[5] as int : 0;
final microsecond = value.length > 6 ? value[6] as int : 0;
return DateTime(year, month, day, hour, minute, second, microsecond);
} catch (e) {
return null;
}
}
return null;
}
static String? _readOptionalString(dynamic value) {
if (value == null) {
return null;
}
if (value is String) {
return value;
}
if (value is num || value is bool) {
return value.toString();
}
return null;
}
static int? _readOptionalInt(dynamic value) {
if (value is int) {
return value;
}
if (value is num) {
return value.toInt();
}
if (value is String) {
return int.tryParse(value);
}
return null;
}
// Helper method to parse common properties
static Map<String, dynamic> parseCommonProperties(Map<String, dynamic> json) {
// Parse the complex id object - can be either a Map or a simple string
String idValue = '';
if (json['id'] is Map) {
final idMap = json['id'] as Map<String, dynamic>;
idValue = idMap['timestamp']?.toString() ?? '';
} else {
idValue = json['id']?.toString() ?? '';
}
// Parse the complex jobId object - can be either a Map or a simple string
String jobIdValue = '';
if (json['jobId'] is Map) {
final jobIdMap = json['jobId'] as Map<String, dynamic>;
jobIdValue = jobIdMap['timestamp']?.toString() ?? '';
} else {
jobIdValue = json['jobId']?.toString() ?? '';
}
// Parse completedAt using the helper method to handle both string and array formats
final completedAt = _parseDateTime(json['completedAt']);
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>?;
final stationOrder = _readOptionalInt(json['stationOrder']);
final title = _readOptionalString(
json['title'] ?? taskSpecificData?['title'],
);
final description = _readOptionalString(
json['description'] ?? taskSpecificData?['description'],
);
final displayName = _readOptionalString(
json['displayName'] ?? taskSpecificData?['displayName'],
);
return {
'id': idValue,
'jobId': jobIdValue,
'stationOrder': stationOrder,
'completed': json['completed'] ?? false,
'optional': json['optional'] ?? false,
'completedAt': completedAt,
'completedBy': json['completedBy'],
'taskOrder': json['taskOrder'],
'title': title,
'description': description,
'displayName': displayName,
};
}
}

View File

@@ -0,0 +1,99 @@
import '../task.dart';
// Barcode Task
class BarcodeTask extends Task {
final int minBarcodeCount;
final int maxBarcodeCount;
BarcodeTask({
required super.id,
required super.jobId,
required this.minBarcodeCount,
required this.maxBarcodeCount,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory BarcodeTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>;
return BarcodeTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
minBarcodeCount: taskSpecificData['minBarcodeCount'] ?? 1,
maxBarcodeCount: taskSpecificData['maxBarcodeCount'] ?? 10,
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {
'taskType': 'BARCODE',
'title': title,
'minBarcodeCount': minBarcodeCount,
'maxBarcodeCount': maxBarcodeCount,
},
};
}
@override
BarcodeTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
int? minBarcodeCount,
int? maxBarcodeCount,
}) {
return BarcodeTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
minBarcodeCount: minBarcodeCount ?? this.minBarcodeCount,
maxBarcodeCount: maxBarcodeCount ?? this.maxBarcodeCount,
);
}
}

View File

@@ -0,0 +1,99 @@
import '../task.dart';
// Comment Task
class CommentTask extends Task {
final String commentText;
final bool required;
CommentTask({
required super.id,
required super.jobId,
required this.commentText,
this.required = false,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory CommentTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>;
return CommentTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
commentText: taskSpecificData['commentText'] ?? '',
required: taskSpecificData['required'] ?? false,
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {
'taskType': 'COMMENT',
'title': title,
'commentText': commentText,
'required': required,
},
};
}
@override
CommentTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
String? commentText,
bool? required,
}) {
return CommentTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
commentText: commentText ?? this.commentText,
required: required ?? this.required,
);
}
}

View File

@@ -0,0 +1,95 @@
import '../task.dart';
// Confirmation Task
class ConfirmationTask extends Task {
final String buttonText;
ConfirmationTask({
required super.id,
required super.jobId,
required this.buttonText,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory ConfirmationTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>;
final buttonText =
taskSpecificData['buttonText']?.toString() ?? 'Bestätigen';
return ConfirmationTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
buttonText: buttonText,
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {
'taskType': 'CONFIRMATION',
'title': title,
'buttonText': buttonText,
},
};
}
@override
ConfirmationTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
String? buttonText,
}) {
return ConfirmationTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
buttonText: buttonText ?? this.buttonText,
);
}
}

View File

@@ -0,0 +1,81 @@
import '../task.dart';
// Generic Task implementation for fallback
class GenericTask extends Task {
GenericTask({
required super.id,
required super.jobId,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory GenericTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
return GenericTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {'taskType': 'GENERIC', 'title': title},
};
}
@override
GenericTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
}) {
return GenericTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
);
}
}

View File

@@ -0,0 +1,99 @@
import '../task.dart';
// Photo Task
class PhotoTask extends Task {
final int minPhotoCount;
final int maxPhotoCount;
PhotoTask({
required super.id,
required super.jobId,
required this.minPhotoCount,
required this.maxPhotoCount,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory PhotoTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>;
return PhotoTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
minPhotoCount: taskSpecificData['minPhotoCount'] ?? 1,
maxPhotoCount: taskSpecificData['maxPhotoCount'] ?? 5,
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {
'taskType': 'PHOTO',
'title': title,
'minPhotoCount': minPhotoCount,
'maxPhotoCount': maxPhotoCount,
},
};
}
@override
PhotoTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
int? minPhotoCount,
int? maxPhotoCount,
}) {
return PhotoTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
minPhotoCount: minPhotoCount ?? this.minPhotoCount,
maxPhotoCount: maxPhotoCount ?? this.maxPhotoCount,
);
}
}

View File

@@ -0,0 +1,82 @@
import '../task.dart';
// Signature Task
class SignatureTask extends Task {
SignatureTask({
required super.id,
required super.jobId,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory SignatureTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
return SignatureTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {'taskType': 'SIGNATURE', 'title': title},
};
}
@override
SignatureTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
}) {
return SignatureTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
);
}
}

View File

@@ -0,0 +1,96 @@
import '../task.dart';
// TodoList Task
class TodoListTask extends Task {
final List<String> todoItems;
TodoListTask({
required super.id,
required super.jobId,
required this.todoItems,
super.stationOrder,
super.completed = false,
super.optional = false,
super.completedAt,
super.completedBy,
super.taskOrder,
super.title,
super.description,
super.displayName,
});
factory TodoListTask.fromJson(Map<String, dynamic> json) {
final commonProps = Task.parseCommonProperties(json);
final taskSpecificData = json['taskSpecificData'] as Map<String, dynamic>;
final rawItems = taskSpecificData['todoItems'] as List? ?? [];
final todoItems = rawItems.map((item) => item?.toString() ?? '').toList();
return TodoListTask(
id: commonProps['id'],
jobId: commonProps['jobId'],
stationOrder: commonProps['stationOrder'],
completed: commonProps['completed'],
optional: commonProps['optional'],
completedAt: commonProps['completedAt'],
completedBy: commonProps['completedBy'],
taskOrder: commonProps['taskOrder'],
title: commonProps['title'],
description: commonProps['description'],
displayName: commonProps['displayName'],
todoItems: todoItems,
);
}
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'jobId': jobId,
'stationOrder': stationOrder,
'completed': completed,
'optional': optional,
'completedAt': completedAt?.toIso8601String(),
'completedBy': completedBy,
'taskOrder': taskOrder,
'description': description,
'displayName': displayName,
'taskSpecificData': {
'taskType': 'TODOLIST',
'title': title,
'todoItems': todoItems,
},
};
}
@override
TodoListTask copyWith({
String? id,
String? jobId,
int? stationOrder,
bool? completed,
bool? optional,
DateTime? completedAt,
String? completedBy,
int? taskOrder,
String? title,
String? description,
String? displayName,
List<String>? todoItems,
}) {
return TodoListTask(
id: id ?? this.id,
jobId: jobId ?? this.jobId,
stationOrder: stationOrder ?? this.stationOrder,
completed: completed ?? this.completed,
optional: optional ?? this.optional,
completedAt: completedAt ?? this.completedAt,
completedBy: completedBy ?? this.completedBy,
taskOrder: taskOrder ?? this.taskOrder,
title: title ?? this.title,
description: description ?? this.description,
displayName: displayName ?? this.displayName,
todoItems: todoItems ?? this.todoItems,
);
}
}

View File

@@ -0,0 +1,4 @@
import 'package:flutter/material.dart';
final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>();

View File

@@ -0,0 +1,281 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:7611693027744165533",
"lastPropertyId": "12:2309286538364575316",
"name": "ChatMessageEntity",
"properties": [
{
"id": "1:2877174849434222825",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:7133214518465925033",
"name": "messageId",
"indexId": "1:5239829446791635795",
"type": 9,
"flags": 2080
},
{
"id": "3:4299321820179972091",
"name": "conversationKey",
"indexId": "2:7958984541640853733",
"type": 9,
"flags": 2048
},
{
"id": "4:5939280612219671854",
"name": "content",
"type": 9
},
{
"id": "5:3937520230579179052",
"name": "contentType",
"type": 9
},
{
"id": "6:824275627423835844",
"name": "createdAt",
"indexId": "3:5224279695299690370",
"type": 10,
"flags": 8
},
{
"id": "7:4938488440801306283",
"name": "origin",
"type": 9
},
{
"id": "8:3675289388362712872",
"name": "messageType",
"type": 9
},
{
"id": "9:5014932119114547439",
"name": "jobId",
"type": 9
},
{
"id": "10:6511433113986718524",
"name": "jobNumber",
"type": 9
},
{
"id": "11:7341546288167795221",
"name": "read",
"type": 1
},
{
"id": "12:2309286538364575316",
"name": "pendingSync",
"type": 1
}
],
"relations": []
},
{
"id": "2:2377210606864651652",
"lastPropertyId": "5:4330118937838819262",
"name": "JobEntity",
"properties": [
{
"id": "1:5764695419124422056",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:5230369747102400974",
"name": "jobId",
"indexId": "4:5912728233158498728",
"type": 9,
"flags": 2080
},
{
"id": "3:8786313603756997847",
"name": "jobData",
"type": 9
},
{
"id": "4:9125996217822689005",
"name": "createdAt",
"type": 10
},
{
"id": "5:4330118937838819262",
"name": "updatedAt",
"type": 10
}
],
"relations": []
},
{
"id": "3:5853367178512403842",
"lastPropertyId": "5:7993370300880580866",
"name": "PhotoEntity",
"properties": [
{
"id": "1:4387782442589683314",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:7269827572185524897",
"name": "taskId",
"type": 9
},
{
"id": "3:117404329471165159",
"name": "photoIndex",
"type": 6
},
{
"id": "4:175585982820628578",
"name": "data",
"type": 9
},
{
"id": "5:7993370300880580866",
"name": "createdAt",
"type": 10
}
],
"relations": []
},
{
"id": "4:3098331694244942316",
"lastPropertyId": "6:8514324254890213490",
"name": "QueuedMessageEntity",
"properties": [
{
"id": "1:7708425959688472926",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:6817215813894799570",
"name": "messageId",
"indexId": "5:853241783777319657",
"type": 9,
"flags": 2080
},
{
"id": "3:60965955604348905",
"name": "topic",
"type": 9
},
{
"id": "4:4779773601253035683",
"name": "payload",
"type": 9
},
{
"id": "5:2120244644382713824",
"name": "createdAt",
"type": 10
},
{
"id": "6:8514324254890213490",
"name": "retryCount",
"type": 6
}
],
"relations": []
},
{
"id": "5:2194624907249454848",
"lastPropertyId": "6:5035828038544573244",
"name": "TaskStatusEntity",
"properties": [
{
"id": "1:2660897068318660363",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:2717300553109032772",
"name": "taskId",
"indexId": "6:3594410711639811810",
"type": 9,
"flags": 2080
},
{
"id": "3:1940249619044527831",
"name": "completed",
"type": 1
},
{
"id": "4:5172336642033012706",
"name": "completedAt",
"type": 10
},
{
"id": "5:5219138373705672631",
"name": "createdAt",
"type": 10
},
{
"id": "6:5035828038544573244",
"name": "updatedAt",
"type": 10
}
],
"relations": []
},
{
"id": "6:753334402157356597",
"lastPropertyId": "5:7622589620848481852",
"name": "UserDataEntity",
"properties": [
{
"id": "1:258617559550142129",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:3549771662483971304",
"name": "key",
"indexId": "7:8692774416314022957",
"type": 9,
"flags": 2080
},
{
"id": "3:6572116502376820780",
"name": "value",
"type": 9
},
{
"id": "4:6220239104166844131",
"name": "createdAt",
"type": 10
},
{
"id": "5:7622589620848481852",
"name": "updatedAt",
"type": 10
}
],
"relations": []
}
],
"lastEntityId": "6:753334402157356597",
"lastIndexId": "7:8692774416314022957",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [],
"retiredRelationUids": [],
"version": 1
}

943
app/lib/objectbox.g.dart Normal file
View File

@@ -0,0 +1,943 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// This code was generated by ObjectBox. To update it run the generator again
// with `dart run build_runner build`.
// See also https://docs.objectbox.io/getting-started#generate-objectbox-code
// ignore_for_file: camel_case_types, depend_on_referenced_packages
// coverage:ignore-file
import 'dart:typed_data';
import 'package:flat_buffers/flat_buffers.dart' as fb;
import 'package:objectbox/internal.dart'
as obx_int; // generated code can access "internal" functionality
import 'package:objectbox/objectbox.dart' as obx;
import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart';
import 'entities/chat_message_entity.dart';
import 'entities/job_entity.dart';
import 'entities/photo_entity.dart';
import 'entities/queued_message_entity.dart';
import 'entities/task_status_entity.dart';
import 'entities/user_data_entity.dart';
export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file
final _entities = <obx_int.ModelEntity>[
obx_int.ModelEntity(
id: const obx_int.IdUid(1, 7611693027744165533),
name: 'ChatMessageEntity',
lastPropertyId: const obx_int.IdUid(12, 2309286538364575316),
flags: 0,
properties: <obx_int.ModelProperty>[
obx_int.ModelProperty(
id: const obx_int.IdUid(1, 2877174849434222825),
name: 'id',
type: 6,
flags: 1,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(2, 7133214518465925033),
name: 'messageId',
type: 9,
flags: 2080,
indexId: const obx_int.IdUid(1, 5239829446791635795),
),
obx_int.ModelProperty(
id: const obx_int.IdUid(3, 4299321820179972091),
name: 'conversationKey',
type: 9,
flags: 2048,
indexId: const obx_int.IdUid(2, 7958984541640853733),
),
obx_int.ModelProperty(
id: const obx_int.IdUid(4, 5939280612219671854),
name: 'content',
type: 9,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(5, 3937520230579179052),
name: 'contentType',
type: 9,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(6, 824275627423835844),
name: 'createdAt',
type: 10,
flags: 8,
indexId: const obx_int.IdUid(3, 5224279695299690370),
),
obx_int.ModelProperty(
id: const obx_int.IdUid(7, 4938488440801306283),
name: 'origin',
type: 9,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(8, 3675289388362712872),
name: 'messageType',
type: 9,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(9, 5014932119114547439),
name: 'jobId',
type: 9,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(10, 6511433113986718524),
name: 'jobNumber',
type: 9,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(11, 7341546288167795221),
name: 'read',
type: 1,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(12, 2309286538364575316),
name: 'pendingSync',
type: 1,
flags: 0,
),
],
relations: <obx_int.ModelRelation>[],
backlinks: <obx_int.ModelBacklink>[],
),
obx_int.ModelEntity(
id: const obx_int.IdUid(2, 2377210606864651652),
name: 'JobEntity',
lastPropertyId: const obx_int.IdUid(5, 4330118937838819262),
flags: 0,
properties: <obx_int.ModelProperty>[
obx_int.ModelProperty(
id: const obx_int.IdUid(1, 5764695419124422056),
name: 'id',
type: 6,
flags: 1,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(2, 5230369747102400974),
name: 'jobId',
type: 9,
flags: 2080,
indexId: const obx_int.IdUid(4, 5912728233158498728),
),
obx_int.ModelProperty(
id: const obx_int.IdUid(3, 8786313603756997847),
name: 'jobData',
type: 9,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(4, 9125996217822689005),
name: 'createdAt',
type: 10,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(5, 4330118937838819262),
name: 'updatedAt',
type: 10,
flags: 0,
),
],
relations: <obx_int.ModelRelation>[],
backlinks: <obx_int.ModelBacklink>[],
),
obx_int.ModelEntity(
id: const obx_int.IdUid(3, 5853367178512403842),
name: 'PhotoEntity',
lastPropertyId: const obx_int.IdUid(5, 7993370300880580866),
flags: 0,
properties: <obx_int.ModelProperty>[
obx_int.ModelProperty(
id: const obx_int.IdUid(1, 4387782442589683314),
name: 'id',
type: 6,
flags: 1,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(2, 7269827572185524897),
name: 'taskId',
type: 9,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(3, 117404329471165159),
name: 'photoIndex',
type: 6,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(4, 175585982820628578),
name: 'data',
type: 9,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(5, 7993370300880580866),
name: 'createdAt',
type: 10,
flags: 0,
),
],
relations: <obx_int.ModelRelation>[],
backlinks: <obx_int.ModelBacklink>[],
),
obx_int.ModelEntity(
id: const obx_int.IdUid(4, 3098331694244942316),
name: 'QueuedMessageEntity',
lastPropertyId: const obx_int.IdUid(6, 8514324254890213490),
flags: 0,
properties: <obx_int.ModelProperty>[
obx_int.ModelProperty(
id: const obx_int.IdUid(1, 7708425959688472926),
name: 'id',
type: 6,
flags: 1,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(2, 6817215813894799570),
name: 'messageId',
type: 9,
flags: 2080,
indexId: const obx_int.IdUid(5, 853241783777319657),
),
obx_int.ModelProperty(
id: const obx_int.IdUid(3, 60965955604348905),
name: 'topic',
type: 9,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(4, 4779773601253035683),
name: 'payload',
type: 9,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(5, 2120244644382713824),
name: 'createdAt',
type: 10,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(6, 8514324254890213490),
name: 'retryCount',
type: 6,
flags: 0,
),
],
relations: <obx_int.ModelRelation>[],
backlinks: <obx_int.ModelBacklink>[],
),
obx_int.ModelEntity(
id: const obx_int.IdUid(5, 2194624907249454848),
name: 'TaskStatusEntity',
lastPropertyId: const obx_int.IdUid(6, 5035828038544573244),
flags: 0,
properties: <obx_int.ModelProperty>[
obx_int.ModelProperty(
id: const obx_int.IdUid(1, 2660897068318660363),
name: 'id',
type: 6,
flags: 1,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(2, 2717300553109032772),
name: 'taskId',
type: 9,
flags: 2080,
indexId: const obx_int.IdUid(6, 3594410711639811810),
),
obx_int.ModelProperty(
id: const obx_int.IdUid(3, 1940249619044527831),
name: 'completed',
type: 1,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(4, 5172336642033012706),
name: 'completedAt',
type: 10,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(5, 5219138373705672631),
name: 'createdAt',
type: 10,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(6, 5035828038544573244),
name: 'updatedAt',
type: 10,
flags: 0,
),
],
relations: <obx_int.ModelRelation>[],
backlinks: <obx_int.ModelBacklink>[],
),
obx_int.ModelEntity(
id: const obx_int.IdUid(6, 753334402157356597),
name: 'UserDataEntity',
lastPropertyId: const obx_int.IdUid(5, 7622589620848481852),
flags: 0,
properties: <obx_int.ModelProperty>[
obx_int.ModelProperty(
id: const obx_int.IdUid(1, 258617559550142129),
name: 'id',
type: 6,
flags: 1,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(2, 3549771662483971304),
name: 'key',
type: 9,
flags: 2080,
indexId: const obx_int.IdUid(7, 8692774416314022957),
),
obx_int.ModelProperty(
id: const obx_int.IdUid(3, 6572116502376820780),
name: 'value',
type: 9,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(4, 6220239104166844131),
name: 'createdAt',
type: 10,
flags: 0,
),
obx_int.ModelProperty(
id: const obx_int.IdUid(5, 7622589620848481852),
name: 'updatedAt',
type: 10,
flags: 0,
),
],
relations: <obx_int.ModelRelation>[],
backlinks: <obx_int.ModelBacklink>[],
),
];
/// Shortcut for [obx.Store.new] that passes [getObjectBoxModel] and for Flutter
/// apps by default a [directory] using `defaultStoreDirectory()` from the
/// ObjectBox Flutter library.
///
/// Note: for desktop apps it is recommended to specify a unique [directory].
///
/// See [obx.Store.new] for an explanation of all parameters.
///
/// For Flutter apps, also calls `loadObjectBoxLibraryAndroidCompat()` from
/// the ObjectBox Flutter library to fix loading the native ObjectBox library
/// on Android 6 and older.
Future<obx.Store> openStore({
String? directory,
int? maxDBSizeInKB,
int? maxDataSizeInKB,
int? fileMode,
int? maxReaders,
bool queriesCaseSensitiveDefault = true,
String? macosApplicationGroup,
}) async {
await loadObjectBoxLibraryAndroidCompat();
return obx.Store(
getObjectBoxModel(),
directory: directory ?? (await defaultStoreDirectory()).path,
maxDBSizeInKB: maxDBSizeInKB,
maxDataSizeInKB: maxDataSizeInKB,
fileMode: fileMode,
maxReaders: maxReaders,
queriesCaseSensitiveDefault: queriesCaseSensitiveDefault,
macosApplicationGroup: macosApplicationGroup,
);
}
/// Returns the ObjectBox model definition for this project for use with
/// [obx.Store.new].
obx_int.ModelDefinition getObjectBoxModel() {
final model = obx_int.ModelInfo(
entities: _entities,
lastEntityId: const obx_int.IdUid(6, 753334402157356597),
lastIndexId: const obx_int.IdUid(7, 8692774416314022957),
lastRelationId: const obx_int.IdUid(0, 0),
lastSequenceId: const obx_int.IdUid(0, 0),
retiredEntityUids: const [],
retiredIndexUids: const [],
retiredPropertyUids: const [],
retiredRelationUids: const [],
modelVersion: 5,
modelVersionParserMinimum: 5,
version: 1,
);
final bindings = <Type, obx_int.EntityDefinition>{
ChatMessageEntity: obx_int.EntityDefinition<ChatMessageEntity>(
model: _entities[0],
toOneRelations: (ChatMessageEntity object) => [],
toManyRelations: (ChatMessageEntity object) => {},
getId: (ChatMessageEntity object) => object.id,
setId: (ChatMessageEntity object, int id) {
object.id = id;
},
objectToFB: (ChatMessageEntity object, fb.Builder fbb) {
final messageIdOffset = fbb.writeString(object.messageId);
final conversationKeyOffset = fbb.writeString(object.conversationKey);
final contentOffset = fbb.writeString(object.content);
final contentTypeOffset = fbb.writeString(object.contentType);
final originOffset = fbb.writeString(object.origin);
final messageTypeOffset = fbb.writeString(object.messageType);
final jobIdOffset = object.jobId == null
? null
: fbb.writeString(object.jobId!);
final jobNumberOffset = object.jobNumber == null
? null
: fbb.writeString(object.jobNumber!);
fbb.startTable(13);
fbb.addInt64(0, object.id);
fbb.addOffset(1, messageIdOffset);
fbb.addOffset(2, conversationKeyOffset);
fbb.addOffset(3, contentOffset);
fbb.addOffset(4, contentTypeOffset);
fbb.addInt64(5, object.createdAt.millisecondsSinceEpoch);
fbb.addOffset(6, originOffset);
fbb.addOffset(7, messageTypeOffset);
fbb.addOffset(8, jobIdOffset);
fbb.addOffset(9, jobNumberOffset);
fbb.addBool(10, object.read);
fbb.addBool(11, object.pendingSync);
fbb.finish(fbb.endTable());
return object.id;
},
objectFromFB: (obx.Store store, ByteData fbData) {
final buffer = fb.BufferContext(fbData);
final rootOffset = buffer.derefObject(0);
final messageIdParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 6, '');
final conversationKeyParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 8, '');
final contentParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 10, '');
final contentTypeParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 12, '');
final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
const fb.Int64Reader().vTableGet(buffer, rootOffset, 14, 0),
);
final originParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 16, '');
final messageTypeParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 18, '');
final jobIdParam = const fb.StringReader(
asciiOptimization: true,
).vTableGetNullable(buffer, rootOffset, 20);
final jobNumberParam = const fb.StringReader(
asciiOptimization: true,
).vTableGetNullable(buffer, rootOffset, 22);
final readParam = const fb.BoolReader().vTableGet(
buffer,
rootOffset,
24,
false,
);
final pendingSyncParam = const fb.BoolReader().vTableGet(
buffer,
rootOffset,
26,
false,
);
final object = ChatMessageEntity(
messageId: messageIdParam,
conversationKey: conversationKeyParam,
content: contentParam,
contentType: contentTypeParam,
createdAt: createdAtParam,
origin: originParam,
messageType: messageTypeParam,
jobId: jobIdParam,
jobNumber: jobNumberParam,
read: readParam,
pendingSync: pendingSyncParam,
)..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
return object;
},
),
JobEntity: obx_int.EntityDefinition<JobEntity>(
model: _entities[1],
toOneRelations: (JobEntity object) => [],
toManyRelations: (JobEntity object) => {},
getId: (JobEntity object) => object.id,
setId: (JobEntity object, int id) {
object.id = id;
},
objectToFB: (JobEntity object, fb.Builder fbb) {
final jobIdOffset = fbb.writeString(object.jobId);
final jobDataOffset = fbb.writeString(object.jobData);
fbb.startTable(6);
fbb.addInt64(0, object.id);
fbb.addOffset(1, jobIdOffset);
fbb.addOffset(2, jobDataOffset);
fbb.addInt64(3, object.createdAt.millisecondsSinceEpoch);
fbb.addInt64(4, object.updatedAt.millisecondsSinceEpoch);
fbb.finish(fbb.endTable());
return object.id;
},
objectFromFB: (obx.Store store, ByteData fbData) {
final buffer = fb.BufferContext(fbData);
final rootOffset = buffer.derefObject(0);
final jobIdParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 6, '');
final jobDataParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 8, '');
final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0),
);
final updatedAtParam = DateTime.fromMillisecondsSinceEpoch(
const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0),
);
final object = JobEntity(
jobId: jobIdParam,
jobData: jobDataParam,
createdAt: createdAtParam,
updatedAt: updatedAtParam,
)..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
return object;
},
),
PhotoEntity: obx_int.EntityDefinition<PhotoEntity>(
model: _entities[2],
toOneRelations: (PhotoEntity object) => [],
toManyRelations: (PhotoEntity object) => {},
getId: (PhotoEntity object) => object.id,
setId: (PhotoEntity object, int id) {
object.id = id;
},
objectToFB: (PhotoEntity object, fb.Builder fbb) {
final taskIdOffset = fbb.writeString(object.taskId);
final dataOffset = fbb.writeString(object.data);
fbb.startTable(6);
fbb.addInt64(0, object.id);
fbb.addOffset(1, taskIdOffset);
fbb.addInt64(2, object.photoIndex);
fbb.addOffset(3, dataOffset);
fbb.addInt64(4, object.createdAt.millisecondsSinceEpoch);
fbb.finish(fbb.endTable());
return object.id;
},
objectFromFB: (obx.Store store, ByteData fbData) {
final buffer = fb.BufferContext(fbData);
final rootOffset = buffer.derefObject(0);
final taskIdParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 6, '');
final photoIndexParam = const fb.Int64Reader().vTableGet(
buffer,
rootOffset,
8,
0,
);
final dataParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 10, '');
final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0),
);
final object = PhotoEntity(
taskId: taskIdParam,
photoIndex: photoIndexParam,
data: dataParam,
createdAt: createdAtParam,
)..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
return object;
},
),
QueuedMessageEntity: obx_int.EntityDefinition<QueuedMessageEntity>(
model: _entities[3],
toOneRelations: (QueuedMessageEntity object) => [],
toManyRelations: (QueuedMessageEntity object) => {},
getId: (QueuedMessageEntity object) => object.id,
setId: (QueuedMessageEntity object, int id) {
object.id = id;
},
objectToFB: (QueuedMessageEntity object, fb.Builder fbb) {
final messageIdOffset = fbb.writeString(object.messageId);
final topicOffset = fbb.writeString(object.topic);
final payloadOffset = fbb.writeString(object.payload);
fbb.startTable(7);
fbb.addInt64(0, object.id);
fbb.addOffset(1, messageIdOffset);
fbb.addOffset(2, topicOffset);
fbb.addOffset(3, payloadOffset);
fbb.addInt64(4, object.createdAt.millisecondsSinceEpoch);
fbb.addInt64(5, object.retryCount);
fbb.finish(fbb.endTable());
return object.id;
},
objectFromFB: (obx.Store store, ByteData fbData) {
final buffer = fb.BufferContext(fbData);
final rootOffset = buffer.derefObject(0);
final messageIdParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 6, '');
final topicParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 8, '');
final payloadParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 10, '');
final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0),
);
final retryCountParam = const fb.Int64Reader().vTableGet(
buffer,
rootOffset,
14,
0,
);
final object = QueuedMessageEntity(
messageId: messageIdParam,
topic: topicParam,
payload: payloadParam,
createdAt: createdAtParam,
retryCount: retryCountParam,
)..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
return object;
},
),
TaskStatusEntity: obx_int.EntityDefinition<TaskStatusEntity>(
model: _entities[4],
toOneRelations: (TaskStatusEntity object) => [],
toManyRelations: (TaskStatusEntity object) => {},
getId: (TaskStatusEntity object) => object.id,
setId: (TaskStatusEntity object, int id) {
object.id = id;
},
objectToFB: (TaskStatusEntity object, fb.Builder fbb) {
final taskIdOffset = fbb.writeString(object.taskId);
fbb.startTable(7);
fbb.addInt64(0, object.id);
fbb.addOffset(1, taskIdOffset);
fbb.addBool(2, object.completed);
fbb.addInt64(3, object.completedAt?.millisecondsSinceEpoch);
fbb.addInt64(4, object.createdAt.millisecondsSinceEpoch);
fbb.addInt64(5, object.updatedAt.millisecondsSinceEpoch);
fbb.finish(fbb.endTable());
return object.id;
},
objectFromFB: (obx.Store store, ByteData fbData) {
final buffer = fb.BufferContext(fbData);
final rootOffset = buffer.derefObject(0);
final completedAtValue = const fb.Int64Reader().vTableGetNullable(
buffer,
rootOffset,
10,
);
final taskIdParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 6, '');
final completedParam = const fb.BoolReader().vTableGet(
buffer,
rootOffset,
8,
false,
);
final completedAtParam = completedAtValue == null
? null
: DateTime.fromMillisecondsSinceEpoch(completedAtValue);
final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0),
);
final updatedAtParam = DateTime.fromMillisecondsSinceEpoch(
const fb.Int64Reader().vTableGet(buffer, rootOffset, 14, 0),
);
final object = TaskStatusEntity(
taskId: taskIdParam,
completed: completedParam,
completedAt: completedAtParam,
createdAt: createdAtParam,
updatedAt: updatedAtParam,
)..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
return object;
},
),
UserDataEntity: obx_int.EntityDefinition<UserDataEntity>(
model: _entities[5],
toOneRelations: (UserDataEntity object) => [],
toManyRelations: (UserDataEntity object) => {},
getId: (UserDataEntity object) => object.id,
setId: (UserDataEntity object, int id) {
object.id = id;
},
objectToFB: (UserDataEntity object, fb.Builder fbb) {
final keyOffset = fbb.writeString(object.key);
final valueOffset = fbb.writeString(object.value);
fbb.startTable(6);
fbb.addInt64(0, object.id);
fbb.addOffset(1, keyOffset);
fbb.addOffset(2, valueOffset);
fbb.addInt64(3, object.createdAt.millisecondsSinceEpoch);
fbb.addInt64(4, object.updatedAt.millisecondsSinceEpoch);
fbb.finish(fbb.endTable());
return object.id;
},
objectFromFB: (obx.Store store, ByteData fbData) {
final buffer = fb.BufferContext(fbData);
final rootOffset = buffer.derefObject(0);
final keyParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 6, '');
final valueParam = const fb.StringReader(
asciiOptimization: true,
).vTableGet(buffer, rootOffset, 8, '');
final createdAtParam = DateTime.fromMillisecondsSinceEpoch(
const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0),
);
final updatedAtParam = DateTime.fromMillisecondsSinceEpoch(
const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0),
);
final object = UserDataEntity(
key: keyParam,
value: valueParam,
createdAt: createdAtParam,
updatedAt: updatedAtParam,
)..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
return object;
},
),
};
return obx_int.ModelDefinition(model, bindings);
}
/// [ChatMessageEntity] entity fields to define ObjectBox queries.
class ChatMessageEntity_ {
/// See [ChatMessageEntity.id].
static final id = obx.QueryIntegerProperty<ChatMessageEntity>(
_entities[0].properties[0],
);
/// See [ChatMessageEntity.messageId].
static final messageId = obx.QueryStringProperty<ChatMessageEntity>(
_entities[0].properties[1],
);
/// See [ChatMessageEntity.conversationKey].
static final conversationKey = obx.QueryStringProperty<ChatMessageEntity>(
_entities[0].properties[2],
);
/// See [ChatMessageEntity.content].
static final content = obx.QueryStringProperty<ChatMessageEntity>(
_entities[0].properties[3],
);
/// See [ChatMessageEntity.contentType].
static final contentType = obx.QueryStringProperty<ChatMessageEntity>(
_entities[0].properties[4],
);
/// See [ChatMessageEntity.createdAt].
static final createdAt = obx.QueryDateProperty<ChatMessageEntity>(
_entities[0].properties[5],
);
/// See [ChatMessageEntity.origin].
static final origin = obx.QueryStringProperty<ChatMessageEntity>(
_entities[0].properties[6],
);
/// See [ChatMessageEntity.messageType].
static final messageType = obx.QueryStringProperty<ChatMessageEntity>(
_entities[0].properties[7],
);
/// See [ChatMessageEntity.jobId].
static final jobId = obx.QueryStringProperty<ChatMessageEntity>(
_entities[0].properties[8],
);
/// See [ChatMessageEntity.jobNumber].
static final jobNumber = obx.QueryStringProperty<ChatMessageEntity>(
_entities[0].properties[9],
);
/// See [ChatMessageEntity.read].
static final read = obx.QueryBooleanProperty<ChatMessageEntity>(
_entities[0].properties[10],
);
/// See [ChatMessageEntity.pendingSync].
static final pendingSync = obx.QueryBooleanProperty<ChatMessageEntity>(
_entities[0].properties[11],
);
}
/// [JobEntity] entity fields to define ObjectBox queries.
class JobEntity_ {
/// See [JobEntity.id].
static final id = obx.QueryIntegerProperty<JobEntity>(
_entities[1].properties[0],
);
/// See [JobEntity.jobId].
static final jobId = obx.QueryStringProperty<JobEntity>(
_entities[1].properties[1],
);
/// See [JobEntity.jobData].
static final jobData = obx.QueryStringProperty<JobEntity>(
_entities[1].properties[2],
);
/// See [JobEntity.createdAt].
static final createdAt = obx.QueryDateProperty<JobEntity>(
_entities[1].properties[3],
);
/// See [JobEntity.updatedAt].
static final updatedAt = obx.QueryDateProperty<JobEntity>(
_entities[1].properties[4],
);
}
/// [PhotoEntity] entity fields to define ObjectBox queries.
class PhotoEntity_ {
/// See [PhotoEntity.id].
static final id = obx.QueryIntegerProperty<PhotoEntity>(
_entities[2].properties[0],
);
/// See [PhotoEntity.taskId].
static final taskId = obx.QueryStringProperty<PhotoEntity>(
_entities[2].properties[1],
);
/// See [PhotoEntity.photoIndex].
static final photoIndex = obx.QueryIntegerProperty<PhotoEntity>(
_entities[2].properties[2],
);
/// See [PhotoEntity.data].
static final data = obx.QueryStringProperty<PhotoEntity>(
_entities[2].properties[3],
);
/// See [PhotoEntity.createdAt].
static final createdAt = obx.QueryDateProperty<PhotoEntity>(
_entities[2].properties[4],
);
}
/// [QueuedMessageEntity] entity fields to define ObjectBox queries.
class QueuedMessageEntity_ {
/// See [QueuedMessageEntity.id].
static final id = obx.QueryIntegerProperty<QueuedMessageEntity>(
_entities[3].properties[0],
);
/// See [QueuedMessageEntity.messageId].
static final messageId = obx.QueryStringProperty<QueuedMessageEntity>(
_entities[3].properties[1],
);
/// See [QueuedMessageEntity.topic].
static final topic = obx.QueryStringProperty<QueuedMessageEntity>(
_entities[3].properties[2],
);
/// See [QueuedMessageEntity.payload].
static final payload = obx.QueryStringProperty<QueuedMessageEntity>(
_entities[3].properties[3],
);
/// See [QueuedMessageEntity.createdAt].
static final createdAt = obx.QueryDateProperty<QueuedMessageEntity>(
_entities[3].properties[4],
);
/// See [QueuedMessageEntity.retryCount].
static final retryCount = obx.QueryIntegerProperty<QueuedMessageEntity>(
_entities[3].properties[5],
);
}
/// [TaskStatusEntity] entity fields to define ObjectBox queries.
class TaskStatusEntity_ {
/// See [TaskStatusEntity.id].
static final id = obx.QueryIntegerProperty<TaskStatusEntity>(
_entities[4].properties[0],
);
/// See [TaskStatusEntity.taskId].
static final taskId = obx.QueryStringProperty<TaskStatusEntity>(
_entities[4].properties[1],
);
/// See [TaskStatusEntity.completed].
static final completed = obx.QueryBooleanProperty<TaskStatusEntity>(
_entities[4].properties[2],
);
/// See [TaskStatusEntity.completedAt].
static final completedAt = obx.QueryDateProperty<TaskStatusEntity>(
_entities[4].properties[3],
);
/// See [TaskStatusEntity.createdAt].
static final createdAt = obx.QueryDateProperty<TaskStatusEntity>(
_entities[4].properties[4],
);
/// See [TaskStatusEntity.updatedAt].
static final updatedAt = obx.QueryDateProperty<TaskStatusEntity>(
_entities[4].properties[5],
);
}
/// [UserDataEntity] entity fields to define ObjectBox queries.
class UserDataEntity_ {
/// See [UserDataEntity.id].
static final id = obx.QueryIntegerProperty<UserDataEntity>(
_entities[5].properties[0],
);
/// See [UserDataEntity.key].
static final key = obx.QueryStringProperty<UserDataEntity>(
_entities[5].properties[1],
);
/// See [UserDataEntity.value].
static final value = obx.QueryStringProperty<UserDataEntity>(
_entities[5].properties[2],
);
/// See [UserDataEntity.createdAt].
static final createdAt = obx.QueryDateProperty<UserDataEntity>(
_entities[5].properties[3],
);
/// See [UserDataEntity.updatedAt].
static final updatedAt = obx.QueryDateProperty<UserDataEntity>(
_entities[5].properties[4],
);
}

141
app/lib/routing_view.dart Normal file
View File

@@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'l10n/app_localizations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'widgets/offline_banner.dart';
// Routing view that immediately opens a Google Maps navigation inside a WebView
// It receives an address string and optionally a title.
class RoutingView extends StatefulWidget {
final String address;
final String? title;
final bool isDelivery; // to distinguish pickup/delivery if needed later
const RoutingView({super.key, required this.address, this.title, this.isDelivery = true});
@override
State<RoutingView> createState() => _RoutingViewState();
}
class _RoutingViewState extends State<RoutingView> {
bool _initialized = false;
late final WebViewController _controller;
double _progress = 0.0;
String _buildDirectionsUrl(String rawAddress) {
const apiKey = 'AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE';
final query = Uri.encodeComponent(rawAddress);
// Google Maps Directions URL with API key appended.
return 'https://www.google.com/maps/dir/?api=1&destination=$query&travelmode=driving&key=$apiKey';
}
String? _extractBrowserFallbackUrl(String intentUrl) {
const key = 'S.browser_fallback_url=';
final idx = intentUrl.indexOf(key);
if (idx == -1) return null;
final start = idx + key.length;
final end = intentUrl.indexOf(';', start);
final encoded = end == -1 ? intentUrl.substring(start) : intentUrl.substring(start, end);
try {
return Uri.decodeComponent(encoded);
} catch (_) {
return encoded;
}
}
String? _convertIntentToHttps(String intentUrl) {
const prefix = 'intent://';
if (!intentUrl.startsWith(prefix)) return null;
final after = intentUrl.substring(prefix.length);
final hashIndex = after.indexOf('#');
final pathPart = hashIndex == -1 ? after : after.substring(0, hashIndex);
// default scheme is https unless specified
var scheme = 'https';
final schemeKey = '#Intent;scheme=';
final schemeIdx = intentUrl.indexOf(schemeKey);
if (schemeIdx != -1) {
final start = schemeIdx + schemeKey.length;
final end = intentUrl.indexOf(';', start);
if (end > start) {
scheme = intentUrl.substring(start, end);
}
}
return '$scheme://$pathPart';
}
@override
void initState() {
assert(!_initialized);
super.initState();
final url = _buildDirectionsUrl(widget.address);
_controller =
WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) async {
final uri = Uri.tryParse(request.url);
if (uri == null) {
return NavigationDecision.prevent;
}
// Handle intent:// and other non-http(s) schemes by launching externally or using fallback.
if (uri.scheme == 'intent') {
final fallback = _extractBrowserFallbackUrl(request.url);
if (fallback != null) {
await _controller.loadRequest(Uri.parse(fallback));
} else {
// Try converting to https as a naive fallback
final httpsCandidate = _convertIntentToHttps(request.url);
if (httpsCandidate != null && await canLaunchUrl(Uri.parse(httpsCandidate))) {
await launchUrl(Uri.parse(httpsCandidate), mode: LaunchMode.externalApplication);
} else {
// As a last resort, prevent navigation and show a hint
if (mounted) {
ScaffoldMessenger.maybeOf(context)?.showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).connectionError)));
}
}
}
return NavigationDecision.prevent;
}
if (uri.scheme != 'http' && uri.scheme != 'https' && uri.scheme != 'about' && uri.scheme != 'data') {
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
onProgress: (int progress) {
setState(() {
_progress = progress / 100.0;
});
},
onWebResourceError: (WebResourceError error) {
// Optionally show a snackbar on error
if (mounted) {
ScaffoldMessenger.maybeOf(context)?.showSnackBar(SnackBar(content: Text('${AppLocalizations.of(context).error}: ${error.errorCode}')));
}
},
),
)
..loadRequest(Uri.parse(url));
_initialized = true;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title?.isNotEmpty == true ? widget.title! : (widget.isDelivery ? 'Route zur Zustelladresse' : 'Route zur Abholadresse'))),
body: Column(
children: [
OfflineBanner(),
Expanded(child: Stack(children: [WebViewWidget(key: const ValueKey('routing-webview'), controller: _controller), if (_progress < 1.0) LinearProgressIndicator(value: _progress)])),
],
),
);
}
}

View File

@@ -0,0 +1,143 @@
import 'package:flutter/foundation.dart';
/// Represents a message awaiting acknowledgment
class PendingMessage {
/// Unique message identifier
final String messageId;
/// Target topic
final String topic;
/// The JSON payload to retry sending
final String jsonPayload;
/// When the message was originally sent
final DateTime sentAt;
/// Number of retry attempts so far
int retryCount;
PendingMessage({
required this.messageId,
required this.topic,
required this.jsonPayload,
required this.sentAt,
this.retryCount = 0,
});
}
/// Tracks pending messages awaiting acknowledgment and handles retries.
///
/// This class is extracted from WebSocketService for testability.
/// It manages:
/// - Tracking sent messages that require ACK
/// - Removing messages when ACK is received
/// - Retrying unacknowledged messages
/// - Timing out messages after max retries
class AckTracker {
final Map<String, PendingMessage> _pendingMessages = {};
/// Maximum number of retry attempts before timeout
final int maxRetries;
/// Callback to retry sending a message.
/// Returns true if send was successful.
final Future<bool> Function(String topic, String payload)? onRetry;
/// Callback when a message times out (max retries exceeded)
final void Function(String messageId, String topic)? onTimeout;
AckTracker({
this.maxRetries = 4,
this.onRetry,
this.onTimeout,
});
/// Track a sent message that requires acknowledgment
void track(String messageId, String topic, String payload) {
_pendingMessages[messageId] = PendingMessage(
messageId: messageId,
topic: topic,
jsonPayload: payload,
sentAt: DateTime.now(),
);
}
/// Remove a message from tracking (ACK received)
void acknowledge(String messageId) {
_pendingMessages.remove(messageId);
}
/// Check if a message is pending acknowledgment
bool isPending(String messageId) =>
_pendingMessages.containsKey(messageId);
/// Get the number of pending messages
int get pendingCount => _pendingMessages.length;
/// Get all pending message IDs
List<String> get pendingMessageIds =>
List.unmodifiable(_pendingMessages.keys);
/// Get a pending message by ID (for testing)
@visibleForTesting
PendingMessage? getPendingMessage(String messageId) =>
_pendingMessages[messageId];
/// Process all pending messages and retry if needed.
///
/// This should be called periodically (e.g., every 5 seconds).
/// Messages that exceed maxRetries will be timed out and removed.
///
/// Set [isConnected] to false to skip retry attempts while disconnected.
Future<void> processRetries({bool isConnected = true}) async {
if (_pendingMessages.isEmpty) {
return;
}
final messagesToRemove = <String>[];
for (final entry in _pendingMessages.entries) {
final messageId = entry.key;
final pending = entry.value;
if (pending.retryCount >= maxRetries) {
// Max retries exceeded - timeout
onTimeout?.call(messageId, pending.topic);
messagesToRemove.add(messageId);
} else if (isConnected) {
// Increment retry count and attempt resend
pending.retryCount++;
if (onRetry != null) {
await onRetry!(pending.topic, pending.jsonPayload);
}
}
}
// Remove timed out messages
for (final messageId in messagesToRemove) {
_pendingMessages.remove(messageId);
}
}
/// Clear all pending messages.
///
/// Primarily for testing purposes.
@visibleForTesting
void clearAll() => _pendingMessages.clear();
/// Clear pending messages for a specific topic pattern.
///
/// Useful for clearing login messages when auth response is received.
void clearForTopic(String topicPattern) {
final toRemove = _pendingMessages.entries
.where((e) => e.value.topic == topicPattern)
.map((e) => e.key)
.toList();
for (final messageId in toRemove) {
_pendingMessages.remove(messageId);
}
}
}

View File

@@ -0,0 +1,500 @@
import 'dart:async';
import 'package:votianlt_app/services/developer.dart' as developer;
import '../app_state.dart';
import '../models/chat.dart';
import '../models/chat_message.dart';
import 'database_service.dart';
class ChatService {
ChatService._internal();
static final ChatService _instance = ChatService._internal();
factory ChatService() => _instance;
static const _jobIdPrefix = 'job:';
static const _jobNumberPrefix = 'job_number:';
static const _generalPrefix = 'general:';
final DatabaseService _databaseService = DatabaseService();
final AppState _appState = AppState();
final List<Chat> _chats = <Chat>[];
final StreamController<List<Chat>> _chatsController =
StreamController<List<Chat>>.broadcast();
final StreamController<int> _unreadCountController =
StreamController<int>.broadcast();
bool _initialized = false;
Completer<void>? _initCompleter;
int _unreadCount = 0;
Stream<List<Chat>> get chatsStream => _chatsController.stream;
List<Chat> get currentChats => List<Chat>.unmodifiable(_chats);
Stream<int> get unreadCountStream => _unreadCountController.stream;
int get unreadCount => _unreadCount;
Future<void> initialize() async {
if (_initialized) {
await _loadChatsFromDatabase();
developer.log(
'ChatService already initialized, refreshed chats/unread count: $_unreadCount',
name: 'ChatService',
);
return;
}
if (_initCompleter != null) {
return _initCompleter!.future;
}
_initCompleter = Completer<void>();
developer.log('Initializing ChatService...', name: 'ChatService');
await _loadChatsFromDatabase();
_initialized = true;
_initCompleter!.complete();
developer.log(
'ChatService initialized with unread count: $_unreadCount',
name: 'ChatService',
);
}
Future<void> dispose() async {
await _chatsController.close();
await _unreadCountController.close();
_initialized = false;
_initCompleter = null;
}
Future<List<ChatMessage>> loadMessagesForChat(String conversationKey) async {
await initialize();
return _databaseService.loadChatMessages(conversationKey: conversationKey);
}
Future<void> markConversationRead(String conversationKey) async {
await initialize();
await _databaseService.markConversationRead(conversationKey);
await _refreshConversation(conversationKey);
// Note: _refreshConversation already calls _updateUnreadCount,
// so we don't need to call it again here
}
Future<void> deleteJobChats(String jobId, {String? jobNumber}) async {
if (!_initialized) {
await initialize();
}
final trimmedJobId = jobId.trim();
final lowerJobId = trimmedJobId.toLowerCase();
final trimmedJobNumber = jobNumber?.trim() ?? '';
final lowerJobNumber = trimmedJobNumber.toLowerCase();
final conversationKeys = <String>[
if (trimmedJobId.isNotEmpty) '$_jobIdPrefix$lowerJobId',
if (trimmedJobNumber.isNotEmpty) '$_jobNumberPrefix$lowerJobNumber',
];
await _databaseService.deleteChatMessagesForJob(
jobId: trimmedJobId.isNotEmpty ? trimmedJobId : null,
jobNumber: trimmedJobNumber.isNotEmpty ? trimmedJobNumber : null,
conversationKeys: conversationKeys,
);
_chats.removeWhere((chat) {
final matchesKey = conversationKeys.contains(chat.id);
final matchesId = trimmedJobId.isNotEmpty &&
(chat.jobId?.trim().toLowerCase() == lowerJobId);
final matchesNumber = trimmedJobNumber.isNotEmpty &&
(chat.jobNumber?.trim().toLowerCase() == lowerJobNumber);
return matchesKey || matchesId || matchesNumber;
});
_ensureDefaultGeneralChat();
_sortChats();
_emitChats();
await _updateUnreadCount();
developer.log(
'Removed chat conversations for jobId=$jobId jobNumber=$jobNumber',
name: 'ChatService',
);
}
String conversationKeyForMessage(ChatMessage message) {
developer.log(
'[DEBUG_LOG] conversationKeyForMessage called for message ${message.id}, messageType=${message.messageType}, direction=${message.direction}',
name: 'ChatService',
);
// Messages with GENERAL messageType should always go to the default general chat
if (message.messageType == ChatMessageType.general) {
final localId = _primaryLocalIdentifier();
if (localId != null && localId.isNotEmpty) {
final key = _conversationKeyForParticipants(
localId,
_appState.loggedInEmail!,
);
developer.log(
'[DEBUG_LOG] GENERAL message detected, routing to conversation key: $key (localId=$localId, receiver=${_appState.loggedInEmail})',
name: 'ChatService',
);
return key;
}
}
// Job-related messages go to job-specific chats
final jobId = message.jobId?.trim();
if (jobId != null && jobId.isNotEmpty) {
final normalizedJobId = jobId.toLowerCase();
final key = '$_jobIdPrefix$normalizedJobId';
developer.log(
'[DEBUG_LOG] Job-related message (by jobId), routing to conversation key: $key',
name: 'ChatService',
);
return key;
}
final jobNumber = message.jobNumber?.trim();
if (jobNumber != null && jobNumber.isNotEmpty) {
final normalizedJobNumber = jobNumber.toLowerCase();
final key = '$_jobNumberPrefix$normalizedJobNumber';
developer.log(
'[DEBUG_LOG] Job-related message (by jobNumber), routing to conversation key: $key',
name: 'ChatService',
);
return key;
}
// Fallback: create conversation based on userId
final localId = _primaryLocalIdentifier();
if (localId != null && localId.isNotEmpty) {
final key = _conversationKeyForParticipants(
localId,
_appState.loggedInEmail!,
);
developer.log(
'[DEBUG_LOG] Using fallback routing, conversation key: $key',
name: 'ChatService',
);
return key;
}
developer.log(
'[DEBUG_LOG] No local identifier available for fallback routing',
name: 'ChatService',
);
return '$_generalPrefix${_appState.loggedInEmail!}';
}
String _conversationKeyForParticipants(String a, String b) {
final participants = <String>[a.toLowerCase(), b.toLowerCase()]..sort();
return '$_generalPrefix${participants.join('|')}';
}
Future<void> saveIncomingMessage(ChatMessage message) async {
if (!_initialized) {
await initialize();
}
await _persistMessage(message.copyWith(pendingSync: false));
}
Future<void> saveOutgoingMessage(ChatMessage message) async {
if (!_initialized) {
await initialize();
}
await _persistMessage(message);
}
Future<void> _persistMessage(ChatMessage message) async {
final conversationKey = conversationKeyForMessage(message);
final jobId = message.jobId?.trim();
if (jobId != null && jobId.isNotEmpty) {
final legacyKey = '$_jobIdPrefix$jobId';
if (legacyKey != conversationKey) {
await _databaseService.migrateConversationKey(
legacyKey,
conversationKey,
);
}
} else {
final jobNumber = message.jobNumber?.trim();
if (jobNumber != null && jobNumber.isNotEmpty) {
final legacyKey = '$_jobNumberPrefix$jobNumber';
if (legacyKey != conversationKey) {
await _databaseService.migrateConversationKey(
legacyKey,
conversationKey,
);
}
}
}
await _databaseService.upsertChatMessage(message, conversationKey);
if (!message.pendingSync) {
await _databaseService.removePendingDuplicates(conversationKey, message);
}
await _refreshConversation(conversationKey);
}
Future<void> _loadChatsFromDatabase() async {
await _databaseService.ensureInitialized();
final grouped = await _databaseService.loadAllChatMessagesGrouped();
_chats.clear();
grouped.forEach((conversationKey, messages) {
final chat = _buildChat(conversationKey, messages);
if (chat != null) {
_chats.add(chat);
}
});
_ensureDefaultGeneralChat();
_sortChats();
_emitChats();
await _updateUnreadCount();
}
Future<void> _refreshConversation(String conversationKey) async {
final messages = await _databaseService.loadChatMessages(
conversationKey: conversationKey,
);
final index = _chats.indexWhere((chat) => chat.id == conversationKey);
if (messages.isEmpty) {
if (index != -1) {
_chats.removeAt(index);
}
_ensureDefaultGeneralChat();
_sortChats();
_emitChats();
await _updateUnreadCount();
return;
}
final chat = _buildChat(conversationKey, messages);
if (chat == null) {
return;
}
if (index == -1) {
_chats.add(chat);
} else {
_chats[index] = chat;
}
_ensureDefaultGeneralChat();
_sortChats();
_emitChats();
await _updateUnreadCount();
}
Chat? _buildChat(String conversationKey, List<ChatMessage> messages) {
if (messages.isEmpty) {
return null;
}
messages.sort((a, b) => a.createdAt.compareTo(b.createdAt));
final lastMessage = messages.last;
final jobId = messages
.map((m) => m.jobId)
.firstWhere(
(value) => value != null && value.isNotEmpty,
orElse: () => null,
);
final jobNumber = messages
.map((m) => m.jobNumber)
.firstWhere(
(value) => value != null && value.isNotEmpty,
orElse: () => null,
);
final counterpart = _determineCounterpart(conversationKey, messages);
final isJobChat =
conversationKey.startsWith(_jobIdPrefix) ||
conversationKey.startsWith(_jobNumberPrefix) ||
messages.any((m) => m.messageType == ChatMessageType.jobRelated);
final chatType = isJobChat ? ChatType.jobSpecific : ChatType.general;
final counterpartNormalized =
counterpart != null &&
counterpart.toLowerCase() == _appState.loggedInEmail!.toLowerCase()
? _appState.loggedInEmail!
: counterpart;
final bool isDefaultGeneral =
!isJobChat &&
conversationKey.startsWith(_generalPrefix) &&
(counterpartNormalized?.toLowerCase() ==
_appState.loggedInEmail!.toLowerCase());
final title =
isJobChat
? _buildJobTitle(jobNumber, jobId)
: (isDefaultGeneral
? 'Allgemeine Nachrichten'
: (counterpart ?? 'Allgemeiner Chat'));
return Chat(
id: conversationKey,
title: title,
receiver: counterpartNormalized,
type: chatType,
jobId: jobId,
jobNumber: jobNumber,
messages: List<ChatMessage>.unmodifiable(messages),
lastMessageTime: lastMessage.createdAt,
lastMessagePreview:
lastMessage.contentType == ChatContentType.image
? '[Bild]'
: lastMessage.content,
);
}
String _buildJobTitle(String? jobNumber, String? jobId) {
if (jobNumber != null && jobNumber.isNotEmpty) {
return 'Job $jobNumber';
}
if (jobId != null && jobId.length >= 6) {
return 'Job ${jobId.substring(0, 6).toUpperCase()}';
}
return 'Job-Chat';
}
String? _determineCounterpart(
String conversationKey,
List<ChatMessage> messages,
) {
// Receiver is always the userId for general chats
return _appState.loggedInEmail;
}
void _sortChats() {
_chats.sort((a, b) => b.lastMessageTime.compareTo(a.lastMessageTime));
}
void _emitChats() {
if (_chatsController.isClosed) {
return;
}
_chatsController.add(List<Chat>.unmodifiable(_chats));
}
Future<void> _updateUnreadCount() async {
try {
await _databaseService.ensureInitialized();
final count = await _databaseService.getTotalUnreadMessageCount();
developer.log(
'[DEBUG_LOG] Unread count from database: $count',
name: 'ChatService',
);
_unreadCount = count;
if (!_unreadCountController.isClosed) {
_unreadCountController.add(count);
developer.log(
'[DEBUG_LOG] Emitted unread count to stream: $count',
name: 'ChatService',
);
} else {
developer.log(
'[DEBUG_LOG] Unread count controller is closed, cannot emit',
name: 'ChatService',
);
}
} catch (e, st) {
developer.log('Error updating unread count: $e', name: 'ChatService');
developer.log('Stack trace: $st', name: 'ChatService');
}
}
void _ensureDefaultGeneralChat() {
final localId = _primaryLocalIdentifier();
if (localId == null || localId.isEmpty) {
developer.log(
'[DEBUG_LOG] _ensureDefaultGeneralChat: No local identifier available, skipping',
name: 'ChatService',
);
return;
}
final conversationKey = _conversationKeyForParticipants(
localId,
_appState.loggedInEmail!,
);
developer.log(
'[DEBUG_LOG] _ensureDefaultGeneralChat: Creating/ensuring default general chat with key: $conversationKey (localId=$localId, receiver=${_appState.loggedInEmail})',
name: 'ChatService',
);
_chats.removeWhere(
(chat) =>
chat.id != conversationKey &&
chat.type == ChatType.general &&
chat.receiver != null &&
chat.receiver!.toLowerCase() ==
_appState.loggedInEmail!.toLowerCase() &&
chat.messages.isEmpty,
);
final index = _chats.indexWhere((chat) => chat.id == conversationKey);
if (index == -1) {
developer.log(
'[DEBUG_LOG] _ensureDefaultGeneralChat: Chat not found, creating new "Allgemeine Nachrichten" chat',
name: 'ChatService',
);
_chats.add(
Chat(
id: conversationKey,
title: 'Allgemeine Nachrichten',
receiver: _appState.loggedInEmail!,
type: ChatType.general,
jobId: null,
jobNumber: null,
messages: const [],
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(0),
lastMessagePreview: 'Noch keine Nachrichten',
),
);
} else {
developer.log(
'[DEBUG_LOG] _ensureDefaultGeneralChat: Chat already exists at index $index, verifying/updating it',
name: 'ChatService',
);
final existing = _chats[index];
if (existing.type != ChatType.general ||
existing.receiver == null ||
existing.receiver!.toLowerCase() !=
_appState.loggedInEmail!.toLowerCase() ||
(existing.messages.isEmpty &&
existing.title != 'Allgemeine Nachrichten')) {
developer.log(
'[DEBUG_LOG] _ensureDefaultGeneralChat: Updating existing chat to ensure correct settings',
name: 'ChatService',
);
_chats[index] = Chat(
id: existing.id,
title:
existing.messages.isEmpty
? 'Allgemeine Nachrichten'
: existing.title,
receiver: _appState.loggedInEmail!,
type: ChatType.general,
jobId: existing.jobId,
jobNumber: existing.jobNumber,
messages: existing.messages,
lastMessageTime: existing.lastMessageTime,
lastMessagePreview: existing.lastMessagePreview,
);
} else {
developer.log(
'[DEBUG_LOG] _ensureDefaultGeneralChat: Existing chat is already correctly configured (${existing.messages.length} messages)',
name: 'ChatService',
);
}
}
}
String? _primaryLocalIdentifier() {
return _appState.loggedInEmail;
}
}

View File

@@ -0,0 +1,100 @@
import 'package:votianlt_app/services/developer.dart' as developer;
/// A lightweight in-app message bus ("dart_mq") for pub/sub style communication.
///
// Usage:
// final mq = DartMQ();
// final sub = mq.subscribe<bool>('connection/status', (isOnline) { /* ... */ });
// mq.publish('connection/status', true);
// sub.cancel();
class DartMQ {
DartMQ._internal();
static final DartMQ _instance = DartMQ._internal();
factory DartMQ() => _instance;
final Map<String, List<_DartMQSubscriber>> _subscribers = {};
/// Subscribe to a topic. Returns a cancellable subscription handle.
DartMQSubscription subscribe<T>(String topic, void Function(T data) handler) {
final sub = _DartMQSubscriber<T>(topic: topic, handler: handler);
final list = _subscribers.putIfAbsent(topic, () => <_DartMQSubscriber>[]);
list.add(sub);
return DartMQSubscription._(this, sub);
}
/// Publish a message to a topic. If no subscribers exist, this is a no-op.
void publish<T>(String topic, T data) {
final list = _subscribers[topic];
if (list == null || list.isEmpty) return;
// Make a defensive copy to allow cancellation during iteration.
final current = List<_DartMQSubscriber>.from(list);
for (final s in current) {
// Only deliver if types match; otherwise, try dynamic fallback
if (s is _DartMQSubscriber<T>) {
try {
s.handler(data);
} catch (e, stackTrace) {
developer.log(
'Error delivering message to subscriber on topic "$topic": $e',
);
developer.log('Stack trace: $stackTrace');
}
} else {
// Fallback delivery for handlers expecting dynamic or different T
try {
final dynamicHandler = s.handler as dynamic;
dynamicHandler(data);
} catch (e, stackTrace) {
developer.log(
'Error delivering dynamic message to subscriber on topic "$topic": $e',
);
developer.log('Stack trace: $stackTrace');
}
}
}
}
void _cancel(_DartMQSubscriber subscriber) {
final list = _subscribers[subscriber.topic];
if (list == null) return;
list.remove(subscriber);
if (list.isEmpty) {
_subscribers.remove(subscriber.topic);
}
}
}
/// Cancellable subscription handle
class DartMQSubscription {
final DartMQ _mq;
final _DartMQSubscriber _subscriber;
bool _isCancelled = false;
DartMQSubscription._(this._mq, this._subscriber);
void cancel() {
if (_isCancelled) return;
_isCancelled = true;
_mq._cancel(_subscriber);
}
}
class _DartMQSubscriber<T> {
final String topic;
final void Function(T data) handler;
_DartMQSubscriber({required this.topic, required this.handler});
}
/// Common topics used in the app
class MQTopics {
static const connectionStatus = 'connection/status'; // bool
static const authResponse = 'auth/response'; // Map<String, dynamic>
static const jobsResponse = 'jobs/response'; // List<dynamic>
static const taskEvents = 'task/events'; // Map<String, dynamic>
static const jobsUpdated = 'app/jobsUpdated'; // void/null
static const jobDeleted = 'job/deleted'; // Map<String, dynamic> {jobId, jobNumber, deletedAt}
static const jobCreated = 'job/created'; // Map<String, dynamic> - full job data
static const chatIncoming = 'chat/incoming'; // ChatMessage
static const chatOutgoing = 'chat/outgoing'; // ChatMessage
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
// Wrapper around dart:developer.log that also outputs logs in release mode.
//
// Usage: import this file as `developer` instead of `dart:developer`.
// Then call `developer.log(...)` as usual. In debug/profile, it forwards to
// dart:developer.log; in release it prints to stdout so logs are visible.
export 'dart:developer' hide log;
import 'dart:async' show Zone;
import 'dart:developer' as dev;
import 'package:flutter/foundation.dart';
void log(
String message, {
DateTime? time,
int? sequenceNumber,
int level = 0,
String name = '',
Zone? zone,
Object? error,
StackTrace? stackTrace,
}) {
if (kReleaseMode) {
final ts = (time ?? DateTime.now()).toIso8601String();
final tag = name.isNotEmpty ? '[$name] ' : '';
final seq = sequenceNumber != null ? ' #$sequenceNumber' : '';
final lvl = level != 0 ? ' L$level' : '';
final err = error != null ? ' | error: $error' : '';
final st = stackTrace != null ? ' | stack: $stackTrace' : '';
// Keep it a single line to avoid mixing with platform loggers.
// Using print to ensure output in release builds.
// Example: 2025-09-13T12:47:00.123Z [StompService] Connected ... L800 #42
// Note: Some platforms may trim long lines; we still prefer a single print.
// ignore: avoid_print
print('$ts $tag$message$seq$lvl$err$st');
} else {
dev.log(
message,
time: time,
sequenceNumber: sequenceNumber,
level: level,
name: name,
zone: zone,
error: error,
stackTrace: stackTrace,
);
}
}

View File

@@ -0,0 +1,184 @@
import 'dart:async';
import 'dart:convert';
import 'package:geolocator/geolocator.dart';
import 'package:votianlt_app/services/developer.dart' as developer;
import 'websocket_service.dart';
/// Service for tracking and sending GPS location.
/// Sends position every 30 seconds when online.
/// Does not buffer location data when offline.
class LocationService {
static final LocationService _instance = LocationService._internal();
factory LocationService() => _instance;
LocationService._internal();
Timer? _locationTimer;
bool _isTracking = false;
Position? _lastPosition;
static const String _topic = '/server/location';
static const int _sendIntervalSeconds = 30;
/// Check if location services are enabled and permission is granted
Future<bool> _checkPermissions() async {
// Check if location services are enabled
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
developer.log(
'Location services are disabled',
name: 'LocationService',
);
return false;
}
// Check location permission
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
developer.log(
'Location permission denied',
name: 'LocationService',
);
return false;
}
}
if (permission == LocationPermission.deniedForever) {
developer.log(
'Location permission permanently denied',
name: 'LocationService',
);
return false;
}
return true;
}
/// Start location tracking and periodic sending
Future<void> startTracking() async {
if (_isTracking) {
developer.log(
'Location tracking already active',
name: 'LocationService',
);
return;
}
final hasPermission = await _checkPermissions();
if (!hasPermission) {
developer.log(
'Cannot start location tracking - permission not granted',
name: 'LocationService',
);
return;
}
_isTracking = true;
developer.log(
'Starting location tracking (sending every $_sendIntervalSeconds seconds)',
name: 'LocationService',
);
// Get initial position
await _updateAndSendPosition();
// Start periodic timer
_locationTimer = Timer.periodic(
const Duration(seconds: _sendIntervalSeconds),
(_) => _updateAndSendPosition(),
);
}
/// Stop location tracking
void stopTracking() {
if (!_isTracking) return;
developer.log(
'Stopping location tracking',
name: 'LocationService',
);
_locationTimer?.cancel();
_locationTimer = null;
_isTracking = false;
}
/// Get current position and send to server if online
Future<void> _updateAndSendPosition() async {
try {
final position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.best,
),
);
_lastPosition = position;
developer.log(
'Position updated: ${position.latitude}, ${position.longitude}',
name: 'LocationService',
);
await _sendPosition(position);
} catch (e, st) {
developer.log(
'Error getting position: $e',
name: 'LocationService',
);
developer.log('Stack: $st', name: 'LocationService');
}
}
/// Send position to server if online
/// Does NOT buffer when offline - location data is time-sensitive
Future<void> _sendPosition(Position position) async {
final wsService = WebSocketService();
// Only send if connected and authenticated
if (!wsService.isConnected || !wsService.isAuthenticated) {
developer.log(
'Not sending position - not connected/authenticated',
name: 'LocationService',
);
return;
}
final payload = {
'latitude': position.latitude,
'longitude': position.longitude,
'accuracy': position.accuracy,
'altitude': position.altitude,
'speed': position.speed,
'heading': position.heading,
'timestamp': position.timestamp.toIso8601String(),
};
try {
const topic = _topic;
final jsonPayload = jsonEncode(payload);
// Use direct WebSocket send to avoid buffering
wsService.sendMessage(topic, jsonPayload);
developer.log(
'Position sent to server: ${position.latitude}, ${position.longitude}',
name: 'LocationService',
);
} catch (e, st) {
developer.log(
'Error sending position: $e',
name: 'LocationService',
);
developer.log('Stack: $st', name: 'LocationService');
}
}
/// Get the last known position
Position? get lastPosition => _lastPosition;
/// Check if tracking is active
bool get isTracking => _isTracking;
}

View File

@@ -0,0 +1,114 @@
import 'package:flutter/foundation.dart';
import '../models/message_envelope.dart';
/// Result of unwrapping a message envelope
class UnwrapResult {
/// The unwrapped payload
final dynamic payload;
/// The message ID (null if not an envelope)
final String? messageId;
/// Whether this message requires acknowledgment
final bool requiresAck;
UnwrapResult({
required this.payload,
this.messageId,
this.requiresAck = false,
});
}
/// Handles message envelope unwrapping and deduplication.
///
/// This class is extracted from WebSocketService for testability.
/// It manages:
/// - Detecting and unwrapping MessageEnvelope structures
/// - Deduplicating messages by messageId
/// - Triggering ACK callbacks when required
class MessageHandler {
final Set<String> _processedMessageIds = {};
/// Maximum number of message IDs to track for deduplication
final int maxProcessedIds;
/// Callback invoked when an ACK should be sent
final void Function(String messageId)? onAckRequired;
MessageHandler({
this.maxProcessedIds = 100,
this.onAckRequired,
});
/// Check if data is a valid MessageEnvelope structure.
///
/// A valid envelope must contain:
/// - messageId
/// - timestamp
/// - topic
/// - payload
bool isEnvelopeMessage(dynamic data) {
if (data is! Map<String, dynamic>) return false;
return data.containsKey('messageId') &&
data.containsKey('timestamp') &&
data.containsKey('topic') &&
data.containsKey('payload');
}
/// Unwrap a message envelope and handle deduplication.
///
/// Returns null if the message was already processed (duplicate).
/// For duplicates, still triggers onAckRequired if the original required ACK.
///
/// Returns [UnwrapResult] with payload and ACK info for new messages.
/// If data is not an envelope, returns it as-is with requiresAck=false.
UnwrapResult? unwrapEnvelope(dynamic data) {
if (!isEnvelopeMessage(data)) {
// Not an envelope, return data as-is (no ACK needed)
return UnwrapResult(
payload: data,
messageId: null,
requiresAck: false,
);
}
final envelope = MessageEnvelope.fromJson(data as Map<String, dynamic>);
// Check for duplicate
if (_processedMessageIds.contains(envelope.messageId)) {
// Still send ACK for duplicate messages
if (envelope.requiresAck && onAckRequired != null) {
onAckRequired!(envelope.messageId);
}
return null;
}
// Track this message as processed
_processedMessageIds.add(envelope.messageId);
// Limit set size to prevent memory growth (FIFO eviction)
if (_processedMessageIds.length > maxProcessedIds) {
_processedMessageIds.remove(_processedMessageIds.first);
}
return UnwrapResult(
payload: envelope.payload,
messageId: envelope.messageId,
requiresAck: envelope.requiresAck,
);
}
/// Check if a message ID was already processed
bool wasProcessed(String messageId) =>
_processedMessageIds.contains(messageId);
/// Get the count of tracked message IDs
int get processedCount => _processedMessageIds.length;
/// Clear all processed message IDs.
///
/// Primarily for testing purposes.
@visibleForTesting
void clearProcessedIds() => _processedMessageIds.clear();
}

View File

@@ -0,0 +1,123 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:votianlt_app/services/developer.dart' as developer;
class NotificationService {
NotificationService._internal();
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
final FlutterLocalNotificationsPlugin _plugin =
FlutterLocalNotificationsPlugin();
bool _initialized = false;
/// The conversation key of the chat currently being viewed by the user.
/// When set, incoming chat notifications for this conversation are suppressed.
String? activeConversationKey;
static const String _chatChannelId = 'chat_messages';
static const String _chatChannelName = 'Chat-Nachrichten';
static const String _chatChannelDescription =
'Benachrichtigungen bei neuen Chat-Nachrichten';
static const String _jobChannelId = 'new_jobs';
static const String _jobChannelName = 'Neue Jobs';
static const String _jobChannelDescription =
'Benachrichtigungen bei neuen Job-Zuweisungen';
int _nextId = 0;
Future<void> initialize() async {
if (_initialized) return;
const androidSettings = AndroidInitializationSettings(
'@mipmap/ic_launcher',
);
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _plugin.initialize(initSettings);
await _plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();
_initialized = true;
developer.log('NotificationService initialized',
name: 'NotificationService');
}
Future<void> showChatNotification({
required String title,
required String body,
required String conversationKey,
}) async {
if (!_initialized) return;
if (activeConversationKey == conversationKey) return;
const androidDetails = AndroidNotificationDetails(
_chatChannelId,
_chatChannelName,
channelDescription: _chatChannelDescription,
importance: Importance.high,
priority: Priority.high,
playSound: true,
enableVibration: true,
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _plugin.show(_nextId++, title, body, details,
payload: 'chat:$conversationKey');
}
Future<void> showJobNotification({
required String title,
required String body,
}) async {
if (!_initialized) return;
const androidDetails = AndroidNotificationDetails(
_jobChannelId,
_jobChannelName,
channelDescription: _jobChannelDescription,
importance: Importance.high,
priority: Priority.high,
playSound: true,
enableVibration: true,
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _plugin.show(_nextId++, title, body, details, payload: 'job');
}
}

View File

@@ -0,0 +1,535 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:votianlt_app/config/translation_config.dart';
import 'package:votianlt_app/services/dart_mq.dart';
import 'package:votianlt_app/services/developer.dart' as developer;
import 'package:votianlt_app/app_state.dart';
/// Service für Übersetzungen unterstützt LM Studio (lokal) und Moonshot AI (Cloud).
///
/// Das aktive Backend wird in [TranslationConfig.activeBackend] konfiguriert.
/// Verwendet das Singleton-Pattern wie andere Services in der App.
/// Übersetzt in die vom Benutzer in der App eingestellte Sprache.
class TranslationService {
static final TranslationService _instance = TranslationService._internal();
factory TranslationService() => _instance;
TranslationService._internal();
static const String _chatCompletionsEndpoint = '/chat/completions';
// HTTP Client
final http.Client _client = http.Client();
// Verfügbarkeitsstatus
bool _isAvailable = false;
// Aktuell eingestellte Zielsprache (aus der App)
String get _targetLanguageCode => AppState().languageCode;
/// Gibt an ob das Übersetzungsbackend verfügbar ist
bool get isAvailable => _isAvailable;
/// Name des aktiven Backends (für Logs und DartMQ-Nachrichten)
String get _backendName => switch (TranslationConfig.activeBackend) {
TranslationBackend.lmStudio => 'lm-studio',
TranslationBackend.moonshot => 'moonshot-ai',
};
// Verfügbare Sprachen für Übersetzung (alle unterstützten App-Sprachen)
static final Map<String, String> supportedLanguages = {
'de': 'German',
'en': 'English',
'es': 'Spanish',
'fr': 'French',
'pl': 'Polish',
'ru': 'Russian',
'tr': 'Turkish',
'et': 'Estonian',
'lv': 'Latvian',
'lt': 'Lithuanian',
};
/// Initialisiert den Translation Service
Future<void> initialize() async {
try {
// Auf Sprachänderungen hören
_listenToLanguageChanges();
// Verfügbarkeit prüfen
_isAvailable = await _checkAvailability();
_notifyInitialization();
developer.log(
'TranslationService initialisiert - Backend: $_backendName, Zielsprache: ${supportedLanguages[_targetLanguageCode]}',
name: 'TranslationService',
);
} catch (e) {
developer.log('Fehler bei Initialisierung des TranslationService: $e',
name: 'TranslationService');
_isAvailable = false;
}
}
/// Prüft ob das konfigurierte Backend erreichbar ist
Future<bool> _checkAvailability() async {
switch (TranslationConfig.activeBackend) {
case TranslationBackend.lmStudio:
return _checkLmStudioAvailability();
case TranslationBackend.moonshot:
return _checkMoonshotAvailability();
}
}
Future<bool> _checkLmStudioAvailability() async {
try {
final response = await _client
.get(Uri.parse('${TranslationConfig.lmStudioBaseUrl}/v1/models'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final models = data['data'] as List<dynamic>?;
developer.log(
'LM Studio verbunden - Verfügbare Modelle: ${models?.length ?? 0}',
name: 'TranslationService',
);
return true;
}
return false;
} catch (e) {
developer.log('LM Studio nicht erreichbar: $e', name: 'TranslationService');
return false;
}
}
Future<bool> _checkMoonshotAvailability() async {
try {
final response = await _client
.get(
Uri.parse('${TranslationConfig.moonshotBaseUrl}/models'),
headers: {'Authorization': 'Bearer ${TranslationConfig.moonshotApiKey}'},
)
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
developer.log('Moonshot AI verbunden - API erreichbar', name: 'TranslationService');
return true;
}
return false;
} catch (e) {
developer.log('Moonshot AI nicht erreichbar: $e', name: 'TranslationService');
return false;
}
}
/// Sendet Initialisierungs-Notification über DartMQ
void _notifyInitialization() {
DartMQ().publish<Map<String, dynamic>>('translation/service_initialized', {
'language': _targetLanguageCode,
'backend': _backendName,
'isAvailable': isAvailable,
'endpoint': _activeEndpoint,
});
}
/// Hört auf Sprachänderungen und aktualisiert den Service
void _listenToLanguageChanges() {
localeNotifier.addListener(() {
final newLanguage = AppState().languageCode;
developer.log(
'Sprache in App geändert zu: ${supportedLanguages[newLanguage] ?? newLanguage}',
name: 'TranslationService',
);
DartMQ().publish<Map<String, dynamic>>('translation/language_changed', {
'language': newLanguage,
'displayName': supportedLanguages[newLanguage],
'backend': _backendName,
});
});
}
/// Basis-URL des aktiven Backends
String get _activeEndpoint => switch (TranslationConfig.activeBackend) {
TranslationBackend.lmStudio => TranslationConfig.lmStudioBaseUrl,
TranslationBackend.moonshot => TranslationConfig.moonshotBaseUrl,
};
/// Übersetzt einen Text in die vom Benutzer eingestellte Sprache
///
/// [text] - Der zu übersetzende Text
/// [sourceLanguage] - Die Ausgangssprache (optional, wird automatisch erkannt wenn null)
///
/// Gibt den übersetzten Text zurück oder den Originaltext bei Fehlern
Future<String> translate(
String text, {
String? sourceLanguage,
}) async {
if (text.isEmpty) {
return text;
}
// Bei rein numerischem Text oder sehr kurzem Text nicht übersetzen
if (_shouldSkipTranslation(text)) {
developer.log('Übersetzung übersprungen (kein Text): "$text"',
name: 'TranslationService');
return text;
}
// Zielsprache aus der App holen
final targetCode = _targetLanguageCode;
// Wenn Quelle gleich Ziel, nicht übersetzen
final detectedSource = sourceLanguage ?? await _detectLanguage(text);
if (detectedSource == targetCode) {
developer.log('Übersetzung übersprungen (Quelle = Ziel): "$text"',
name: 'TranslationService');
return text;
}
try {
final translatedText = await _translate(text, detectedSource, targetCode);
developer.log(
'Übersetzung [${supportedLanguages[detectedSource]} -> ${supportedLanguages[targetCode]}]:\n'
' Original: "$text"\n'
' Übersetzt: "$translatedText"',
name: 'TranslationService',
);
return translatedText;
} catch (e) {
developer.log(
'Fehler bei der Übersetzung: $e\n Original: "$text"',
name: 'TranslationService',
);
return text; // Bei Fehler Original zurückgeben
}
}
/// Übersetzt eine Liste von Texten in die vom Benutzer eingestellte Sprache
Future<List<String>> translateList(
List<String> texts, {
String? sourceLanguage,
}) async {
if (texts.isEmpty) return texts;
final results = <String>[];
final targetCode = _targetLanguageCode;
developer.log(
'Starte Batch-Übersetzung von ${texts.length} Texten nach ${supportedLanguages[targetCode]}',
name: 'TranslationService',
);
for (int i = 0; i < texts.length; i++) {
final text = texts[i];
if (text.isEmpty || _shouldSkipTranslation(text)) {
results.add(text);
continue;
}
try {
final detectedSource = sourceLanguage ?? await _detectLanguage(text);
if (detectedSource == targetCode) {
results.add(text);
continue;
}
final translatedText = await _translate(text, detectedSource, targetCode);
developer.log(
'Batch [${i + 1}/${texts.length}] [${supportedLanguages[detectedSource]} -> ${supportedLanguages[targetCode]}]:\n'
' Original: "$text"\n'
' Übersetzt: "$translatedText"',
name: 'TranslationService',
);
results.add(translatedText);
} catch (e) {
developer.log(
'Fehler bei Batch-Übersetzung [${i + 1}/${texts.length}]: $e\n Original: "$text"',
name: 'TranslationService',
);
results.add(text);
}
}
developer.log(
'Batch-Übersetzung abgeschlossen: ${texts.length} Texte verarbeitet',
name: 'TranslationService',
);
return results;
}
/// Dispatcht die Übersetzung an das konfigurierte Backend
Future<String> _translate(String text, String sourceCode, String targetCode) {
switch (TranslationConfig.activeBackend) {
case TranslationBackend.lmStudio:
return _translateWithLmStudio(text, sourceCode, targetCode);
case TranslationBackend.moonshot:
return _translateWithMoonshot(text, sourceCode, targetCode);
}
}
/// Übersetzung mit LM Studio REST API (lokales Modell, kein API-Key)
Future<String> _translateWithLmStudio(
String text,
String sourceCode,
String targetCode,
) async {
final targetName = supportedLanguages[targetCode] ?? targetCode;
final sourceName = supportedLanguages[sourceCode] ?? sourceCode;
final systemPrompt =
'You are a professional translator. Translate the user input from $sourceName to $targetName. '
'Return ONLY the translation, without any additional text, explanations, or quotes.';
final requestBody = {
'model': TranslationConfig.lmStudioModel,
'messages': [
{'role': 'system', 'content': systemPrompt},
{'role': 'user', 'content': text},
],
'temperature': 0.1,
'max_tokens': 2048,
'stream': false,
};
developer.log(
'Sende Übersetzungsanfrage an LM Studio: $sourceName -> $targetName (${text.length} Zeichen)',
name: 'TranslationService',
);
final response = await _client
.post(
Uri.parse('${TranslationConfig.lmStudioBaseUrl}$_chatCompletionsEndpoint'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(requestBody),
)
.timeout(const Duration(seconds: 30));
if (response.statusCode != 200) {
throw Exception('LM Studio API Fehler: ${response.statusCode} - ${response.body}');
}
return _extractTranslation(response.body, 'LM Studio');
}
/// Übersetzung mit Moonshot AI Cloud API (Kimi, API-Key erforderlich)
Future<String> _translateWithMoonshot(
String text,
String sourceCode,
String targetCode,
) async {
final targetName = supportedLanguages[targetCode] ?? targetCode;
final sourceName = supportedLanguages[sourceCode] ?? sourceCode;
final systemPrompt =
'You are a professional translator. Translate the user input from $sourceName to $targetName. '
'Return ONLY the translation, without any additional text, explanations, or quotes.';
final requestBody = {
'model': TranslationConfig.moonshotModel,
'messages': [
{'role': 'system', 'content': systemPrompt},
{'role': 'user', 'content': text},
],
'temperature': 0.1,
'max_tokens': 2048,
'stream': false,
};
developer.log(
'Sende Übersetzungsanfrage an Moonshot AI: $sourceName -> $targetName (${text.length} Zeichen)',
name: 'TranslationService',
);
final response = await _client
.post(
Uri.parse('${TranslationConfig.moonshotBaseUrl}$_chatCompletionsEndpoint'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${TranslationConfig.moonshotApiKey}',
},
body: jsonEncode(requestBody),
)
.timeout(const Duration(seconds: 30));
if (response.statusCode != 200) {
throw Exception('Moonshot AI API Fehler: ${response.statusCode} - ${response.body}');
}
return _extractTranslation(response.body, 'Moonshot AI');
}
/// Extrahiert den Übersetzungstext aus der OpenAI-kompatiblen API-Antwort
String _extractTranslation(String responseBody, String backendLabel) {
final data = jsonDecode(responseBody);
final choices = data['choices'] as List<dynamic>?;
if (choices == null || choices.isEmpty) {
throw Exception('Leere Antwort von $backendLabel');
}
final message = choices[0]['message'] as Map<String, dynamic>?;
String translated = message?['content']?.toString().trim() ?? '';
// Anführungszeichen entfernen falls vorhanden
if ((translated.startsWith('"') && translated.endsWith('"')) ||
(translated.startsWith("'") && translated.endsWith("'"))) {
translated = translated.substring(1, translated.length - 1);
}
return translated;
}
/// Hilfsmethode: Erkennt die Sprache eines Textes
Future<String> _detectLanguage(String text) async {
// Für kurze Texte: Default zu Englisch
if (text.length < 10) {
return 'en';
}
// Einfache Heuristik basierend auf häufigen Wörtern/Zeichen
final lowerText = text.toLowerCase();
// Deutsche Wörter prüfen
final germanWords = ['der', 'die', 'das', 'und', 'ist', 'zu', 'den', 'mit', 'von', 'für'];
if (germanWords.any((word) =>
lowerText.contains(' $word ') || lowerText.startsWith('$word '))) {
return 'de';
}
// Französische Wörter prüfen
final frenchWords = ['le', 'la', 'les', 'et', 'est', 'pour', 'dans', 'sur', 'avec', 'une'];
if (frenchWords.any((word) =>
lowerText.contains(' $word ') || lowerText.startsWith('$word '))) {
return 'fr';
}
// Spanische Wörter prüfen
final spanishWords = ['el', 'la', 'los', 'las', 'y', 'es', 'para', 'con', 'por', 'del'];
if (spanishWords.any((word) =>
lowerText.contains(' $word ') || lowerText.startsWith('$word '))) {
return 'es';
}
// Polnische Wörter prüfen
final polishWords = ['jest', 'i', 'w', 'na', 'do', 'nie', 'się', 'tego', 'tej'];
if (polishWords.any((word) =>
lowerText.contains(' $word ') || lowerText.startsWith('$word '))) {
return 'pl';
}
// Russische/Cyrillische Zeichen prüfen
if (RegExp(r'[а-яА-Я]').hasMatch(text)) {
return 'ru';
}
// Türkische Zeichen prüfen
if (RegExp(r'[çğıöşüÇĞİÖŞÜ]').hasMatch(text)) {
return 'tr';
}
// Estnische Zeichen prüfen
if (RegExp(r'[äöüõÄÖÜÕ]').hasMatch(text)) {
return 'et';
}
// Lettische Zeichen prüfen
if (RegExp(r'[āčēģīķļņšūžĀČĒĢĪĶĻŅŠŪŽ]').hasMatch(text)) {
return 'lv';
}
// Litauische Zeichen prüfen
if (RegExp(r'[ąčęėįšųūžĄČĘĖĮŠŲŪŽ]').hasMatch(text)) {
return 'lt';
}
// Arabische Zeichen prüfen
if (RegExp(r'[\u0600-\u06FF]').hasMatch(text)) {
return 'ar';
}
// Chinesische/Japanische/Koreanische Zeichen prüfen
if (RegExp(r'[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff]').hasMatch(text)) {
return 'zh';
}
// Default: Englisch
return 'en';
}
/// Prüft ob die Übersetzung übersprungen werden sollte
bool _shouldSkipTranslation(String text) {
// Numerische Werte nicht übersetzen
if (RegExp(r'^\d+$').hasMatch(text.trim())) {
return true;
}
// Sehr kurze Codes nicht übersetzen
if (text.trim().length <= 2) {
return true;
}
// E-Mail Adressen nicht übersetzen
if (text.contains('@') && text.contains('.')) {
return true;
}
// URLs nicht übersetzen
if (text.startsWith('http://') || text.startsWith('https://')) {
return true;
}
return false;
}
/// Prüft ob ein Übersetzungsmodell verfügbar ist
Future<bool> isModelAvailable() async {
return _checkAvailability();
}
/// Gibt detaillierte Verfügbarkeitsinformationen zurück
Future<Map<String, dynamic>> getAvailabilityInfo() async {
final isOnline = await _checkAvailability();
return {
'isAvailable': isOnline,
'backend': _backendName,
'targetLanguage': _targetLanguageCode,
'endpoint': _activeEndpoint,
'platform': Platform.operatingSystem,
};
}
/// Gibt die aktuell eingestellte Zielsprache zurück
String get targetLanguageCode => _targetLanguageCode;
/// Gibt den Anzeigenamen der aktuellen Sprache zurück
String get targetLanguageDisplayName {
return supportedLanguages[_targetLanguageCode] ?? _targetLanguageCode;
}
/// Gibt eine Liste aller verfügbaren Sprachen zurück
List<MapEntry<String, String>> getAvailableLanguages() {
return supportedLanguages.entries.toList();
}
/// Gibt den Anzeigenamen einer Sprache zurück
String getLanguageDisplayName(String code) {
return supportedLanguages[code] ?? code;
}
/// Schließt den Service und gibt Ressourcen frei
Future<void> dispose() async {
_client.close();
_isAvailable = false;
developer.log('TranslationService disposed', name: 'TranslationService');
}
}

File diff suppressed because it is too large Load Diff

225
app/lib/settings_view.dart Normal file
View File

@@ -0,0 +1,225 @@
import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart';
import 'app_state.dart';
/// Supported languages with their display names and flag emojis
class LanguageOption {
final String code;
final String name;
final String flagEmoji;
const LanguageOption({
required this.code,
required this.name,
required this.flagEmoji,
});
}
class SettingsView extends StatefulWidget {
const SettingsView({super.key});
@override
State<SettingsView> createState() => _SettingsViewState();
}
class _SettingsViewState extends State<SettingsView> {
late String _selectedLanguageCode;
final AppState _appState = AppState();
@override
void initState() {
super.initState();
_selectedLanguageCode = _appState.languageCode;
}
void _onLanguageSelected(String languageCode) async {
setState(() {
_selectedLanguageCode = languageCode;
});
// Save language preference
await _appState.setLanguage(languageCode);
// Show confirmation snackbar
_showLanguageChangedSnackBar(languageCode);
}
void _showLanguageChangedSnackBar(String languageCode) {
final l10n = AppLocalizations.of(context);
// Get the language name from the corresponding localization
String languageName;
String flagEmoji;
switch (languageCode) {
case 'de':
languageName = 'Deutsch';
flagEmoji = '🇩🇪';
break;
case 'en':
languageName = 'English';
flagEmoji = '🇬🇧';
break;
case 'es':
languageName = 'Español';
flagEmoji = '🇪🇸';
break;
case 'fr':
languageName = 'Français';
flagEmoji = '🇫🇷';
break;
case 'pl':
languageName = 'Polski';
flagEmoji = '🇵🇱';
break;
case 'ru':
languageName = 'Русский';
flagEmoji = '🇷🇺';
break;
case 'tr':
languageName = 'Türkçe';
flagEmoji = '🇹🇷';
break;
case 'et':
languageName = 'Eesti';
flagEmoji = '🇪🇪';
break;
case 'lv':
languageName = 'Latviešu';
flagEmoji = '🇱🇻';
break;
case 'lt':
languageName = 'Lietuvių';
flagEmoji = '🇱🇹';
break;
default:
languageName = languageCode;
flagEmoji = '🌐';
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${l10n.languageChanged}: $flagEmoji $languageName',
),
duration: const Duration(seconds: 2),
backgroundColor: Colors.green,
),
);
}
/// Get all available language options with their localized names
List<LanguageOption> _getLanguageOptions() {
return [
const LanguageOption(code: 'de', name: 'Deutsch', flagEmoji: '🇩🇪'),
const LanguageOption(code: 'en', name: 'English', flagEmoji: '🇬🇧'),
const LanguageOption(code: 'es', name: 'Español', flagEmoji: '🇪🇸'),
const LanguageOption(code: 'fr', name: 'Français', flagEmoji: '🇫🇷'),
const LanguageOption(code: 'pl', name: 'Polski', flagEmoji: '🇵🇱'),
const LanguageOption(code: 'ru', name: 'Русский', flagEmoji: '🇷🇺'),
const LanguageOption(code: 'tr', name: 'Türkçe', flagEmoji: '🇹🇷'),
const LanguageOption(code: 'et', name: 'Eesti', flagEmoji: '🇪🇪'),
const LanguageOption(code: 'lv', name: 'Latviešu', flagEmoji: '🇱🇻'),
const LanguageOption(code: 'lt', name: 'Lietuvių', flagEmoji: '🇱🇹'),
];
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final languageOptions = _getLanguageOptions();
return Scaffold(
appBar: AppBar(
title: Text(l10n.settings),
backgroundColor: Colors.deepPurple[100],
),
body: ListView(
children: [
// Language Selection Section
Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
l10n.language.toUpperCase(),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey,
letterSpacing: 1.2,
),
),
),
const Divider(height: 1),
// Language List
...languageOptions.map((language) {
final isSelected = language.code == _selectedLanguageCode;
return Column(
children: [
ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Text(
language.flagEmoji,
style: const TextStyle(fontSize: 24),
),
),
),
title: Text(
language.name,
style: TextStyle(
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected ? Colors.deepPurple : Colors.black87,
),
),
trailing: isSelected
? const Icon(
Icons.check_circle,
color: Colors.deepPurple,
)
: const Icon(
Icons.circle_outlined,
color: Colors.grey,
),
onTap: () => _onLanguageSelected(language.code),
selected: isSelected,
selectedTileColor: Colors.deepPurple.withValues(alpha: 0.05),
),
const Divider(height: 1, indent: 72),
],
);
}),
// App Info Section
Padding(
padding: const EdgeInsets.fromLTRB(16, 32, 16, 8),
child: Text(
l10n.appInfo,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey,
letterSpacing: 1.2,
),
),
),
const Divider(height: 1),
ListTile(
leading: Icon(
Icons.info_outline,
color: Colors.grey[600],
),
title: Text(l10n.version),
subtitle: const Text('0.9.2'),
),
const Divider(height: 1, indent: 72),
],
),
);
}
}

796
app/lib/task_view.dart Normal file
View File

@@ -0,0 +1,796 @@
import 'dart:async';
import 'dart:convert';
import 'package:votianlt_app/services/developer.dart' as developer;
import 'package:image/image.dart' as img;
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'l10n/app_localizations.dart';
import 'models/job.dart';
import 'models/task.dart';
import 'models/tasks/confirmation_task.dart';
import 'models/tasks/photo_task.dart';
import 'models/tasks/todolist_task.dart';
import 'models/tasks/signature_task.dart';
import 'models/tasks/barcode_task.dart';
import 'models/tasks/comment_task.dart';
import 'services/database_service.dart';
import 'widgets/offline_banner.dart';
import 'services/websocket_service.dart';
import 'Tasks/photo_capture_screen.dart';
import 'Tasks/barcode_capture_screen.dart';
import 'Tasks/signature_capture_screen.dart';
class TaskView extends StatefulWidget {
final Job job;
final int? stationOrder;
final String? stationTitle;
const TaskView({
super.key,
required this.job,
this.stationOrder,
this.stationTitle,
});
@override
State<TaskView> createState() => _TaskViewState();
}
class _TaskViewState extends State<TaskView> {
final Set<String> _completedTasks = {};
final Set<String> _skippedTasks = {};
final DatabaseService _databaseService = DatabaseService();
// Store SVG representations of signatures per task for later use
final Map<String, String> _signatureSvgByTask = {};
@override
void initState() {
super.initState();
_loadTaskStatuses();
}
List<Task> get _visibleTasks {
final stationOrder = widget.stationOrder;
if (stationOrder == null) {
return widget.job.tasks;
}
return widget.job.tasks
.where((task) => task.stationOrder == stationOrder)
.toList();
}
/// Load task completion statuses from database and merge with JSON task states
Future<void> _loadTaskStatuses() async {
final statuses = await _databaseService.loadAllTaskStatuses();
setState(() {
_completedTasks.clear();
// 1) Add all completed from DB
for (final entry in statuses.entries) {
if (entry.value) {
_completedTasks.add(entry.key);
}
}
// 2) Merge: also mark tasks completed if the job JSON already had them completed
for (final t in widget.job.tasks) {
if (t.completed) {
_completedTasks.add(t.id);
}
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
widget.stationTitle?.isNotEmpty == true
? '${AppLocalizations.of(context).tasks} - ${widget.stationTitle}'
: '${AppLocalizations.of(context).tasks} - ${widget.job.jobNumber}',
),
backgroundColor: Colors.deepPurple[100],
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
},
),
actions: [
IconButton(
icon: const Icon(Icons.chat),
onPressed: () {
Navigator.of(context).pushNamed('/chats');
},
tooltip: AppLocalizations.of(context).openChat,
),
],
),
body: Column(
children: [
OfflineBanner(),
if (_getRemark().isNotEmpty)
Container(
width: double.infinity,
margin: const EdgeInsets.all(5),
constraints: const BoxConstraints(maxHeight: 150),
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
border: Border.all(color: Colors.grey[300]!, width: 1),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
child: Text(
_getRemark(),
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Expanded(child: _buildTasksStepper())],
),
),
),
],
),
);
}
Widget _buildTasksStepper() {
if (_visibleTasks.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.task_outlined, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context).noTasks,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context).noTasksMessage,
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
textAlign: TextAlign.center,
),
],
),
);
}
return ListView.builder(
itemCount: _visibleTasks.length,
itemBuilder: (context, index) {
final task = _visibleTasks[index];
final isCompleted = _completedTasks.contains(task.id);
final isSkipped = _skippedTasks.contains(task.id);
final canBeCompletedNow =
!isCompleted && !isSkipped && _arePreviousTasksCompleted(index);
// Hintergrundfarbe je nach Status:
// abgeschlossen → hellgrün, übersprungen → hellgelb, bearbeitbar → weiß, gesperrt → hellgrau
final Color cardColor =
isCompleted
? const Color(0xFFE8F5E9) // hellgrün
: isSkipped
? const Color(0xFFFFF8E1) // hellgelb
: canBeCompletedNow
? Colors.white
: const Color(0xFFF5F5F5); // hellgrau
final Color borderColor =
isCompleted
? Colors.green[300]!
: isSkipped
? Colors.amber[300]!
: canBeCompletedNow
? Colors.grey[300]!
: Colors.grey[200]!;
final Color circleColor =
isCompleted
? Colors.green[600]!
: isSkipped
? Colors.amber[600]!
: canBeCompletedNow
? Colors.deepPurple[400]!
: Colors.grey[400]!;
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: isCompleted || canBeCompletedNow ? 2 : 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: borderColor, width: 1),
),
child: InkWell(
onTap:
canBeCompletedNow
? () => _showTaskCompletionDialog(task, index)
: null,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: cardColor,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Task number circle
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: circleColor,
),
child: Center(
child: Text(
'${index + 1}',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
const SizedBox(width: 16),
// Task content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTaskDisplayText(
task,
isCompleted || isSkipped,
index,
),
if (_getTaskStationLabel(task) != null) ...[
const SizedBox(height: 4),
Text(
_getTaskStationLabel(task)!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
],
],
),
),
if (isCompleted) ...[
const SizedBox(width: 8),
Icon(Icons.check_circle, color: Colors.green[600]),
],
if (isSkipped) ...[
const SizedBox(width: 8),
Icon(Icons.skip_next, color: Colors.amber[600]),
],
],
),
),
),
);
},
);
}
void _showTaskCompletionDialog(Task task, int taskIndex) {
switch (task) {
case ConfirmationTask():
_showConfirmationDialog(task, taskIndex);
break;
case PhotoTask():
_showPhotoDialog(task);
break;
case TodoListTask():
_showTodoListDialog(task);
break;
case SignatureTask():
_showSignatureDialog(task);
break;
case BarcodeTask():
_showBarcodeDialog(task);
break;
case CommentTask():
_showCommentDialog(task);
break;
default:
_showGenericDialog(task);
break;
}
}
void _showConfirmationDialog(ConfirmationTask task, int taskIndex) {
final description =
task.description?.isNotEmpty == true
? task.description!
: AppLocalizations.of(context).confirmationDescription;
final buttonText =
task.buttonText.isNotEmpty
? task.buttonText
: AppLocalizations.of(context).confirm;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).confirmationRequired),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text(description)],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context).cancel),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_completeTask(task.id, taskType: 'CONFIRMATION');
},
child: Text(buttonText),
),
],
);
},
);
}
// Compress photos and Base64-encode while keeping total payload under a cap
Future<List<String>> _compressAndEncodePhotos(
List<Uint8List> photos, {
int maxDim = 1280,
int jpegQuality = 70,
int maxTotalBase64Bytes = 450 * 1024,
}) async {
final List<String> encoded = [];
int total = 0;
for (final bytes in photos) {
try {
final img.Image? decoded = img.decodeImage(bytes);
if (decoded == null) {
continue;
}
// Resize if needed keeping aspect ratio
final int w = decoded.width;
final int h = decoded.height;
img.Image resized = decoded;
final int longest = w > h ? w : h;
if (longest > maxDim) {
if (w >= h) {
resized = img.copyResize(decoded, width: maxDim);
} else {
resized = img.copyResize(decoded, height: maxDim);
}
}
final List<int> jpg = img.encodeJpg(resized, quality: jpegQuality);
final String b64 = base64Encode(jpg);
// Respect total payload cap
if (total + b64.length > maxTotalBase64Bytes) {
break;
}
encoded.add(b64);
total += b64.length;
} catch (e, st) {
developer.log('Photo compress/encode error: $e', name: 'TaskView');
developer.log('Stack: $st', name: 'TaskView');
}
}
return encoded;
}
void _showPhotoDialog(PhotoTask task) {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => PhotoCaptureScreen(
task: task,
onPhotosCompleted: (List<Uint8List> photoData) async {
// Compress + encode photos for network send (limit payload)
final List<String> base64List = await _compressAndEncodePhotos(
photoData,
);
final bool truncated = base64List.length < photoData.length;
// Try to persist full-quality (encoded) photos to DB for offline/backup
try {
// Persist the compressed versions to keep DB size reasonable as well
await _databaseService.saveTaskPhotos(task.id, base64List);
} catch (e, stackTrace) {
developer.log(
'Error saving task photos: $e',
name: 'TaskView',
);
developer.log('Stack trace: $stackTrace', name: 'TaskView');
}
// Always complete the task regardless of persistence success/failure
_completeTask(
task.id,
taskType: 'PHOTO',
extraData: {
'photos': base64List,
'count': photoData.length,
if (truncated) 'truncated': true,
},
);
},
),
),
);
}
void _showTodoListDialog(TodoListTask task) {
final items = task.todoItems;
final List<bool> checkedItems = List.filled(items.length, false);
showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Text(AppLocalizations.of(context).checklist),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(context).checklistDescription),
const SizedBox(height: 16),
...items.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return CheckboxListTile(
title: Text(item),
value: checkedItems[index],
onChanged: (bool? value) {
setState(() {
checkedItems[index] = value ?? false;
});
},
dense: true,
contentPadding: EdgeInsets.zero,
);
}),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context).abort),
),
ElevatedButton(
onPressed:
checkedItems.every((checked) => checked)
? () {
Navigator.of(context).pop();
_completeTask(
task.id,
taskType: 'TODOLIST',
extraData: {
'items': task.todoItems,
'checkedStates': checkedItems,
},
);
}
: null,
child: Text(AppLocalizations.of(context).finish),
),
],
);
},
);
},
);
}
void _showSignatureDialog(SignatureTask task) {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => SignatureCaptureScreen(
task: task,
onSignatureCompleted: (String svg) async {
try {
// Persist SVG only (no PNG)
await _databaseService.saveTaskSignature(task.id, svg);
} catch (e, stackTrace) {
developer.log(
'Error saving task signature: $e',
name: 'TaskView',
);
developer.log('Stack trace: $stackTrace', name: 'TaskView');
}
// Store SVG for later use in this TaskView session
setState(() {
_signatureSvgByTask[task.id] = svg;
});
// Read back once (for analyzer to see it used) and optional debug
debugPrint(
'Signature SVG stored for task ${task.id}: length=${_signatureSvgByTask[task.id]?.length ?? 0}',
);
_completeTask(
task.id,
taskType: 'SIGNATURE',
extraData: {
'signatureSvg': svg,
'svgLength': svg.length,
'hasSignature': true,
},
);
},
),
),
);
}
void _showBarcodeDialog(BarcodeTask task) {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => BarcodeCaptureScreen(
task: task,
onBarcodesCompleted: (List<String> barcodes) async {
try {
// Save barcodes to database for later use
await _databaseService.saveTaskBarcodes(task.id, barcodes);
} catch (e, stackTrace) {
developer.log(
'Error saving task barcodes: $e',
name: 'TaskView',
);
developer.log('Stack trace: $stackTrace', name: 'TaskView');
}
_completeTask(
task.id,
taskType: 'BARCODE',
extraData: {'barcodes': barcodes, 'count': barcodes.length},
);
},
),
),
);
}
void _showGenericDialog(Task task) {
final TextEditingController noteController = TextEditingController();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).completeTask),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(context).completeTaskConfirm),
const SizedBox(height: 16),
TextField(
controller: noteController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).completeTaskNote,
border: const OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context).abort),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_completeTask(task.id, taskType: 'GENERIC');
},
child: Text(AppLocalizations.of(context).complete),
),
],
);
},
);
}
void _completeTask(
String taskId, {
String? taskType,
Map<String, dynamic>? extraData,
}) {
setState(() {
_completedTasks.add(taskId);
});
// Save to database
_databaseService.saveTaskStatus(taskId, true);
// Notify server via STOMP about task completion (best-effort)
try {
StompService().sendTaskCompleted(
taskId: taskId,
taskType: taskType,
extraData: extraData,
);
} catch (e) {
developer.log('Error sending task completion: $e', name: 'TaskView');
}
}
bool _arePreviousTasksCompleted(int index) {
if (index <= 0) return true;
for (int i = 0; i < index; i++) {
final t = _visibleTasks[i];
if (!t.optional &&
!_completedTasks.contains(t.id) &&
!_skippedTasks.contains(t.id)) {
return false;
}
}
return true;
}
void _showCommentDialog(CommentTask task) {
final TextEditingController commentController = TextEditingController();
commentController.text = task.commentText;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).enterComment),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(context).commentDescription),
const SizedBox(height: 16),
TextField(
controller: commentController,
decoration: InputDecoration(
labelText:
task.required
? AppLocalizations.of(context).commentRequired
: AppLocalizations.of(context).comment,
border: const OutlineInputBorder(),
hintText: '...',
),
maxLines: 4,
minLines: 2,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context).abort),
),
ElevatedButton(
onPressed: () {
final comment = commentController.text.trim();
if (task.required && comment.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).commentRequired,
),
),
);
return;
}
Navigator.of(context).pop();
_completeTask(
task.id,
taskType: 'COMMENT',
extraData: {
'commentText': comment,
'required': task.required,
},
);
},
child: Text(AppLocalizations.of(context).save),
),
],
);
},
);
}
Widget _buildTaskDisplayText(Task task, bool isCompleted, int taskIndex) {
final titleStyle = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
decoration: isCompleted ? TextDecoration.lineThrough : null,
);
final subtitleStyle = TextStyle(
fontSize: 13,
color: Colors.grey[600],
decoration: isCompleted ? TextDecoration.lineThrough : null,
);
final displayName = task.displayName;
final description = task.description;
if (displayName?.isNotEmpty == true) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(displayName!, style: titleStyle),
if (description?.isNotEmpty == true) ...[
const SizedBox(height: 2),
Text(description!, style: subtitleStyle),
],
],
);
}
if (description?.isNotEmpty == true) {
return Text(description!, style: titleStyle);
}
// Fall back to standard text based on task type
return Text(_getStandardTaskDisplayText(task), style: titleStyle);
}
String _getStandardTaskDisplayText(Task task) {
// Generate display text based on task type
switch (task) {
case PhotoTask():
return '${AppLocalizations.of(context).takePhotos} (${task.minPhotoCount}-${task.maxPhotoCount} ${AppLocalizations.of(context).photosCount})';
case TodoListTask():
return '${AppLocalizations.of(context).checklist} (${task.todoItems.length} ${AppLocalizations.of(context).checklistPoints})';
case SignatureTask():
return AppLocalizations.of(context).signatureRequiredText;
case BarcodeTask():
return '${AppLocalizations.of(context).scanBarcodes} (${task.minBarcodeCount}-${task.maxBarcodeCount} ${AppLocalizations.of(context).barcodeCount})';
case CommentTask():
return task.required
? AppLocalizations.of(context).commentRequired
: AppLocalizations.of(context).commentOptional;
default:
return AppLocalizations.of(context).genericTask;
}
}
String _getRemark() => widget.job.remark;
String? _getTaskStationLabel(Task task) {
if (widget.stationOrder != null) {
return null;
}
final stationOrder = task.stationOrder;
if (stationOrder == null) {
return null;
}
for (final station in widget.job.deliveryStations) {
if (station.stationOrder == stationOrder) {
final suffix =
station.displayName.isNotEmpty ? station.displayName : station.city;
return suffix.isNotEmpty
? 'Station ${stationOrder + 1}: $suffix'
: 'Station ${stationOrder + 1}';
}
}
return 'Station ${stationOrder + 1}';
}
}

View File

@@ -0,0 +1,227 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import '../l10n/app_localizations.dart';
import '../models/tasks/barcode_task.dart';
import '../widgets/offline_banner.dart';
class BarcodeCaptureScreen extends StatefulWidget {
final BarcodeTask task;
final Function(List<String>) onBarcodesCompleted;
const BarcodeCaptureScreen({super.key, required this.task, required this.onBarcodesCompleted});
@override
State<BarcodeCaptureScreen> createState() => _BarcodeCaptureScreenState();
}
class _BarcodeCaptureScreenState extends State<BarcodeCaptureScreen> {
final List<String> _scannedBarcodes = [];
final List<TextEditingController> _textControllers = [];
MobileScannerController? _scannerController;
bool _isMobilePlatform = false;
bool _isScannerInitialized = false;
@override
void initState() {
super.initState();
_detectPlatformAndInit();
}
@override
void dispose() {
_scannerController?.dispose();
for (final controller in _textControllers) {
controller.dispose();
}
super.dispose();
}
void _detectPlatformAndInit() {
// Determine if we're on a mobile platform
if (kIsWeb) {
_isMobilePlatform = false;
_initializeDesktopMode();
} else {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
_isMobilePlatform = true;
_initializeMobileScanner();
break;
case TargetPlatform.macOS:
case TargetPlatform.windows:
case TargetPlatform.linux:
_isMobilePlatform = false;
_initializeDesktopMode();
break;
default:
_isMobilePlatform = false;
_initializeDesktopMode();
}
}
}
void _initializeMobileScanner() {
try {
_scannerController = MobileScannerController();
setState(() {
_isScannerInitialized = true;
});
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${AppLocalizations.of(context).cameraError}: $e')));
}
}
}
void _initializeDesktopMode() {
// Create text controllers for desktop input fields
for (int i = 0; i < widget.task.maxBarcodeCount; i++) {
_textControllers.add(TextEditingController());
}
setState(() {
_isScannerInitialized = true;
});
}
void _onBarcodeDetected(BarcodeCapture capture) {
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
final String? code = barcode.rawValue;
if (code != null && code.isNotEmpty && !_scannedBarcodes.contains(code)) {
if (_scannedBarcodes.length < widget.task.maxBarcodeCount) {
setState(() {
_scannedBarcodes.add(code);
});
}
}
}
}
void _removeBarcode(int index) {
setState(() {
_scannedBarcodes.removeAt(index);
});
}
void _finishTask() {
final List<String> barcodes;
if (_isMobilePlatform) {
barcodes = _scannedBarcodes;
} else {
// Collect barcodes from text fields
barcodes = [];
for (final controller in _textControllers) {
if (controller.text.trim().isNotEmpty) {
barcodes.add(controller.text.trim());
}
}
}
// Navigate back to task view first
Navigator.of(context).pop();
// Then call the completion callback
widget.onBarcodesCompleted(barcodes);
}
bool _canFinish() {
if (_isMobilePlatform) {
return _scannedBarcodes.length >= widget.task.minBarcodeCount;
} else {
int filledFields = 0;
for (final controller in _textControllers) {
if (controller.text.trim().isNotEmpty) {
filledFields++;
}
}
return filledFields >= widget.task.minBarcodeCount;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(title: Text(AppLocalizations.of(context).barcodeScan), backgroundColor: Colors.deepPurple[100], leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.of(context).pop())), body: Column(children: [OfflineBanner(), Expanded(child: _isScannerInitialized ? (_isMobilePlatform ? _buildMobileView() : _buildDesktopView()) : const Center(child: CircularProgressIndicator()))]));
}
Widget _buildMobileView() {
return Column(
children: [
// Scanner view
Expanded(
flex: 3,
child: Stack(
children: [
MobileScanner(controller: _scannerController, onDetect: _onBarcodeDetected),
// Overlay with scanning frame
Container(decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.5)), child: Center(child: Container(width: 250, height: 250, decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2), borderRadius: BorderRadius.circular(12)), child: Container(margin: const EdgeInsets.all(20), decoration: BoxDecoration(border: Border.all(color: Colors.green, width: 2), borderRadius: BorderRadius.circular(8)))))),
],
),
),
// Scanned barcodes list
Expanded(
flex: 2,
child: Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${AppLocalizations.of(context).scannedBarcodes} (${_scannedBarcodes.length}/${widget.task.maxBarcodeCount})', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('${AppLocalizations.of(context).minBarcodes} ${widget.task.minBarcodeCount} ${AppLocalizations.of(context).barcodesRequired}', style: TextStyle(fontSize: 14, color: Colors.grey[600])),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: _scannedBarcodes.length,
itemBuilder: (context, index) {
return Card(child: ListTile(leading: const Icon(Icons.qr_code), title: Text(_scannedBarcodes[index]), trailing: IconButton(icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => _removeBarcode(index))));
},
),
),
const SizedBox(height: 16),
SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _canFinish() ? _finishTask : null, style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), child: Text(AppLocalizations.of(context).finish))),
],
),
),
),
],
);
}
Widget _buildDesktopView() {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(context).enterBarcode, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('${AppLocalizations.of(context).barcodeEnterDescription} (${widget.task.minBarcodeCount}-${widget.task.maxBarcodeCount})', style: TextStyle(fontSize: 16, color: Colors.grey[600])),
const SizedBox(height: 24),
Expanded(
child: ListView.builder(
itemCount: widget.task.maxBarcodeCount,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextField(
controller: _textControllers[index],
decoration: InputDecoration(labelText: index < widget.task.minBarcodeCount ? AppLocalizations.of(context).barcodeNumberRequired(index + 1) : AppLocalizations.of(context).barcodeNumberOptional(index + 1), border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.qr_code)),
onChanged: (value) {
setState(() {
// Trigger rebuild to update button state
});
},
),
);
},
),
),
const SizedBox(height: 16),
SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _canFinish() ? _finishTask : null, style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), child: Text(AppLocalizations.of(context).finish))),
],
),
);
}
}

View File

@@ -0,0 +1,679 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:camera/camera.dart';
import 'package:file_picker/file_picker.dart';
import 'package:file_selector/file_selector.dart' as fsel;
import 'package:votianlt_app/services/developer.dart' as developer;
import '../l10n/app_localizations.dart';
import '../models/tasks/photo_task.dart';
import '../widgets/offline_banner.dart';
class PhotoCaptureScreen extends StatefulWidget {
final PhotoTask task;
final Function(List<Uint8List>) onPhotosCompleted;
const PhotoCaptureScreen({
super.key,
required this.task,
required this.onPhotosCompleted,
});
@override
State<PhotoCaptureScreen> createState() => _PhotoCaptureScreenState();
}
class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
CameraController? _cameraController; // Android/iOS/Web
List<CameraDescription>? _cameras;
final List<Uint8List> _capturedPhotos = [];
final PageController _pageController = PageController();
int _currentPhotoIndex = 0;
bool _isCameraInitialized = false;
bool _isCameraSupportedOnThisPlatform = false;
bool _useFilePickerMode = false; // desktop fallback
@override
void initState() {
super.initState();
_detectPlatformSupportAndInit();
}
@override
void dispose() {
_cameraController?.dispose();
_pageController.dispose();
super.dispose();
}
void _detectPlatformSupportAndInit() {
// Requirement: Desktop (macOS/Windows/Linux) uses file picker; Android/iOS uses camera.
if (kIsWeb) {
// Keep web behavior using camera if available
_isCameraSupportedOnThisPlatform = true;
_initializeCamera();
return;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
_isCameraSupportedOnThisPlatform = true; // enable capture button
_useFilePickerMode = false;
_initializeCamera();
return;
case TargetPlatform.macOS:
case TargetPlatform.windows:
case TargetPlatform.linux:
// Desktop → use file picker instead of camera
_isCameraSupportedOnThisPlatform = true; // enable button
_useFilePickerMode = true;
return;
default:
_isCameraSupportedOnThisPlatform = false;
}
}
Future<void> _initializeCamera() async {
try {
// Android/iOS/Web camera initialization
_cameras = await availableCameras();
if (_cameras != null && _cameras!.isNotEmpty) {
_cameraController = CameraController(
_cameras![0],
ResolutionPreset.medium,
);
await _cameraController!.initialize();
if (mounted) {
setState(() {
_isCameraInitialized = true;
});
}
}
} catch (e, stackTrace) {
developer.log('Error initializing camera: $e', name: 'PhotoCaptureScreen');
developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${AppLocalizations.of(context).cameraError}: $e')),
);
}
}
}
Future<void> _capturePhoto() async {
try {
// Camera plugin path (Android/iOS/Web)
if (_cameraController != null && _isCameraInitialized) {
final XFile photo = await _cameraController!.takePicture();
final Uint8List photoBytes = await photo.readAsBytes();
setState(() {
_capturedPhotos.add(photoBytes);
});
// Navigate to the newly added photo (even if it's the first one). If the
// PageView is not attached yet, _showPhotoAt will schedule a post-frame jump.
_showPhotoAt(_capturedPhotos.length - 1);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).cameraNotReady)),
);
}
}
} catch (e, stackTrace) {
developer.log('Error capturing photo: $e', name: 'PhotoCaptureScreen');
developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${AppLocalizations.of(context).photoError}: $e')),
);
}
}
}
Future<void> _pickPhotoFromFile() async {
try {
// Use file_selector for desktop and web for robust platform support
final bool useFileSelector = kIsWeb ||
defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.windows ||
defaultTargetPlatform == TargetPlatform.linux;
if (useFileSelector) {
final typeGroup = fsel.XTypeGroup(
label: 'images',
extensions: ['jpg', 'jpeg', 'png', 'heic', 'bmp', 'gif', 'webp'],
);
final fsel.XFile? picked = await fsel.openFile(acceptedTypeGroups: [typeGroup]);
if (picked != null) {
final data = await picked.readAsBytes();
setState(() {
_capturedPhotos.add(data);
});
_showPhotoAt(_capturedPhotos.length - 1);
}
} else {
// On Android/iOS, use file_picker which integrates with platform pickers
final result = await FilePicker.platform.pickFiles(
allowMultiple: false,
type: FileType.image,
withData: true,
);
if (result != null && result.files.isNotEmpty) {
final file = result.files.first;
final bytes = file.bytes;
if (bytes != null) {
setState(() {
_capturedPhotos.add(bytes);
});
_showPhotoAt(_capturedPhotos.length - 1);
} else {
// On some platforms, bytes may be null if withData was false; try path
final path = file.path;
if (path != null) {
final data = await XFile(path).readAsBytes();
setState(() {
_capturedPhotos.add(data);
});
if (_capturedPhotos.length > 1) {
_showPhotoAt(_capturedPhotos.length - 1);
}
}
}
}
}
} catch (e, stackTrace) {
developer.log('Error picking photo from file: $e', name: 'PhotoCaptureScreen');
developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${AppLocalizations.of(context).photoError}: $e')),
);
}
}
}
void _deletePhoto(int index) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).deletePhoto),
content: Text(AppLocalizations.of(context).deletePhotoConfirm),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context).cancel),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// Remove and navigate to a valid photo if any remain
int targetIndex = _currentPhotoIndex;
setState(() {
_capturedPhotos.removeAt(index);
if (_capturedPhotos.isEmpty) {
_currentPhotoIndex = 0;
} else {
if (targetIndex >= _capturedPhotos.length) {
targetIndex = _capturedPhotos.length - 1;
}
_currentPhotoIndex = targetIndex;
}
});
if (_capturedPhotos.isNotEmpty) {
_showPhotoAt(_currentPhotoIndex);
}
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: Text(AppLocalizations.of(context).delete, style: const TextStyle(color: Colors.white)),
),
],
);
},
);
}
bool get _canComplete {
return _capturedPhotos.length >= widget.task.minPhotoCount &&
_capturedPhotos.length <= widget.task.maxPhotoCount;
}
bool get _canTakeMore {
return _capturedPhotos.length < widget.task.maxPhotoCount;
}
bool get _isDesktopPlatform {
if (kIsWeb) return false;
switch (defaultTargetPlatform) {
case TargetPlatform.macOS:
case TargetPlatform.windows:
case TargetPlatform.linux:
return true;
default:
return false;
}
}
void _showPhotoAt(int index) {
if (_capturedPhotos.isEmpty) return;
final int clamped = index.clamp(0, _capturedPhotos.length - 1);
if (!mounted) return;
// Always schedule navigation after the current frame so that
// PageView has rebuilt with the latest itemCount before we jump/animate.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
if (_pageController.hasClients) {
try {
_pageController.animateToPage(
clamped,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
} catch (e, stackTrace) {
developer.log('Error animating to page: $e', name: 'PhotoCaptureScreen');
developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen');
_pageController.jumpToPage(clamped);
}
setState(() {
_currentPhotoIndex = clamped;
});
}
});
}
void _goToPreviousPhoto() {
if (_currentPhotoIndex > 0) {
_showPhotoAt(_currentPhotoIndex - 1);
}
}
void _goToNextPhoto() {
if (_currentPhotoIndex < _capturedPhotos.length - 1) {
_showPhotoAt(_currentPhotoIndex + 1);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).photoCapture),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
actions: [
if (_canComplete)
TextButton(
onPressed: () {
widget.onPhotosCompleted(_capturedPhotos);
Navigator.of(context).pop();
},
child: Text(
AppLocalizations.of(context).finish,
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
],
),
body: Column(
children: [
OfflineBanner(),
// Task info header
Container(
width: double.infinity,
padding: EdgeInsets.all(16),
color: Colors.grey[100],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${AppLocalizations.of(context).requiredPhotos}: ${widget.task.minPhotoCount}-${widget.task.maxPhotoCount}',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Text(
'${AppLocalizations.of(context).photosTaken}: ${_capturedPhotos.length}',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
),
// Camera preview, photo gallery or empty state
Expanded(
child: _capturedPhotos.isEmpty
? _buildCameraOrEmptyState()
: _buildPhotoGallery(),
),
// Bottom controls
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.3),
spreadRadius: 1,
blurRadius: 5,
offset: Offset(0, -3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
// Camera or file select button
Expanded(
child: ElevatedButton.icon(
onPressed: _canTakeMore && _isCameraSupportedOnThisPlatform
? (_useFilePickerMode
? _pickPhotoFromFile
: (_isCameraInitialized ? _capturePhoto : null))
: null,
icon: Icon(_useFilePickerMode ? Icons.photo_library : Icons.camera_alt),
label: Text(
!_isCameraSupportedOnThisPlatform
? AppLocalizations.of(context).cameraNotSupportedOnPlatform
: (!_canTakeMore
? AppLocalizations.of(context).maxPhotosReached
: (_useFilePickerMode
? AppLocalizations.of(context).selectPhoto
: (_isCameraInitialized ? AppLocalizations.of(context).takePhoto : (defaultTargetPlatform == TargetPlatform.macOS ? AppLocalizations.of(context).cameraReadyNoPreview : AppLocalizations.of(context).cameraLoading)))),
),
style: ElevatedButton.styleFrom(
backgroundColor: _canTakeMore && (_useFilePickerMode || _isCameraInitialized) ? Colors.blue : Colors.grey,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12),
),
),
),
if (_capturedPhotos.isNotEmpty) ...[
SizedBox(width: 16),
// Delete current photo button
ElevatedButton.icon(
onPressed: () => _deletePhoto(_currentPhotoIndex),
icon: Icon(Icons.delete),
label: Text(AppLocalizations.of(context).delete),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
),
),
],
],
),
SizedBox(height: 12),
// Bottom 'Fertig' button placed under the row
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _canComplete
? () {
widget.onPhotosCompleted(_capturedPhotos);
Navigator.of(context).pop();
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: _canComplete ? Colors.green : Colors.grey,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 14),
),
child: Text(AppLocalizations.of(context).finish, style: const TextStyle(fontWeight: FontWeight.bold)),
),
),
],
),
),
],
),
);
}
Widget _buildCameraOrEmptyState() {
// If platform not supported, show informative message
if (!_isCameraSupportedOnThisPlatform) {
return Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.desktop_windows, size: 80, color: Colors.grey[400]),
SizedBox(height: 16),
Text(
AppLocalizations.of(context).cameraNotAvailable,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[700]),
),
SizedBox(height: 8),
Text(
AppLocalizations.of(context).cameraNotSupportedMessage,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
),
);
}
// macOS fallback: explain file selection
if (_useFilePickerMode) {
return Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.photo_library, size: 80, color: Colors.grey[400]),
SizedBox(height: 16),
Text(
AppLocalizations.of(context).addPhotos,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[700]),
),
SizedBox(height: 8),
Text(
AppLocalizations.of(context).addPhotosInstruction,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
),
);
}
// Show camera preview if available on any platform
if (_isCameraInitialized && _cameraController != null) {
return Container(
margin: EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.3),
spreadRadius: 2,
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CameraPreview(_cameraController!),
),
);
}
// When camera is not available, show empty state
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.camera_alt,
size: 80,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
AppLocalizations.of(context).cameraInitializing,
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
Text(
AppLocalizations.of(context).cameraLoadingMessage,
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildPhotoGallery() {
return Column(
children: [
// Photo counter (if more than one photo)
if (_capturedPhotos.length > 1)
Container(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text(
'${_currentPhotoIndex + 1} ${AppLocalizations.of(context).photoOf} ${_capturedPhotos.length}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.grey[700],
),
),
),
// Photo viewer with swipe gestures and navigation arrows
Expanded(
child: Stack(
children: [
PageView.builder(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentPhotoIndex = index;
});
},
itemCount: _capturedPhotos.length,
itemBuilder: (context, index) {
return Container(
margin: EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.3),
spreadRadius: 2,
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.memory(
_capturedPhotos[index],
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[300],
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 50, color: Colors.grey[600]),
SizedBox(height: 8),
Text(AppLocalizations.of(context).photoError),
],
),
),
);
},
),
),
);
},
),
if (_capturedPhotos.length > 1 && _isDesktopPlatform)
Positioned(
left: 8,
top: 0,
bottom: 0,
child: Center(
child: IconButton(
onPressed: _currentPhotoIndex > 0
? _goToPreviousPhoto
: null,
icon: Icon(Icons.chevron_left, size: 36),
style: IconButton.styleFrom(
backgroundColor: Colors.white.withValues(alpha: 0.7),
),
),
),
),
if (_capturedPhotos.length > 1 && _isDesktopPlatform)
Positioned(
right: 8,
top: 0,
bottom: 0,
child: Center(
child: IconButton(
onPressed: _currentPhotoIndex < _capturedPhotos.length - 1
? _goToNextPhoto
: null,
icon: Icon(Icons.chevron_right, size: 36),
style: IconButton.styleFrom(
backgroundColor: Colors.white.withValues(alpha: 0.7),
),
),
),
),
],
),
),
// Photo indicators (if more than one photo)
if (_capturedPhotos.length > 1)
Container(
padding: EdgeInsets.symmetric(vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _capturedPhotos.asMap().entries.map((entry) {
return Container(
width: 8,
height: 8,
margin: EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _currentPhotoIndex == entry.key
? Colors.blue
: Colors.grey[400],
),
);
}).toList(),
),
),
],
);
}
}

View File

@@ -0,0 +1,260 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:signature/signature.dart';
import '../l10n/app_localizations.dart';
import '../models/tasks/signature_task.dart';
import '../widgets/offline_banner.dart';
class SignatureCaptureScreen extends StatefulWidget {
final SignatureTask task;
final void Function(String svg) onSignatureCompleted;
const SignatureCaptureScreen({
super.key,
required this.task,
required this.onSignatureCompleted,
});
@override
State<SignatureCaptureScreen> createState() => _SignatureCaptureScreenState();
}
class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
late final SignatureController _controller;
bool _hasSignature = false;
bool _isMobilePlatform = false;
@override
void initState() {
super.initState();
_controller = SignatureController(
penStrokeWidth: 3,
penColor: Colors.black,
exportBackgroundColor: Colors.white,
);
// Listen to signature controller changes
_controller.addListener(_onSignatureChanged);
_detectPlatformAndSetOrientation();
}
void _onSignatureChanged() {
final bool hasPoints = _controller.points.isNotEmpty;
if (hasPoints != _hasSignature) {
setState(() {
_hasSignature = hasPoints;
});
}
}
void _detectPlatformAndSetOrientation() {
// Check if we're on a mobile platform
if (!kIsWeb) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
_isMobilePlatform = true;
// Rotate screen 90 degrees to the right (landscape left)
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
]);
break;
default:
_isMobilePlatform = false;
}
}
}
void _restoreOrientation() {
// Restore original orientation when leaving the screen
if (_isMobilePlatform) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
}
@override
void dispose() {
_controller.removeListener(_onSignatureChanged);
_controller.dispose();
_restoreOrientation();
super.dispose();
}
String _buildSvgFromPoints(List<Point?> points, {double strokeWidth = 3.0, String strokeColor = '#000000'}) {
// Convert collected signature points (with null separators for stroke breaks) into an SVG string
// Determine bounds
double? minX, minY, maxX, maxY;
for (final p in points) {
if (p == null) continue;
final x = p.offset.dx;
final y = p.offset.dy;
if (minX == null || x < minX) minX = x;
if (minY == null || y < minY) minY = y;
if (maxX == null || x > maxX) maxX = x;
if (maxY == null || y > maxY) maxY = y;
}
// Fallback bounds if empty or degenerate
if (minX == null || minY == null || maxX == null || maxY == null) {
minX = 0;
minY = 0;
maxX = 1;
maxY = 1;
}
double width = (maxX - minX);
double height = (maxY - minY);
if (width <= 0) width = 1;
if (height <= 0) height = 1;
final StringBuffer d = StringBuffer();
bool newStroke = true;
for (final p in points) {
if (p == null) {
newStroke = true;
continue;
}
final x = (p.offset.dx - minX);
final y = (p.offset.dy - minY);
if (newStroke) {
d.write('M${x.toStringAsFixed(2)} ${y.toStringAsFixed(2)} ');
newStroke = false;
} else {
d.write('L${x.toStringAsFixed(2)} ${y.toStringAsFixed(2)} ');
}
}
final String svg = '<svg xmlns="http://www.w3.org/2000/svg" width="${width.toStringAsFixed(2)}" height="${height.toStringAsFixed(2)}" viewBox="0 0 ${width.toStringAsFixed(2)} ${height.toStringAsFixed(2)}"><path d="${d.toString().trim()}" fill="none" stroke="$strokeColor" stroke-width="$strokeWidth" stroke-linecap="round" stroke-linejoin="round"/></svg>';
return svg;
}
Future<void> _finish() async {
try {
// Ensure there is at least one non-null point in the signature
final hasAnyPoint = _controller.points.isNotEmpty;
if (!hasAnyPoint) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).signatureRequired)),
);
return;
}
// Build SVG from the captured signature points
final String svg = _buildSvgFromPoints(_controller.points);
// Close this screen first to show the updated TaskView quickly
if (!mounted) return;
_restoreOrientation();
Navigator.of(context).pop();
// Then notify the caller (SVG only)
widget.onSignatureCompleted(svg);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${AppLocalizations.of(context).signatureError}: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).signatureCapture),
backgroundColor: Colors.deepPurple[100],
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
_restoreOrientation();
Navigator.of(context).pop();
},
),
actions: [
IconButton(
tooltip: AppLocalizations.of(context).delete,
onPressed: () {
_controller.clear();
// The listener will automatically update _hasSignature when points are cleared
},
icon: const Icon(Icons.delete_outline),
),
],
),
body: Column(
children: [
OfflineBanner(),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context).signatureInstruction,
style: TextStyle(color: Colors.grey[700]),
),
const SizedBox(height: 12),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[400]!),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Signature(
controller: _controller,
backgroundColor: Colors.white,
),
),
),
),
const SizedBox(height: 16),
Row(
children: [
OutlinedButton.icon(
onPressed: () {
_controller.clear();
// The listener will automatically update _hasSignature when points are cleared
},
icon: const Icon(Icons.refresh),
label: Text(AppLocalizations.of(context).clear),
),
const Spacer(),
SizedBox(
width: 160,
child: ElevatedButton(
onPressed: _hasSignature ? _finish : null,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: Text(AppLocalizations.of(context).finish),
),
),
],
),
],
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,358 @@
import 'package:camera/camera.dart';
import 'package:file_picker/file_picker.dart';
import 'package:file_selector/file_selector.dart' as file_selector;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:votianlt_app/services/developer.dart' as developer;
import '../l10n/app_localizations.dart';
class ChatPhotoDialog extends StatefulWidget {
const ChatPhotoDialog({super.key});
@override
State<ChatPhotoDialog> createState() => _ChatPhotoDialogState();
}
class _ChatPhotoDialogState extends State<ChatPhotoDialog> {
CameraController? _cameraController;
bool _isCameraInitialized = false;
bool _useCamera = false;
bool _useFilePicker = false;
bool _isBusy = false;
Uint8List? _previewBytes;
String? _errorMessage;
@override
void initState() {
super.initState();
_detectAndInit();
}
@override
void dispose() {
_cameraController?.dispose();
super.dispose();
}
Future<void> _detectAndInit() async {
if (kIsWeb) {
setState(() {
_useCamera = true;
_useFilePicker = true;
});
await _initializeCamera();
return;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
setState(() {
_useCamera = true;
_useFilePicker = true;
});
await _initializeCamera();
return;
case TargetPlatform.macOS:
case TargetPlatform.windows:
case TargetPlatform.linux:
setState(() {
_useFilePicker = true;
});
return;
default:
setState(() {
_errorMessage = 'Dieses Gerät unterstützt keine Fotoaufnahme.';
});
}
}
Future<void> _initializeCamera() async {
try {
final cameras = await availableCameras();
if (cameras.isEmpty) {
setState(() {
_errorMessage = 'Keine Kamera gefunden.';
});
return;
}
final controller = CameraController(
cameras.first,
ResolutionPreset.medium,
enableAudio: false,
);
await controller.initialize();
if (!mounted) {
await controller.dispose();
return;
}
setState(() {
_cameraController = controller;
_isCameraInitialized = true;
});
} catch (e, stackTrace) {
developer.log(
'Fehler beim Initialisieren der Kamera: $e',
name: 'ChatPhotoDialog',
);
developer.log('StackTrace: $stackTrace', name: 'ChatPhotoDialog');
if (!mounted) {
return;
}
setState(() {
_errorMessage = 'Kamera konnte nicht gestartet werden.';
});
}
}
Future<void> _capturePhoto() async {
if (_cameraController == null || !_cameraController!.value.isInitialized) {
setState(() {
_errorMessage = 'Kamera ist nicht bereit.';
});
return;
}
try {
setState(() {
_isBusy = true;
_errorMessage = null;
});
final XFile photo = await _cameraController!.takePicture();
final Uint8List bytes = await photo.readAsBytes();
if (!mounted) {
return;
}
setState(() {
_previewBytes = bytes;
});
} catch (e, stackTrace) {
developer.log(
'Fehler beim Aufnehmen des Fotos: $e',
name: 'ChatPhotoDialog',
);
developer.log('StackTrace: $stackTrace', name: 'ChatPhotoDialog');
if (!mounted) {
return;
}
setState(() {
_errorMessage = 'Foto konnte nicht aufgenommen werden.';
});
} finally {
if (mounted) {
setState(() {
_isBusy = false;
});
}
}
}
Future<void> _pickPhotoFromFile() async {
try {
setState(() {
_isBusy = true;
_errorMessage = null;
});
if (kIsWeb ||
defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.windows ||
defaultTargetPlatform == TargetPlatform.linux) {
final group = file_selector.XTypeGroup(
label: 'images',
extensions: ['jpg', 'jpeg', 'png', 'heic', 'bmp', 'gif', 'webp'],
);
final file = await file_selector.openFile(acceptedTypeGroups: [group]);
if (file == null) {
return;
}
final bytes = await file.readAsBytes();
if (!mounted) {
return;
}
setState(() {
_previewBytes = bytes;
});
return;
}
final result = await FilePicker.platform.pickFiles(
allowMultiple: false,
type: FileType.image,
withData: true,
);
if (result == null || result.files.isEmpty) {
return;
}
final picked = result.files.first;
final bytes = picked.bytes;
if (bytes == null) {
setState(() {
_errorMessage = 'Die ausgewählte Datei konnte nicht gelesen werden.';
});
return;
}
if (!mounted) {
return;
}
setState(() {
_previewBytes = bytes;
});
} catch (e, stackTrace) {
developer.log(
'Fehler beim Auswählen eines Fotos: $e',
name: 'ChatPhotoDialog',
);
developer.log('StackTrace: $stackTrace', name: 'ChatPhotoDialog');
if (!mounted) {
return;
}
setState(() {
_errorMessage = 'Foto konnte nicht geladen werden.';
});
} finally {
if (mounted) {
setState(() {
_isBusy = false;
});
}
}
}
void _resetPreview() {
setState(() {
_previewBytes = null;
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).takePhoto),
content: SizedBox(width: 320, child: _buildDialogBody()),
actions: [
TextButton(
onPressed: _isBusy ? null : () => Navigator.of(context).pop(),
child: Text(AppLocalizations.of(context).cancel),
),
TextButton(
onPressed:
_previewBytes != null && !_isBusy
? () => Navigator.of(context).pop(_previewBytes)
: null,
child: Text(AppLocalizations.of(context).send),
),
],
);
}
Widget _buildDialogBody() {
if (_previewBytes != null) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AspectRatio(
aspectRatio: 4 / 3,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.memory(_previewBytes!, fit: BoxFit.cover),
),
),
const SizedBox(height: 12),
TextButton.icon(
onPressed: _isBusy ? null : _resetPreview,
icon: const Icon(Icons.refresh),
label: Text(AppLocalizations.of(context).retakePhoto),
),
],
);
}
if (_errorMessage != null) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.warning, color: Colors.orange[700], size: 40),
const SizedBox(height: 12),
Text(_errorMessage!, textAlign: TextAlign.center),
],
);
}
if (_useCamera) {
if (!_isCameraInitialized) {
return const SizedBox(
height: 200,
child: Center(child: CircularProgressIndicator()),
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AspectRatio(
aspectRatio: _cameraController?.value.aspectRatio ?? (3 / 4),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CameraPreview(_cameraController!),
),
),
const SizedBox(height: 12),
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _isBusy ? null : _capturePhoto,
icon: const Icon(Icons.camera_alt),
label: Text(AppLocalizations.of(context).takePhoto),
),
if (_useFilePicker)
OutlinedButton.icon(
onPressed: _isBusy ? null : _pickPhotoFromFile,
icon: const Icon(Icons.photo_library),
label: Text(AppLocalizations.of(context).selectFromLibrary),
),
],
),
],
);
}
if (_useFilePicker) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.photo_camera_back,
color: Colors.deepPurple[400],
size: 48,
),
const SizedBox(height: 12),
const Text(
'Wähle ein Foto von deinem Gerät aus.',
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: _isBusy ? null : _pickPhotoFromFile,
icon: const Icon(Icons.photo_library),
label: Text(AppLocalizations.of(context).selectPhoto),
),
],
);
}
return const SizedBox(
height: 160,
child: Center(child: CircularProgressIndicator()),
);
}
}

View File

@@ -0,0 +1,168 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:votianlt_app/services/developer.dart' as developer;
import 'package:votianlt_app/services/websocket_service.dart';
import 'package:votianlt_app/services/dart_mq.dart';
class OfflineBanner extends StatefulWidget {
const OfflineBanner({super.key});
@override
State<OfflineBanner> createState() => _OfflineBannerState();
}
class _OfflineBannerState extends State<OfflineBanner> {
final StompService _stompService = StompService();
DartMQSubscription? _connSub;
Timer? _countdownTimer;
int _secondsToRetry = 15;
bool _hadConnection = false; // Track if we ever had a successful connection
@override
void initState() {
super.initState();
// Check if we're already connected (e.g., coming back to this screen)
_hadConnection = _stompService.isConnected && _stompService.isAuthenticated;
// Initialize countdown based on current connection state
_onConnectionChange(_stompService.isConnected && _stompService.isAuthenticated);
_connSub = DartMQ().subscribe<bool>(MQTopics.connectionStatus, _onConnectionChange);
}
void _onConnectionChange(bool isConnected) {
if (!mounted) return;
if (isConnected) {
_hadConnection = true; // Mark that we had a successful connection
_stopCountdown();
setState(() {});
} else {
_startCountdown();
}
}
void _startCountdown() {
_stopCountdown();
setState(() {
_secondsToRetry = 15;
});
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
if (!mounted) return;
if (_stompService.isConnected) {
_stopCountdown();
return;
}
// Decrement until 0, then attempt reconnect
if (_secondsToRetry > 1) {
setState(() {
_secondsToRetry = _secondsToRetry - 1;
});
return;
}
// Show 0 for one tick and try to reconnect now
setState(() {
_secondsToRetry = 0;
});
try {
// Only auto-reconnect if we already know the target; discovery remains user-initiated
await _stompService.connect();
} catch (e, stackTrace) {
developer.log('Auto-reconnect attempt failed: $e', name: 'OfflineBanner');
developer.log('Stack trace: $stackTrace', name: 'OfflineBanner');
}
if (!mounted) return;
if (!_stompService.isConnected) {
// Still offline -> reset countdown for next attempt
setState(() {
_secondsToRetry = 15;
});
}
});
}
void _stopCountdown() {
_countdownTimer?.cancel();
_countdownTimer = null;
}
@override
void dispose() {
_stopCountdown();
_connSub?.cancel();
_connSub = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
final isOnline = _stompService.isConnected && _stompService.isAuthenticated;
if (isOnline) return const SizedBox.shrink();
// Different messages for initial connection vs connection lost
final String title;
final String subtitle;
final IconData icon;
final Color? bgColor;
final Color? iconColor;
final Color? titleColor;
final Color? subtitleColor;
if (_hadConnection) {
// Connection was lost
title = 'Offline Verbindung verloren';
subtitle = 'Verbindung wird wiederhergestellt.';
icon = Icons.wifi_off;
bgColor = Colors.red[50];
iconColor = Colors.red[700];
titleColor = Colors.red[900];
subtitleColor = Colors.red[800];
} else {
// Initial connection attempt
title = 'Verbinde mit Server...';
subtitle = 'Bitte warten.';
icon = Icons.sync;
bgColor = Colors.orange[50];
iconColor = Colors.orange[700];
titleColor = Colors.orange[900];
subtitleColor = Colors.orange[800];
}
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
color: bgColor,
child: Row(
children: [
Icon(icon, color: iconColor),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: TextStyle(
color: titleColor,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
color: subtitleColor,
fontSize: 12,
),
),
],
),
),
],
),
);
}
}