refactor: Projektstruktur in app/ und backend/ aufgeteilt
This commit is contained in:
173
app/lib/app_state.dart
Normal file
173
app/lib/app_state.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
437
app/lib/cargo_items_view.dart
Normal file
437
app/lib/cargo_items_view.dart
Normal 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],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
692
app/lib/chat_details_view.dart
Normal file
692
app/lib/chat_details_view.dart
Normal 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
186
app/lib/chats_view.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
32
app/lib/config/translation_config.dart
Normal file
32
app/lib/config/translation_config.dart
Normal 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';
|
||||
}
|
||||
48
app/lib/entities/chat_message_entity.dart
Normal file
48
app/lib/entities/chat_message_entity.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
|
||||
26
app/lib/entities/job_entity.dart
Normal file
26
app/lib/entities/job_entity.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
|
||||
23
app/lib/entities/photo_entity.dart
Normal file
23
app/lib/entities/photo_entity.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
27
app/lib/entities/queued_message_entity.dart
Normal file
27
app/lib/entities/queued_message_entity.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
30
app/lib/entities/task_status_entity.dart
Normal file
30
app/lib/entities/task_status_entity.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
|
||||
26
app/lib/entities/user_data_entity.dart
Normal file
26
app/lib/entities/user_data_entity.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
|
||||
23
app/lib/jobs_route_mixin.dart
Normal file
23
app/lib/jobs_route_mixin.dart
Normal 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
1866
app/lib/jobs_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
267
app/lib/l10n/app_localizations.dart
Normal file
267
app/lib/l10n/app_localizations.dart
Normal 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;
|
||||
}
|
||||
551
app/lib/l10n/app_localizations_de.dart
Normal file
551
app/lib/l10n/app_localizations_de.dart
Normal 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';
|
||||
}
|
||||
551
app/lib/l10n/app_localizations_en.dart
Normal file
551
app/lib/l10n/app_localizations_en.dart
Normal 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';
|
||||
}
|
||||
385
app/lib/l10n/app_localizations_es.dart
Normal file
385
app/lib/l10n/app_localizations_es.dart
Normal 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';
|
||||
}
|
||||
385
app/lib/l10n/app_localizations_et.dart
Normal file
385
app/lib/l10n/app_localizations_et.dart
Normal 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';
|
||||
}
|
||||
385
app/lib/l10n/app_localizations_fr.dart
Normal file
385
app/lib/l10n/app_localizations_fr.dart
Normal 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';
|
||||
}
|
||||
385
app/lib/l10n/app_localizations_lt.dart
Normal file
385
app/lib/l10n/app_localizations_lt.dart
Normal 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 => 'Iš';
|
||||
@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 => 'iš';
|
||||
|
||||
// ==================== 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';
|
||||
}
|
||||
385
app/lib/l10n/app_localizations_lv.dart
Normal file
385
app/lib/l10n/app_localizations_lv.dart
Normal 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';
|
||||
}
|
||||
385
app/lib/l10n/app_localizations_pl.dart
Normal file
385
app/lib/l10n/app_localizations_pl.dart
Normal 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';
|
||||
}
|
||||
385
app/lib/l10n/app_localizations_ru.dart
Normal file
385
app/lib/l10n/app_localizations_ru.dart
Normal 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 => 'Срочный';
|
||||
}
|
||||
385
app/lib/l10n/app_localizations_tr.dart
Normal file
385
app/lib/l10n/app_localizations_tr.dart
Normal 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 => 'Açıklama';
|
||||
@override
|
||||
String get cargo => 'Yük';
|
||||
@override
|
||||
String get quantity => 'Miktar';
|
||||
@override
|
||||
String get weight => 'Ağı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 => 'Açı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
383
app/lib/login_view.dart
Normal 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
122
app/lib/main.dart
Normal 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.
|
||||
);
|
||||
}
|
||||
}
|
||||
85
app/lib/models/acknowledgment_message.dart
Normal file
85
app/lib/models/acknowledgment_message.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
99
app/lib/models/cargo_item.dart
Normal file
99
app/lib/models/cargo_item.dart
Normal 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
162
app/lib/models/chat.dart
Normal 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;
|
||||
}
|
||||
211
app/lib/models/chat_message.dart
Normal file
211
app/lib/models/chat_message.dart
Normal 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;
|
||||
}
|
||||
136
app/lib/models/delivery_station.dart
Normal file
136
app/lib/models/delivery_station.dart
Normal 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
542
app/lib/models/job.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
90
app/lib/models/message_envelope.dart
Normal file
90
app/lib/models/message_envelope.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
|
||||
51
app/lib/models/queued_message.dart
Normal file
51
app/lib/models/queued_message.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
20
app/lib/models/remark_translation.dart
Normal file
20
app/lib/models/remark_translation.dart
Normal 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
198
app/lib/models/task.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
99
app/lib/models/tasks/barcode_task.dart
Normal file
99
app/lib/models/tasks/barcode_task.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
99
app/lib/models/tasks/comment_task.dart
Normal file
99
app/lib/models/tasks/comment_task.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
95
app/lib/models/tasks/confirmation_task.dart
Normal file
95
app/lib/models/tasks/confirmation_task.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
81
app/lib/models/tasks/generic_task.dart
Normal file
81
app/lib/models/tasks/generic_task.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
99
app/lib/models/tasks/photo_task.dart
Normal file
99
app/lib/models/tasks/photo_task.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
82
app/lib/models/tasks/signature_task.dart
Normal file
82
app/lib/models/tasks/signature_task.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
96
app/lib/models/tasks/todolist_task.dart
Normal file
96
app/lib/models/tasks/todolist_task.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
4
app/lib/navigation_observer.dart
Normal file
4
app/lib/navigation_observer.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>();
|
||||
|
||||
281
app/lib/objectbox-model.json
Normal file
281
app/lib/objectbox-model.json
Normal 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
943
app/lib/objectbox.g.dart
Normal 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
141
app/lib/routing_view.dart
Normal 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)])),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
143
app/lib/services/ack_tracker.dart
Normal file
143
app/lib/services/ack_tracker.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
500
app/lib/services/chat_service.dart
Normal file
500
app/lib/services/chat_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
100
app/lib/services/dart_mq.dart
Normal file
100
app/lib/services/dart_mq.dart
Normal 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
|
||||
}
|
||||
1529
app/lib/services/database_service.dart
Normal file
1529
app/lib/services/database_service.dart
Normal file
File diff suppressed because it is too large
Load Diff
47
app/lib/services/developer.dart
Normal file
47
app/lib/services/developer.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
184
app/lib/services/location_service.dart
Normal file
184
app/lib/services/location_service.dart
Normal 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;
|
||||
}
|
||||
114
app/lib/services/message_handler.dart
Normal file
114
app/lib/services/message_handler.dart
Normal 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();
|
||||
}
|
||||
123
app/lib/services/notification_service.dart
Normal file
123
app/lib/services/notification_service.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
535
app/lib/services/translation_service.dart
Normal file
535
app/lib/services/translation_service.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
1064
app/lib/services/websocket_service.dart
Normal file
1064
app/lib/services/websocket_service.dart
Normal file
File diff suppressed because it is too large
Load Diff
225
app/lib/settings_view.dart
Normal file
225
app/lib/settings_view.dart
Normal 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
796
app/lib/task_view.dart
Normal 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}';
|
||||
}
|
||||
}
|
||||
227
app/lib/tasks/barcode_capture_screen.dart
Normal file
227
app/lib/tasks/barcode_capture_screen.dart
Normal 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))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
679
app/lib/tasks/photo_capture_screen.dart
Normal file
679
app/lib/tasks/photo_capture_screen.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
260
app/lib/tasks/signature_capture_screen.dart
Normal file
260
app/lib/tasks/signature_capture_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
358
app/lib/widgets/chat_photo_dialog.dart
Normal file
358
app/lib/widgets/chat_photo_dialog.dart
Normal 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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
168
app/lib/widgets/offline_banner.dart
Normal file
168
app/lib/widgets/offline_banner.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user