Compare commits
4 Commits
bba5733783
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 069b829294 | |||
| 704d1e7378 | |||
| 6e8bedd9b4 | |||
| 1ac755bcbd |
175
app/lib/app_theme.dart
Normal file
175
app/lib/app_theme.dart
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppColors {
|
||||||
|
static const Color primary = Color(0xFF2563EB);
|
||||||
|
static const Color primaryStrong = Color(0xFF1D4ED8);
|
||||||
|
static const Color primarySoft = Color(0xFFE8F0FF);
|
||||||
|
static const Color secondary = Color(0xFF0F4C5C);
|
||||||
|
static const Color secondarySoft = Color(0xFFDDEEF2);
|
||||||
|
static const Color success = Color(0xFF059669);
|
||||||
|
static const Color successSoft = Color(0xFFE7F6F1);
|
||||||
|
static const Color warning = Color(0xFFD97706);
|
||||||
|
static const Color warningSoft = Color(0xFFFFF4E5);
|
||||||
|
static const Color danger = Color(0xFFDC2626);
|
||||||
|
static const Color dangerSoft = Color(0xFFFDECEC);
|
||||||
|
static const Color surface = Color(0xFFFFFFFF);
|
||||||
|
static const Color surfaceMuted = Color(0xFFF7FAFF);
|
||||||
|
static const Color scaffold = Color(0xFFF5F7FB);
|
||||||
|
static const Color scaffoldAccent = Color(0xFFEEF4FF);
|
||||||
|
static const Color border = Color(0xFFD6DDE7);
|
||||||
|
static const Color borderStrong = Color(0xFFC6D0DD);
|
||||||
|
static const Color text = Color(0xFF1E293B);
|
||||||
|
static const Color textStrong = Color(0xFF0F172A);
|
||||||
|
static const Color textMuted = Color(0xFF64748B);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppGradients {
|
||||||
|
static const LinearGradient shellBackground = LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
AppColors.scaffoldAccent,
|
||||||
|
AppColors.surfaceMuted,
|
||||||
|
AppColors.scaffold,
|
||||||
|
],
|
||||||
|
stops: [0, 0.45, 1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeData buildAppTheme() {
|
||||||
|
final colorScheme = ColorScheme.fromSeed(
|
||||||
|
seedColor: AppColors.primary,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
).copyWith(
|
||||||
|
primary: AppColors.primary,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
primaryContainer: AppColors.primarySoft,
|
||||||
|
onPrimaryContainer: AppColors.primaryStrong,
|
||||||
|
secondary: AppColors.secondary,
|
||||||
|
onSecondary: Colors.white,
|
||||||
|
secondaryContainer: AppColors.secondarySoft,
|
||||||
|
onSecondaryContainer: AppColors.secondary,
|
||||||
|
tertiary: AppColors.success,
|
||||||
|
onTertiary: Colors.white,
|
||||||
|
tertiaryContainer: AppColors.successSoft,
|
||||||
|
onTertiaryContainer: AppColors.success,
|
||||||
|
surface: AppColors.surface,
|
||||||
|
onSurface: AppColors.textStrong,
|
||||||
|
onSurfaceVariant: AppColors.textMuted,
|
||||||
|
outline: AppColors.border,
|
||||||
|
surfaceTint: Colors.transparent,
|
||||||
|
error: AppColors.danger,
|
||||||
|
onError: Colors.white,
|
||||||
|
);
|
||||||
|
final baseTheme = ThemeData(useMaterial3: true, colorScheme: colorScheme);
|
||||||
|
const radius = Radius.circular(14);
|
||||||
|
final border = OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(radius),
|
||||||
|
borderSide: const BorderSide(color: AppColors.border),
|
||||||
|
);
|
||||||
|
|
||||||
|
return baseTheme.copyWith(
|
||||||
|
scaffoldBackgroundColor: AppColors.scaffold,
|
||||||
|
canvasColor: AppColors.scaffold,
|
||||||
|
textTheme: baseTheme.textTheme
|
||||||
|
.apply(bodyColor: AppColors.text, displayColor: AppColors.textStrong)
|
||||||
|
.copyWith(
|
||||||
|
headlineMedium: baseTheme.textTheme.headlineMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.textStrong,
|
||||||
|
),
|
||||||
|
titleLarge: baseTheme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.textStrong,
|
||||||
|
),
|
||||||
|
bodyLarge: baseTheme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: AppColors.text,
|
||||||
|
),
|
||||||
|
bodyMedium: baseTheme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: AppColors.text,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: AppColors.primarySoft,
|
||||||
|
foregroundColor: AppColors.primaryStrong,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
cardTheme: const CardThemeData(
|
||||||
|
color: AppColors.surface,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(18)),
|
||||||
|
side: BorderSide(color: AppColors.border),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dividerTheme: const DividerThemeData(
|
||||||
|
color: AppColors.border,
|
||||||
|
space: 1,
|
||||||
|
thickness: 1,
|
||||||
|
),
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
fillColor: AppColors.surface,
|
||||||
|
labelStyle: const TextStyle(color: AppColors.textMuted),
|
||||||
|
hintStyle: const TextStyle(color: AppColors.textMuted),
|
||||||
|
prefixIconColor: AppColors.textMuted,
|
||||||
|
suffixIconColor: AppColors.textMuted,
|
||||||
|
border: border,
|
||||||
|
enabledBorder: border,
|
||||||
|
focusedBorder: border.copyWith(
|
||||||
|
borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
|
||||||
|
),
|
||||||
|
errorBorder: border.copyWith(
|
||||||
|
borderSide: const BorderSide(color: AppColors.danger),
|
||||||
|
),
|
||||||
|
focusedErrorBorder: border.copyWith(
|
||||||
|
borderSide: const BorderSide(color: AppColors.danger, width: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.primary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
disabledBackgroundColor: AppColors.border,
|
||||||
|
disabledForegroundColor: AppColors.textMuted,
|
||||||
|
elevation: 0,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
|
textStyle: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: AppColors.primaryStrong,
|
||||||
|
side: const BorderSide(color: AppColors.border),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textButtonTheme: TextButtonThemeData(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: AppColors.primaryStrong,
|
||||||
|
textStyle: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
snackBarTheme: SnackBarThemeData(
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: AppColors.textStrong,
|
||||||
|
contentTextStyle: baseTheme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
|
),
|
||||||
|
badgeTheme: const BadgeThemeData(
|
||||||
|
backgroundColor: AppColors.primary,
|
||||||
|
textColor: Colors.white,
|
||||||
|
),
|
||||||
|
progressIndicatorTheme: const ProgressIndicatorThemeData(
|
||||||
|
color: AppColors.primary,
|
||||||
|
),
|
||||||
|
listTileTheme: const ListTileThemeData(iconColor: AppColors.textMuted),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'app_theme.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'l10n/localization_helpers.dart';
|
import 'l10n/localization_helpers.dart';
|
||||||
import 'models/delivery_station.dart';
|
import 'models/delivery_station.dart';
|
||||||
@@ -20,7 +21,7 @@ Color? deliveryStationCardBackgroundColor(
|
|||||||
final isCompleted = station.tasks.every(
|
final isCompleted = station.tasks.every(
|
||||||
(task) => taskStatuses[task.id] ?? task.completed,
|
(task) => taskStatuses[task.id] ?? task.completed,
|
||||||
);
|
);
|
||||||
return isCompleted ? Colors.green[50] : null;
|
return isCompleted ? AppColors.successSoft : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CargoItemsView extends StatefulWidget {
|
class CargoItemsView extends StatefulWidget {
|
||||||
@@ -57,7 +58,6 @@ class _CargoItemsViewState extends State<CargoItemsView> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(widget.job.jobNumber),
|
title: Text(widget.job.jobNumber),
|
||||||
backgroundColor: Colors.deepPurple[100],
|
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -139,7 +139,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.arrow_downward,
|
Icons.arrow_downward,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Colors.blue[600],
|
color: AppColors.primary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
@@ -163,7 +163,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.local_shipping_outlined,
|
Icons.local_shipping_outlined,
|
||||||
size: 24,
|
size: 24,
|
||||||
color: Colors.deepPurple[600],
|
color: AppColors.primary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
@@ -313,7 +313,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
|
|||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.deepPurple[100],
|
color: AppColors.primarySoft,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -321,7 +321,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.deepPurple[700],
|
color: AppColors.primaryStrong,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -359,7 +359,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
|
|||||||
Icons.location_on_outlined,
|
Icons.location_on_outlined,
|
||||||
AppLocalizations.of(context).location,
|
AppLocalizations.of(context).location,
|
||||||
addressLines.join('\n'),
|
addressLines.join('\n'),
|
||||||
Colors.blue,
|
AppColors.primary,
|
||||||
),
|
),
|
||||||
if (station.phone.trim().isNotEmpty) ...[
|
if (station.phone.trim().isNotEmpty) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -367,7 +367,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
|
|||||||
Icons.phone_outlined,
|
Icons.phone_outlined,
|
||||||
l10n.phone,
|
l10n.phone,
|
||||||
station.phone,
|
station.phone,
|
||||||
Colors.green,
|
AppColors.success,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (station.deliveryDate.trim().isNotEmpty ||
|
if (station.deliveryDate.trim().isNotEmpty ||
|
||||||
@@ -380,7 +380,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
|
|||||||
station.deliveryDate,
|
station.deliveryDate,
|
||||||
station.deliveryTime,
|
station.deliveryTime,
|
||||||
].where((part) => part.trim().isNotEmpty).join(' '),
|
].where((part) => part.trim().isNotEmpty).join(' '),
|
||||||
Colors.orange,
|
AppColors.warning,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -388,7 +388,7 @@ class _CargoItemsViewState extends State<CargoItemsView> {
|
|||||||
Icons.task_alt,
|
Icons.task_alt,
|
||||||
AppLocalizations.of(context).tasks,
|
AppLocalizations.of(context).tasks,
|
||||||
'${station.tasks.length}',
|
'${station.tasks.length}',
|
||||||
Colors.deepPurple,
|
AppColors.primaryStrong,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
|
import 'app_theme.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'l10n/localization_helpers.dart';
|
import 'l10n/localization_helpers.dart';
|
||||||
import 'app_state.dart';
|
import 'app_state.dart';
|
||||||
@@ -257,13 +258,12 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
|
|||||||
'Job-Nr: ${_activeChat.jobNumber}',
|
'Job-Nr: ${_activeChat.jobNumber}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey[600],
|
color: AppColors.textMuted,
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.deepPurple[100],
|
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(isJobChat ? Icons.work : Icons.support_agent),
|
icon: Icon(isJobChat ? Icons.work : Icons.support_agent),
|
||||||
@@ -281,7 +281,7 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
|
|||||||
// Messages list
|
// Messages list
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(color: Colors.grey[50]),
|
decoration: const BoxDecoration(color: AppColors.surfaceMuted),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 96),
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 96),
|
||||||
@@ -325,7 +325,7 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
|
|||||||
vertical: isImage ? 6 : 8,
|
vertical: isImage ? 6 : 8,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isOwn ? Colors.deepPurple[100] : Colors.white,
|
color: isOwn ? AppColors.primarySoft : AppColors.surface,
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: const Radius.circular(12),
|
topLeft: const Radius.circular(12),
|
||||||
topRight: const Radius.circular(12),
|
topRight: const Radius.circular(12),
|
||||||
@@ -351,7 +351,10 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_formatMessageTime(message.createdAt),
|
_formatMessageTime(message.createdAt),
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (isOwn) ...[
|
if (isOwn) ...[
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
@@ -362,10 +365,10 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
|
|||||||
size: 14,
|
size: 14,
|
||||||
color:
|
color:
|
||||||
message.pendingSync
|
message.pendingSync
|
||||||
? Colors.orange[700]
|
? AppColors.warning
|
||||||
: (message.read
|
: (message.read
|
||||||
? Colors.deepPurple[400]
|
? AppColors.primary
|
||||||
: Colors.grey[600]),
|
: AppColors.textMuted),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -384,7 +387,7 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
|
|||||||
if (!isImage) {
|
if (!isImage) {
|
||||||
return Text(
|
return Text(
|
||||||
message.content,
|
message.content,
|
||||||
style: TextStyle(fontSize: 15, color: Colors.grey[800]),
|
style: const TextStyle(fontSize: 15, color: AppColors.textStrong),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,8 +458,8 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: AppColors.surface,
|
||||||
border: Border(top: BorderSide(color: Colors.grey[300]!)),
|
border: const Border(top: BorderSide(color: AppColors.border)),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -466,12 +469,12 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[200],
|
color: AppColors.surfaceMuted,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.attach_file,
|
Icons.attach_file,
|
||||||
color: Colors.black87,
|
color: AppColors.text,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -480,7 +483,7 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[100],
|
color: AppColors.surfaceMuted,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
@@ -508,7 +511,7 @@ class _ChatDetailsViewState extends State<ChatDetailsView> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.deepPurple,
|
color: AppColors.primary,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.send, color: Colors.white, size: 20),
|
child: const Icon(Icons.send, color: Colors.white, size: 20),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'app_theme.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'l10n/localization_helpers.dart';
|
import 'l10n/localization_helpers.dart';
|
||||||
import 'models/chat.dart';
|
import 'models/chat.dart';
|
||||||
@@ -52,10 +53,7 @@ class _ChatsViewState extends State<ChatsView> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text(AppLocalizations.of(context).chats)),
|
||||||
title: Text(AppLocalizations.of(context).chats),
|
|
||||||
backgroundColor: Colors.deepPurple[100],
|
|
||||||
),
|
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [const OfflineBanner(), Expanded(child: _buildBody())],
|
children: [const OfflineBanner(), Expanded(child: _buildBody())],
|
||||||
),
|
),
|
||||||
@@ -72,11 +70,15 @@ class _ChatsViewState extends State<ChatsView> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.chat_outlined, size: 64, color: Colors.grey),
|
const Icon(
|
||||||
|
Icons.chat_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).noChatsAvailable,
|
AppLocalizations.of(context).noChatsAvailable,
|
||||||
style: const TextStyle(fontSize: 16, color: Colors.grey),
|
style: const TextStyle(fontSize: 16, color: AppColors.textMuted),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -108,10 +110,11 @@ class _ChatsViewState extends State<ChatsView> {
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: isJobChat ? Colors.blue[100] : Colors.green[100],
|
backgroundColor:
|
||||||
|
isJobChat ? AppColors.primarySoft : AppColors.secondarySoft,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
isJobChat ? Icons.work : Icons.support_agent,
|
isJobChat ? Icons.work : Icons.support_agent,
|
||||||
color: isJobChat ? Colors.blue[700] : Colors.green[700],
|
color: isJobChat ? AppColors.primaryStrong : AppColors.secondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(() {
|
title: Text(() {
|
||||||
@@ -129,7 +132,7 @@ class _ChatsViewState extends State<ChatsView> {
|
|||||||
previewText,
|
previewText,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
style: const TextStyle(fontSize: 14, color: AppColors.textMuted),
|
||||||
),
|
),
|
||||||
trailing: Column(
|
trailing: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
@@ -137,16 +140,17 @@ class _ChatsViewState extends State<ChatsView> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
timeLabel,
|
timeLabel,
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
style: const TextStyle(fontSize: 12, color: AppColors.textMuted),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isJobChat ? Colors.blue[50] : Colors.green[50],
|
color:
|
||||||
|
isJobChat ? AppColors.primarySoft : AppColors.secondarySoft,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isJobChat ? Colors.blue[200]! : Colors.green[200]!,
|
color: isJobChat ? AppColors.primary : AppColors.secondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -154,7 +158,8 @@ class _ChatsViewState extends State<ChatsView> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: isJobChat ? Colors.blue[700] : Colors.green[700],
|
color:
|
||||||
|
isJobChat ? AppColors.primaryStrong : AppColors.secondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'app_state.dart';
|
import 'app_state.dart';
|
||||||
|
import 'app_theme.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'l10n/localization_helpers.dart';
|
import 'l10n/localization_helpers.dart';
|
||||||
import 'services/websocket_service.dart';
|
import 'services/websocket_service.dart';
|
||||||
@@ -98,7 +99,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
if (isConnected && !_wasConnected) {
|
if (isConnected && !_wasConnected) {
|
||||||
_showSnack(
|
_showSnack(
|
||||||
AppLocalizations.of(context).connectionRestored,
|
AppLocalizations.of(context).connectionRestored,
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: AppColors.success,
|
||||||
);
|
);
|
||||||
if (_appState.isLoggedIn) {
|
if (_appState.isLoggedIn) {
|
||||||
_loadJobs();
|
_loadJobs();
|
||||||
@@ -115,7 +116,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
if (_appState.isLoggedIn && !_isLoggingOut) {
|
if (_appState.isLoggedIn && !_isLoggingOut) {
|
||||||
_showSnack(
|
_showSnack(
|
||||||
AppLocalizations.of(context).connectionLost,
|
AppLocalizations.of(context).connectionLost,
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: AppColors.danger,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +147,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
jobNumber != null
|
jobNumber != null
|
||||||
? 'Job $jobNumber ${AppLocalizations.of(context).jobRemoved}'
|
? 'Job $jobNumber ${AppLocalizations.of(context).jobRemoved}'
|
||||||
: AppLocalizations.of(context).jobRemoved;
|
: AppLocalizations.of(context).jobRemoved;
|
||||||
_showSnack(message, backgroundColor: Colors.orange);
|
_showSnack(message, backgroundColor: AppColors.warning);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -177,7 +178,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
jobNumber.isNotEmpty
|
jobNumber.isNotEmpty
|
||||||
? '${AppLocalizations.of(context).newJobReceived}: $jobNumber'
|
? '${AppLocalizations.of(context).newJobReceived}: $jobNumber'
|
||||||
: AppLocalizations.of(context).newJobReceived;
|
: AppLocalizations.of(context).newJobReceived;
|
||||||
_showSnack(message, backgroundColor: Colors.green);
|
_showSnack(message, backgroundColor: AppColors.success);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
developer.log('Error handling job_created event: $e', name: 'JobsView');
|
developer.log('Error handling job_created event: $e', name: 'JobsView');
|
||||||
@@ -204,7 +205,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
});
|
});
|
||||||
_showSnack(
|
_showSnack(
|
||||||
AppLocalizations.of(context).jobsUpdated,
|
AppLocalizations.of(context).jobsUpdated,
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: AppColors.success,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -560,7 +561,6 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
title: Text(AppLocalizations.of(context).availableJobs),
|
title: Text(AppLocalizations.of(context).availableJobs),
|
||||||
backgroundColor: Colors.deepPurple[100],
|
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.logout),
|
icon: const Icon(Icons.logout),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -694,7 +694,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: AppColors.danger,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
child: Text(AppLocalizations.of(context).logout),
|
child: Text(AppLocalizations.of(context).logout),
|
||||||
@@ -766,8 +766,8 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
label: Text(AppLocalizations.of(context).refresh),
|
label: Text(AppLocalizations.of(context).refresh),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.deepPurple[100],
|
backgroundColor: AppColors.primarySoft,
|
||||||
foregroundColor: Colors.deepPurple[700],
|
foregroundColor: AppColors.primaryStrong,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -792,7 +792,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
} else {
|
} else {
|
||||||
_showSnack(
|
_showSnack(
|
||||||
AppLocalizations.of(context).offline,
|
AppLocalizations.of(context).offline,
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: AppColors.danger,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -908,7 +908,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
_showSnack(
|
_showSnack(
|
||||||
AppLocalizations.of(context).jobDeleted,
|
AppLocalizations.of(context).jobDeleted,
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: AppColors.danger,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
@@ -917,7 +917,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
_showSnack(
|
_showSnack(
|
||||||
AppLocalizations.of(context).jobDeleteError,
|
AppLocalizations.of(context).jobDeleteError,
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: AppColors.danger,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -935,19 +935,19 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
Color statusColor;
|
Color statusColor;
|
||||||
switch (job.statusColor) {
|
switch (job.statusColor) {
|
||||||
case 'green':
|
case 'green':
|
||||||
statusColor = Colors.green;
|
statusColor = AppColors.success;
|
||||||
break;
|
break;
|
||||||
case 'blue':
|
case 'blue':
|
||||||
statusColor = Colors.blue;
|
statusColor = AppColors.primary;
|
||||||
break;
|
break;
|
||||||
case 'orange':
|
case 'orange':
|
||||||
statusColor = Colors.orange;
|
statusColor = AppColors.warning;
|
||||||
break;
|
break;
|
||||||
case 'red':
|
case 'red':
|
||||||
statusColor = Colors.red;
|
statusColor = AppColors.danger;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
statusColor = Colors.grey;
|
statusColor = AppColors.textMuted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine card background color based on task completion
|
// Determine card background color based on task completion
|
||||||
@@ -965,9 +965,9 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
if (totalTasks == 0 || completedTasks == 0) {
|
if (totalTasks == 0 || completedTasks == 0) {
|
||||||
cardBg = null; // unchanged (default)
|
cardBg = null; // unchanged (default)
|
||||||
} else if (completedTasks > 0 && completedTasks < totalTasks) {
|
} else if (completedTasks > 0 && completedTasks < totalTasks) {
|
||||||
cardBg = Colors.yellow[50];
|
cardBg = AppColors.warningSoft;
|
||||||
} else if (completedTasks == totalTasks) {
|
} else if (completedTasks == totalTasks) {
|
||||||
cardBg = Colors.green[50];
|
cardBg = AppColors.successSoft;
|
||||||
}
|
}
|
||||||
// Build robust display strings with fallbacks
|
// Build robust display strings with fallbacks
|
||||||
final pickupName = _joinNonEmpty([job.pickupFirstName, job.pickupLastName]);
|
final pickupName = _joinNonEmpty([job.pickupFirstName, job.pickupLastName]);
|
||||||
@@ -1033,7 +1033,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
iconSize: 28,
|
iconSize: 28,
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
icon: const Icon(Icons.delete, color: Colors.red),
|
icon: const Icon(Icons.delete, color: AppColors.danger),
|
||||||
tooltip: AppLocalizations.of(context).deleteJob,
|
tooltip: AppLocalizations.of(context).deleteJob,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (isDeleting) {
|
if (isDeleting) {
|
||||||
@@ -1233,13 +1233,13 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
? 0
|
? 0
|
||||||
: completedTasks / totalTasks,
|
: completedTasks / totalTasks,
|
||||||
minHeight: 8,
|
minHeight: 8,
|
||||||
backgroundColor: Colors.grey[200],
|
backgroundColor: AppColors.border,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
completedTasks >= totalTasks
|
completedTasks >= totalTasks
|
||||||
? Colors.green
|
? AppColors.success
|
||||||
: (completedTasks > 0
|
: (completedTasks > 0
|
||||||
? Colors.amber
|
? AppColors.warning
|
||||||
: Colors.deepPurpleAccent),
|
: AppColors.primary),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1336,7 +1336,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.arrow_downward,
|
Icons.arrow_downward,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Colors.blue[600],
|
color: AppColors.primary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
@@ -1375,7 +1375,7 @@ class _JobsViewState extends State<JobsView> with RouteAware {
|
|||||||
tooltip: 'Route planen',
|
tooltip: 'Route planen',
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.route,
|
Icons.route,
|
||||||
color: Colors.blueAccent,
|
color: AppColors.primary,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_routeActionInProgress) return;
|
if (_routeActionInProgress) return;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get jobs => 'Jobs';
|
String get jobs => 'Jobs';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get availableJobs => 'Verfügbare Jobs';
|
String get availableJobs => 'Auftragsliste';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chats => 'Chats';
|
String get chats => 'Chats';
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get jobs => 'Jobs';
|
String get jobs => 'Jobs';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get availableJobs => 'Available Jobs';
|
String get availableJobs => 'Order List';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chats => 'Chats';
|
String get chats => 'Chats';
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get jobs => 'Trabajos';
|
String get jobs => 'Trabajos';
|
||||||
@override
|
@override
|
||||||
String get availableJobs => 'Trabajos Disponibles';
|
String get availableJobs => 'Lista de pedidos';
|
||||||
@override
|
@override
|
||||||
String get chats => 'Chats';
|
String get chats => 'Chats';
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class AppLocalizationsEt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get jobs => 'Tööd';
|
String get jobs => 'Tööd';
|
||||||
@override
|
@override
|
||||||
String get availableJobs => 'Saadaolevad tööd';
|
String get availableJobs => 'Tellimuste loend';
|
||||||
@override
|
@override
|
||||||
String get chats => 'Vestlused';
|
String get chats => 'Vestlused';
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get jobs => 'Emplois';
|
String get jobs => 'Emplois';
|
||||||
@override
|
@override
|
||||||
String get availableJobs => 'Emplois Disponibles';
|
String get availableJobs => 'Liste des commandes';
|
||||||
@override
|
@override
|
||||||
String get chats => 'Discussions';
|
String get chats => 'Discussions';
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class AppLocalizationsLt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get jobs => 'Darbai';
|
String get jobs => 'Darbai';
|
||||||
@override
|
@override
|
||||||
String get availableJobs => 'Galimi darbai';
|
String get availableJobs => 'Užsakymų sąrašas';
|
||||||
@override
|
@override
|
||||||
String get chats => 'Pokalbiai';
|
String get chats => 'Pokalbiai';
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class AppLocalizationsLv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get jobs => 'Darbi';
|
String get jobs => 'Darbi';
|
||||||
@override
|
@override
|
||||||
String get availableJobs => 'Pieejamie darbi';
|
String get availableJobs => 'Pasūtījumu saraksts';
|
||||||
@override
|
@override
|
||||||
String get chats => 'Tērzēšanas';
|
String get chats => 'Tērzēšanas';
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get jobs => 'Zadania';
|
String get jobs => 'Zadania';
|
||||||
@override
|
@override
|
||||||
String get availableJobs => 'Dostępne Zadania';
|
String get availableJobs => 'Lista zleceń';
|
||||||
@override
|
@override
|
||||||
String get chats => 'Czaty';
|
String get chats => 'Czaty';
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get jobs => 'Задания';
|
String get jobs => 'Задания';
|
||||||
@override
|
@override
|
||||||
String get availableJobs => 'Доступные задания';
|
String get availableJobs => 'Список заказов';
|
||||||
@override
|
@override
|
||||||
String get chats => 'Чаты';
|
String get chats => 'Чаты';
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get jobs => 'İşler';
|
String get jobs => 'İşler';
|
||||||
@override
|
@override
|
||||||
String get availableJobs => 'Mevcut İşler';
|
String get availableJobs => 'Sipariş Listesi';
|
||||||
@override
|
@override
|
||||||
String get chats => 'Sohbetler';
|
String get chats => 'Sohbetler';
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'services/websocket_service.dart';
|
|||||||
import 'services/dart_mq.dart';
|
import 'services/dart_mq.dart';
|
||||||
import 'services/database_service.dart';
|
import 'services/database_service.dart';
|
||||||
import 'app_state.dart';
|
import 'app_state.dart';
|
||||||
|
import 'app_theme.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
|
|
||||||
class LoginView extends StatefulWidget {
|
class LoginView extends StatefulWidget {
|
||||||
@@ -57,7 +58,7 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(AppLocalizations.of(context).loginSuccess),
|
content: Text(AppLocalizations.of(context).loginSuccess),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: AppColors.success,
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(seconds: 1),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -228,7 +229,7 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
content: Text(
|
content: Text(
|
||||||
'${AppLocalizations.of(context).loginFailed}: $errorMessage',
|
'${AppLocalizations.of(context).loginFailed}: $errorMessage',
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: AppColors.danger,
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(seconds: 1),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -292,7 +293,7 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(localizations.connecting),
|
content: Text(localizations.connecting),
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: AppColors.primary,
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(seconds: 1),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -345,7 +346,7 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(localizations.connectionTimeout),
|
content: Text(localizations.connectionTimeout),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: AppColors.danger,
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -364,7 +365,7 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('${localizations.connectionError}: $e'),
|
content: Text('${localizations.connectionError}: $e'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: AppColors.danger,
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(seconds: 1),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -420,7 +421,7 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('${localizations.loginError}: $e'),
|
content: Text('${localizations.loginError}: $e'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: AppColors.danger,
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(seconds: 1),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -440,203 +441,207 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.grey[50],
|
body: DecoratedBox(
|
||||||
body: Column(
|
decoration: const BoxDecoration(gradient: AppGradients.shellBackground),
|
||||||
children: [
|
child: Column(
|
||||||
Expanded(
|
children: [
|
||||||
child: SafeArea(
|
Expanded(
|
||||||
child: Center(
|
child: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: Center(
|
||||||
padding: const EdgeInsets.all(24.0),
|
child: SingleChildScrollView(
|
||||||
child: Form(
|
padding: const EdgeInsets.all(24.0),
|
||||||
key: _formKey,
|
child: Form(
|
||||||
child: Column(
|
key: _formKey,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
// Logo oder App-Name
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.account_circle,
|
Icons.account_circle,
|
||||||
size: 100,
|
size: 100,
|
||||||
color: Colors.deepPurple,
|
color: AppColors.primary,
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
|
|
||||||
Text(
|
|
||||||
l10n.welcomeBack,
|
|
||||||
style: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.headlineMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.grey[800],
|
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
const SizedBox(height: 32),
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
l10n.loginSubtitle,
|
l10n.welcomeBack,
|
||||||
style: Theme.of(context).textTheme.bodyLarge
|
style: Theme.of(
|
||||||
?.copyWith(color: Colors.grey[600]),
|
context,
|
||||||
textAlign: TextAlign.center,
|
).textTheme.headlineMedium?.copyWith(
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
const SizedBox(height: 32),
|
color: AppColors.textStrong,
|
||||||
// E-Mail-Feld
|
|
||||||
TextFormField(
|
|
||||||
controller: _emailController,
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: l10n.emailAddress,
|
|
||||||
hintText: l10n.emailAddressHint,
|
|
||||||
prefixIcon: const Icon(Icons.email_outlined),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
),
|
||||||
filled: true,
|
textAlign: TextAlign.center,
|
||||||
fillColor: Colors.white,
|
|
||||||
),
|
),
|
||||||
validator: (value) {
|
const SizedBox(height: 8),
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return l10n.emailAddressRequired;
|
|
||||||
}
|
|
||||||
if (!RegExp(
|
|
||||||
r'^[A-Za-z0-9_.+-]+@[A-Za-z0-9-]+\.[A-Za-z0-9.-]+$',
|
|
||||||
).hasMatch(value)) {
|
|
||||||
return l10n.emailAddressInvalid;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Passwort-Feld
|
Text(
|
||||||
TextFormField(
|
l10n.loginSubtitle,
|
||||||
controller: _passwordController,
|
style: Theme.of(context).textTheme.bodyLarge
|
||||||
obscureText: !_isPasswordVisible,
|
?.copyWith(color: AppColors.textMuted),
|
||||||
decoration: InputDecoration(
|
textAlign: TextAlign.center,
|
||||||
labelText: l10n.password,
|
),
|
||||||
hintText: l10n.passwordHint,
|
const SizedBox(height: 32),
|
||||||
prefixIcon: const Icon(Icons.lock_outlined),
|
// E-Mail-Feld
|
||||||
suffixIcon: IconButton(
|
TextFormField(
|
||||||
icon: Icon(
|
controller: _emailController,
|
||||||
_isPasswordVisible
|
keyboardType: TextInputType.emailAddress,
|
||||||
? Icons.visibility
|
decoration: InputDecoration(
|
||||||
: Icons.visibility_off,
|
labelText: l10n.emailAddress,
|
||||||
|
hintText: l10n.emailAddressHint,
|
||||||
|
prefixIcon: const Icon(Icons.email_outlined),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
filled: true,
|
||||||
setState(() {
|
fillColor: AppColors.surface,
|
||||||
_isPasswordVisible = !_isPasswordVisible;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
validator: (value) {
|
||||||
borderRadius: BorderRadius.circular(12),
|
if (value == null || value.isEmpty) {
|
||||||
),
|
return l10n.emailAddressRequired;
|
||||||
filled: true,
|
}
|
||||||
fillColor: Colors.white,
|
if (!RegExp(
|
||||||
),
|
r'^[A-Za-z0-9_.+-]+@[A-Za-z0-9-]+\.[A-Za-z0-9.-]+$',
|
||||||
validator: (value) {
|
).hasMatch(value)) {
|
||||||
if (value == null || value.isEmpty) {
|
return l10n.emailAddressInvalid;
|
||||||
return l10n.passwordRequired;
|
}
|
||||||
}
|
return null;
|
||||||
if (value.length < 6) {
|
|
||||||
return l10n.passwordMinLength;
|
|
||||||
}
|
|
||||||
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(l10n.forgotPasswordMessage),
|
|
||||||
duration: const Duration(seconds: 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Text(
|
),
|
||||||
l10n.forgotPassword,
|
const SizedBox(height: 16),
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.deepPurple,
|
// Passwort-Feld
|
||||||
fontWeight: FontWeight.w500,
|
TextFormField(
|
||||||
|
controller: _passwordController,
|
||||||
|
obscureText: !_isPasswordVisible,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: l10n.password,
|
||||||
|
hintText: l10n.passwordHint,
|
||||||
|
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: AppColors.surface,
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return l10n.passwordRequired;
|
||||||
|
}
|
||||||
|
if (value.length < 6) {
|
||||||
|
return l10n.passwordMinLength;
|
||||||
|
}
|
||||||
|
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(l10n.forgotPasswordMessage),
|
||||||
|
duration: const Duration(seconds: 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
l10n.forgotPassword,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.primaryStrong,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Verbindungsstatus
|
// Verbindungsstatus
|
||||||
// Anmelden Button
|
// Anmelden Button
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _isLoggingIn ? null : _handleLogin,
|
onPressed: _isLoggingIn ? null : _handleLogin,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.deepPurple,
|
backgroundColor: AppColors.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
),
|
),
|
||||||
elevation: 2,
|
child:
|
||||||
),
|
_isLoggingIn
|
||||||
child:
|
? Row(
|
||||||
_isLoggingIn
|
mainAxisAlignment:
|
||||||
? Row(
|
MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
children: [
|
const SizedBox(
|
||||||
const SizedBox(
|
width: 18,
|
||||||
width: 18,
|
height: 18,
|
||||||
height: 18,
|
child: CircularProgressIndicator(
|
||||||
child: CircularProgressIndicator(
|
strokeWidth: 2.5,
|
||||||
strokeWidth: 2.5,
|
valueColor:
|
||||||
valueColor:
|
AlwaysStoppedAnimation<Color>(
|
||||||
AlwaysStoppedAnimation<Color>(
|
Colors.white,
|
||||||
Colors.white,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 12),
|
||||||
const SizedBox(width: 12),
|
Text(
|
||||||
Text(
|
l10n.loggingIn,
|
||||||
l10n.loggingIn,
|
style: const TextStyle(
|
||||||
style: const TextStyle(
|
fontSize: 16,
|
||||||
fontSize: 16,
|
fontWeight: FontWeight.w600,
|
||||||
fontWeight: FontWeight.w600,
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
l10n.login,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
l10n.login,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
const SizedBox(height: 24),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (_appVersion.isNotEmpty)
|
||||||
// Version number at the bottom
|
Padding(
|
||||||
if (_appVersion.isNotEmpty)
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
Padding(
|
child: Text(
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
'Version $_appVersion',
|
||||||
child: Text(
|
style: const TextStyle(
|
||||||
'Version $_appVersion',
|
fontSize: 12,
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
color: AppColors.textMuted,
|
||||||
textAlign: TextAlign.center,
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'app_theme.dart';
|
||||||
import 'login_view.dart';
|
import 'login_view.dart';
|
||||||
import 'jobs_view.dart';
|
import 'jobs_view.dart';
|
||||||
import 'cargo_items_view.dart';
|
import 'cargo_items_view.dart';
|
||||||
@@ -104,10 +105,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'VotianLT App',
|
title: 'VotianLT App',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: buildAppTheme(),
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
|
||||||
useMaterial3: true,
|
|
||||||
),
|
|
||||||
// Localization configuration
|
// Localization configuration
|
||||||
locale: locale,
|
locale: locale,
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
@@ -178,10 +176,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text(widget.title)),
|
||||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
|
||||||
title: Text(widget.title),
|
|
||||||
),
|
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import '../task.dart';
|
|||||||
|
|
||||||
// Signature Task
|
// Signature Task
|
||||||
class SignatureTask extends Task {
|
class SignatureTask extends Task {
|
||||||
|
final String? note;
|
||||||
|
|
||||||
SignatureTask({
|
SignatureTask({
|
||||||
required super.id,
|
required super.id,
|
||||||
required super.jobId,
|
required super.jobId,
|
||||||
@@ -14,11 +16,19 @@ class SignatureTask extends Task {
|
|||||||
super.title,
|
super.title,
|
||||||
super.description,
|
super.description,
|
||||||
super.displayName,
|
super.displayName,
|
||||||
|
this.note,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory SignatureTask.fromJson(Map<String, dynamic> json) {
|
factory SignatureTask.fromJson(Map<String, dynamic> json) {
|
||||||
final commonProps = Task.parseCommonProperties(json);
|
final commonProps = Task.parseCommonProperties(json);
|
||||||
|
|
||||||
|
String? note;
|
||||||
|
final taskSpecificData = json['taskSpecificData'];
|
||||||
|
if (taskSpecificData is Map<String, dynamic>) {
|
||||||
|
final n = taskSpecificData['note'];
|
||||||
|
if (n is String) note = n;
|
||||||
|
}
|
||||||
|
|
||||||
return SignatureTask(
|
return SignatureTask(
|
||||||
id: commonProps['id'],
|
id: commonProps['id'],
|
||||||
jobId: commonProps['jobId'],
|
jobId: commonProps['jobId'],
|
||||||
@@ -31,6 +41,7 @@ class SignatureTask extends Task {
|
|||||||
title: commonProps['title'],
|
title: commonProps['title'],
|
||||||
description: commonProps['description'],
|
description: commonProps['description'],
|
||||||
displayName: commonProps['displayName'],
|
displayName: commonProps['displayName'],
|
||||||
|
note: note,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +58,11 @@ class SignatureTask extends Task {
|
|||||||
'taskOrder': taskOrder,
|
'taskOrder': taskOrder,
|
||||||
'description': description,
|
'description': description,
|
||||||
'displayName': displayName,
|
'displayName': displayName,
|
||||||
'taskSpecificData': {'taskType': 'SIGNATURE', 'title': title},
|
'taskSpecificData': {
|
||||||
|
'taskType': 'SIGNATURE',
|
||||||
|
'title': title,
|
||||||
|
'note': note,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +79,7 @@ class SignatureTask extends Task {
|
|||||||
String? title,
|
String? title,
|
||||||
String? description,
|
String? description,
|
||||||
String? displayName,
|
String? displayName,
|
||||||
|
String? note,
|
||||||
}) {
|
}) {
|
||||||
return SignatureTask(
|
return SignatureTask(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -77,6 +93,7 @@ class SignatureTask extends Task {
|
|||||||
title: title ?? this.title,
|
title: title ?? this.title,
|
||||||
description: description ?? this.description,
|
description: description ?? this.description,
|
||||||
displayName: displayName ?? this.displayName,
|
displayName: displayName ?? this.displayName,
|
||||||
|
note: note ?? this.note,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "5:2194624907249454848",
|
"id": "5:2194624907249454848",
|
||||||
"lastPropertyId": "6:5035828038544573244",
|
"lastPropertyId": "7:5673785903451668117",
|
||||||
"name": "TaskStatusEntity",
|
"name": "TaskStatusEntity",
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
@@ -275,7 +275,9 @@
|
|||||||
"modelVersionParserMinimum": 5,
|
"modelVersionParserMinimum": 5,
|
||||||
"retiredEntityUids": [],
|
"retiredEntityUids": [],
|
||||||
"retiredIndexUids": [],
|
"retiredIndexUids": [],
|
||||||
"retiredPropertyUids": [],
|
"retiredPropertyUids": [
|
||||||
|
5673785903451668117
|
||||||
|
],
|
||||||
"retiredRelationUids": [],
|
"retiredRelationUids": [],
|
||||||
"version": 1
|
"version": 1
|
||||||
}
|
}
|
||||||
@@ -240,7 +240,7 @@ final _entities = <obx_int.ModelEntity>[
|
|||||||
obx_int.ModelEntity(
|
obx_int.ModelEntity(
|
||||||
id: const obx_int.IdUid(5, 2194624907249454848),
|
id: const obx_int.IdUid(5, 2194624907249454848),
|
||||||
name: 'TaskStatusEntity',
|
name: 'TaskStatusEntity',
|
||||||
lastPropertyId: const obx_int.IdUid(6, 5035828038544573244),
|
lastPropertyId: const obx_int.IdUid(7, 5673785903451668117),
|
||||||
flags: 0,
|
flags: 0,
|
||||||
properties: <obx_int.ModelProperty>[
|
properties: <obx_int.ModelProperty>[
|
||||||
obx_int.ModelProperty(
|
obx_int.ModelProperty(
|
||||||
@@ -371,7 +371,7 @@ obx_int.ModelDefinition getObjectBoxModel() {
|
|||||||
lastSequenceId: const obx_int.IdUid(0, 0),
|
lastSequenceId: const obx_int.IdUid(0, 0),
|
||||||
retiredEntityUids: const [],
|
retiredEntityUids: const [],
|
||||||
retiredIndexUids: const [],
|
retiredIndexUids: const [],
|
||||||
retiredPropertyUids: const [],
|
retiredPropertyUids: const [5673785903451668117],
|
||||||
retiredRelationUids: const [],
|
retiredRelationUids: const [],
|
||||||
modelVersion: 5,
|
modelVersion: 5,
|
||||||
modelVersionParserMinimum: 5,
|
modelVersionParserMinimum: 5,
|
||||||
@@ -632,7 +632,7 @@ obx_int.ModelDefinition getObjectBoxModel() {
|
|||||||
},
|
},
|
||||||
objectToFB: (TaskStatusEntity object, fb.Builder fbb) {
|
objectToFB: (TaskStatusEntity object, fb.Builder fbb) {
|
||||||
final taskIdOffset = fbb.writeString(object.taskId);
|
final taskIdOffset = fbb.writeString(object.taskId);
|
||||||
fbb.startTable(7);
|
fbb.startTable(8);
|
||||||
fbb.addInt64(0, object.id);
|
fbb.addInt64(0, object.id);
|
||||||
fbb.addOffset(1, taskIdOffset);
|
fbb.addOffset(1, taskIdOffset);
|
||||||
fbb.addBool(2, object.completed);
|
fbb.addBool(2, object.completed);
|
||||||
|
|||||||
@@ -831,6 +831,42 @@ class DatabaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Save signature note (Bemerkung) for a task into user_data table
|
||||||
|
Future<void> saveTaskSignatureNote(String taskId, String note) async {
|
||||||
|
try {
|
||||||
|
if (_store == null) {
|
||||||
|
developer.log('Database not initialized', name: 'DatabaseService');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final key = 'task_signature_note:$taskId';
|
||||||
|
await saveKeyValue(key, note);
|
||||||
|
} catch (e, st) {
|
||||||
|
developer.log(
|
||||||
|
'Error saving task signature note: $e',
|
||||||
|
name: 'DatabaseService',
|
||||||
|
);
|
||||||
|
developer.log('Stack trace: $st', name: 'DatabaseService');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load signature note (Bemerkung) for a task from user_data table
|
||||||
|
Future<String?> loadTaskSignatureNote(String taskId) async {
|
||||||
|
try {
|
||||||
|
if (_store == null) {
|
||||||
|
developer.log('Database not initialized', name: 'DatabaseService');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await loadKeyValue('task_signature_note:$taskId');
|
||||||
|
} catch (e, st) {
|
||||||
|
developer.log(
|
||||||
|
'Error loading task signature note: $e',
|
||||||
|
name: 'DatabaseService',
|
||||||
|
);
|
||||||
|
developer.log('Stack trace: $st', name: 'DatabaseService');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Load signature SVG for a task from user_data table
|
/// Load signature SVG for a task from user_data table
|
||||||
Future<String?> loadTaskSignature(String taskId) async {
|
Future<String?> loadTaskSignature(String taskId) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1189,6 +1189,36 @@ class WebSocketService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send station completion event to server.
|
||||||
|
/// Messages are buffered if offline and sent automatically when reconnected.
|
||||||
|
Future<void> sendStationCompleted({
|
||||||
|
required String jobId,
|
||||||
|
required String jobNumber,
|
||||||
|
required int stationOrder,
|
||||||
|
bool hasIncompleteOptionalTasks = false,
|
||||||
|
}) async {
|
||||||
|
const String destination = '/server/station_completed';
|
||||||
|
|
||||||
|
final payload = <String, dynamic>{
|
||||||
|
'jobId': jobId,
|
||||||
|
'jobNumber': jobNumber,
|
||||||
|
'stationOrder': stationOrder,
|
||||||
|
'completedAt': DateTime.now().toUtc().toIso8601String(),
|
||||||
|
'hasIncompleteOptionalTasks': hasIncompleteOptionalTasks,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final jsonPayload = jsonEncode(payload);
|
||||||
|
sendMessage(destination, jsonPayload);
|
||||||
|
} catch (e, st) {
|
||||||
|
developer.log(
|
||||||
|
'Error sending station completion: $e',
|
||||||
|
name: 'WebSocketService',
|
||||||
|
);
|
||||||
|
developer.log('Stack: $st', name: 'WebSocketService');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Dispose resources
|
/// Dispose resources
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_stopReconnectTimer();
|
_stopReconnectTimer();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'app_state.dart';
|
import 'app_state.dart';
|
||||||
|
import 'app_theme.dart';
|
||||||
|
|
||||||
/// Supported languages with their display names and flag emojis
|
/// Supported languages with their display names and flag emojis
|
||||||
class LanguageOption {
|
class LanguageOption {
|
||||||
@@ -98,11 +99,9 @@ class _SettingsViewState extends State<SettingsView> {
|
|||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text('${l10n.languageChanged}: $flagEmoji $languageName'),
|
||||||
'${l10n.languageChanged}: $flagEmoji $languageName',
|
|
||||||
),
|
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: AppColors.success,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -129,10 +128,7 @@ class _SettingsViewState extends State<SettingsView> {
|
|||||||
final languageOptions = _getLanguageOptions();
|
final languageOptions = _getLanguageOptions();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text(l10n.settings)),
|
||||||
title: Text(l10n.settings),
|
|
||||||
backgroundColor: Colors.deepPurple[100],
|
|
||||||
),
|
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
// Language Selection Section
|
// Language Selection Section
|
||||||
@@ -143,7 +139,7 @@ class _SettingsViewState extends State<SettingsView> {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.grey,
|
color: AppColors.textMuted,
|
||||||
letterSpacing: 1.2,
|
letterSpacing: 1.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -160,7 +156,7 @@ class _SettingsViewState extends State<SettingsView> {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[100],
|
color: AppColors.surfaceMuted,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -173,22 +169,27 @@ class _SettingsViewState extends State<SettingsView> {
|
|||||||
title: Text(
|
title: Text(
|
||||||
language.name,
|
language.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
fontWeight:
|
||||||
color: isSelected ? Colors.deepPurple : Colors.black87,
|
isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||||
|
color:
|
||||||
|
isSelected
|
||||||
|
? AppColors.primaryStrong
|
||||||
|
: AppColors.textStrong,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: isSelected
|
trailing:
|
||||||
? const Icon(
|
isSelected
|
||||||
Icons.check_circle,
|
? const Icon(
|
||||||
color: Colors.deepPurple,
|
Icons.check_circle,
|
||||||
)
|
color: AppColors.primary,
|
||||||
: const Icon(
|
)
|
||||||
Icons.circle_outlined,
|
: const Icon(
|
||||||
color: Colors.grey,
|
Icons.circle_outlined,
|
||||||
),
|
color: AppColors.textMuted,
|
||||||
|
),
|
||||||
onTap: () => _onLanguageSelected(language.code),
|
onTap: () => _onLanguageSelected(language.code),
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
selectedTileColor: Colors.deepPurple.withValues(alpha: 0.05),
|
selectedTileColor: AppColors.primarySoft,
|
||||||
),
|
),
|
||||||
const Divider(height: 1, indent: 72),
|
const Divider(height: 1, indent: 72),
|
||||||
],
|
],
|
||||||
@@ -203,17 +204,14 @@ class _SettingsViewState extends State<SettingsView> {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.grey,
|
color: AppColors.textMuted,
|
||||||
letterSpacing: 1.2,
|
letterSpacing: 1.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(
|
leading: Icon(Icons.info_outline, color: AppColors.textMuted),
|
||||||
Icons.info_outline,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
|
||||||
title: Text(l10n.version),
|
title: Text(l10n.version),
|
||||||
subtitle: const Text('0.9.2'),
|
subtitle: const Text('0.9.2'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:image/image.dart' as img;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'app_theme.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'l10n/localization_helpers.dart';
|
import 'l10n/localization_helpers.dart';
|
||||||
import 'models/job.dart';
|
import 'models/job.dart';
|
||||||
@@ -40,7 +41,6 @@ class TaskView extends StatefulWidget {
|
|||||||
|
|
||||||
class _TaskViewState extends State<TaskView> {
|
class _TaskViewState extends State<TaskView> {
|
||||||
final Set<String> _completedTasks = {};
|
final Set<String> _completedTasks = {};
|
||||||
final Set<String> _skippedTasks = {};
|
|
||||||
final DatabaseService _databaseService = DatabaseService();
|
final DatabaseService _databaseService = DatabaseService();
|
||||||
// Store SVG representations of signatures per task for later use
|
// Store SVG representations of signatures per task for later use
|
||||||
final Map<String, String> _signatureSvgByTask = {};
|
final Map<String, String> _signatureSvgByTask = {};
|
||||||
@@ -61,7 +61,7 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load task completion statuses from database and merge with JSON task states
|
/// Load task completion and skipped statuses from database and merge with JSON task states
|
||||||
Future<void> _loadTaskStatuses() async {
|
Future<void> _loadTaskStatuses() async {
|
||||||
final statuses = await _databaseService.loadAllTaskStatuses();
|
final statuses = await _databaseService.loadAllTaskStatuses();
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -90,7 +90,6 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
? '${AppLocalizations.of(context).tasks} - ${widget.stationTitle}'
|
? '${AppLocalizations.of(context).tasks} - ${widget.stationTitle}'
|
||||||
: '${AppLocalizations.of(context).tasks} - ${widget.job.jobNumber}',
|
: '${AppLocalizations.of(context).tasks} - ${widget.job.jobNumber}',
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.deepPurple[100],
|
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -117,14 +116,14 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
constraints: const BoxConstraints(maxHeight: 150),
|
constraints: const BoxConstraints(maxHeight: 150),
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F9FA),
|
color: AppColors.surfaceMuted,
|
||||||
border: Border.all(color: Colors.grey[300]!, width: 1),
|
border: Border.all(color: AppColors.border, width: 1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Text(
|
child: Text(
|
||||||
_getRemark(),
|
_getRemark(),
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
style: const TextStyle(fontSize: 14, color: AppColors.text),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -133,7 +132,13 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [Expanded(child: _buildTasksStepper())],
|
children: [
|
||||||
|
Expanded(child: _buildTasksStepper()),
|
||||||
|
if (_visibleTasks.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildCompleteStationButton(),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -142,22 +147,111 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _canCompleteStation {
|
||||||
|
// Station kann abgeschlossen werden, wenn alle nicht-optionalen Aufgaben
|
||||||
|
// erledigt sind. Sind nur optionale Aufgaben vorhanden, ist der Button
|
||||||
|
// immer aktiv.
|
||||||
|
for (final t in _visibleTasks) {
|
||||||
|
if (!t.optional && !_completedTasks.contains(t.id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _hasIncompleteOptionalTasks {
|
||||||
|
for (final t in _visibleTasks) {
|
||||||
|
if (t.optional && !_completedTasks.contains(t.id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCompleteStationButton() {
|
||||||
|
final bool enabled = _canCompleteStation;
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: enabled ? _onCompleteStationPressed : null,
|
||||||
|
icon: const Icon(Icons.flag),
|
||||||
|
label: const Text('Station abschließen'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCompleteStationPressed() {
|
||||||
|
if (_hasIncompleteOptionalTasks) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Offene optionale Aufgaben'),
|
||||||
|
content: const Text(
|
||||||
|
'Es gibt nicht erledigte optionale Aufgaben. Möchten Sie die Station trotzdem abschließen?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(ctx).pop(),
|
||||||
|
child: Text(AppLocalizations.of(context).cancel),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(ctx).pop();
|
||||||
|
_sendStationCompleted();
|
||||||
|
},
|
||||||
|
child: const Text('Trotzdem abschließen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_sendStationCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendStationCompleted() {
|
||||||
|
final stationOrder = widget.stationOrder ?? 0;
|
||||||
|
try {
|
||||||
|
StompService().sendStationCompleted(
|
||||||
|
jobId: widget.job.id,
|
||||||
|
jobNumber: widget.job.jobNumber,
|
||||||
|
stationOrder: stationOrder,
|
||||||
|
hasIncompleteOptionalTasks: _hasIncompleteOptionalTasks,
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Station abgeschlossen')),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
} catch (e) {
|
||||||
|
developer.log('Error sending station completion: $e', name: 'TaskView');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildTasksStepper() {
|
Widget _buildTasksStepper() {
|
||||||
if (_visibleTasks.isEmpty) {
|
if (_visibleTasks.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.task_outlined, size: 64, color: Colors.grey[400]),
|
const Icon(
|
||||||
|
Icons.task_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).noTasks,
|
AppLocalizations.of(context).noTasks,
|
||||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
style: const TextStyle(fontSize: 16, color: AppColors.textMuted),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).noTasksMessage,
|
AppLocalizations.of(context).noTasksMessage,
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
style: const TextStyle(fontSize: 14, color: AppColors.textMuted),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -170,36 +264,30 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final task = _visibleTasks[index];
|
final task = _visibleTasks[index];
|
||||||
final isCompleted = _completedTasks.contains(task.id);
|
final isCompleted = _completedTasks.contains(task.id);
|
||||||
final isSkipped = _skippedTasks.contains(task.id);
|
|
||||||
final canBeCompletedNow =
|
final canBeCompletedNow =
|
||||||
!isCompleted && !isSkipped && _arePreviousTasksCompleted(index);
|
!isCompleted && _arePreviousTasksCompleted(index);
|
||||||
|
|
||||||
// Hintergrundfarbe je nach Status:
|
// Hintergrundfarbe je nach Status:
|
||||||
// abgeschlossen → hellgrün, übersprungen → hellgelb, bearbeitbar → weiß, gesperrt → hellgrau
|
// abgeschlossen → hellgrün, bearbeitbar → weiß, gesperrt → hellgrau
|
||||||
|
// (Optionale Aufgaben werden durch einen Chip markiert, nicht per Farbe.)
|
||||||
final Color cardColor =
|
final Color cardColor =
|
||||||
isCompleted
|
isCompleted
|
||||||
? const Color(0xFFE8F5E9) // hellgrün
|
? AppColors.successSoft
|
||||||
: isSkipped
|
|
||||||
? const Color(0xFFFFF8E1) // hellgelb
|
|
||||||
: canBeCompletedNow
|
: canBeCompletedNow
|
||||||
? Colors.white
|
? AppColors.surface
|
||||||
: const Color(0xFFF5F5F5); // hellgrau
|
: AppColors.surfaceMuted;
|
||||||
final Color borderColor =
|
final Color borderColor =
|
||||||
isCompleted
|
isCompleted
|
||||||
? Colors.green[300]!
|
? AppColors.success.withValues(alpha: 0.35)
|
||||||
: isSkipped
|
|
||||||
? Colors.amber[300]!
|
|
||||||
: canBeCompletedNow
|
: canBeCompletedNow
|
||||||
? Colors.grey[300]!
|
? AppColors.border
|
||||||
: Colors.grey[200]!;
|
: AppColors.border.withValues(alpha: 0.7);
|
||||||
final Color circleColor =
|
final Color circleColor =
|
||||||
isCompleted
|
isCompleted
|
||||||
? Colors.green[600]!
|
? AppColors.success
|
||||||
: isSkipped
|
|
||||||
? Colors.amber[600]!
|
|
||||||
: canBeCompletedNow
|
: canBeCompletedNow
|
||||||
? Colors.deepPurple[400]!
|
? AppColors.primary
|
||||||
: Colors.grey[400]!;
|
: AppColors.textMuted;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
@@ -220,61 +308,93 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: cardColor,
|
color: cardColor,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Stack(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
// Task number circle
|
Row(
|
||||||
Container(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
width: 40,
|
children: [
|
||||||
height: 40,
|
// Task number circle
|
||||||
decoration: BoxDecoration(
|
Container(
|
||||||
shape: BoxShape.circle,
|
width: 40,
|
||||||
color: circleColor,
|
height: 40,
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
child: Center(
|
shape: BoxShape.circle,
|
||||||
child: Text(
|
color: circleColor,
|
||||||
'${index + 1}',
|
),
|
||||||
style: TextStyle(
|
child: Center(
|
||||||
color: Colors.white,
|
child: Text(
|
||||||
fontWeight: FontWeight.bold,
|
'${index + 1}',
|
||||||
fontSize: 16,
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
// Task content
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
right: task.optional ? 72 : 0,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildTaskDisplayText(task, isCompleted, 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),
|
||||||
|
const Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
color: AppColors.success,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (task.optional)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.warningSoft,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.warning.withValues(alpha: 0.5),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Optional',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.warning,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
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]),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -502,10 +622,11 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
builder:
|
builder:
|
||||||
(context) => SignatureCaptureScreen(
|
(context) => SignatureCaptureScreen(
|
||||||
task: task,
|
task: task,
|
||||||
onSignatureCompleted: (String svg) async {
|
onSignatureCompleted: (String svg, String note) async {
|
||||||
try {
|
try {
|
||||||
// Persist SVG only (no PNG)
|
// Persist SVG only (no PNG)
|
||||||
await _databaseService.saveTaskSignature(task.id, svg);
|
await _databaseService.saveTaskSignature(task.id, svg);
|
||||||
|
await _databaseService.saveTaskSignatureNote(task.id, note);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
developer.log(
|
developer.log(
|
||||||
'Error saving task signature: $e',
|
'Error saving task signature: $e',
|
||||||
@@ -529,6 +650,7 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
'signatureSvg': svg,
|
'signatureSvg': svg,
|
||||||
'svgLength': svg.length,
|
'svgLength': svg.length,
|
||||||
'hasSignature': true,
|
'hasSignature': true,
|
||||||
|
'signatureNote': note,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -612,6 +734,7 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
String? taskType,
|
String? taskType,
|
||||||
Map<String, dynamic>? extraData,
|
Map<String, dynamic>? extraData,
|
||||||
}) {
|
}) {
|
||||||
|
final bool hadOpenMandatoryBefore = _hasOpenMandatoryTasks;
|
||||||
setState(() {
|
setState(() {
|
||||||
_completedTasks.add(taskId);
|
_completedTasks.add(taskId);
|
||||||
});
|
});
|
||||||
@@ -628,15 +751,60 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
developer.log('Error sending task completion: $e', name: 'TaskView');
|
developer.log('Error sending task completion: $e', name: 'TaskView');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wenn die letzte nicht-optionale Aufgabe gerade erledigt wurde,
|
||||||
|
// den Benutzer fragen, ob er die Station jetzt abschließen möchte.
|
||||||
|
if (hadOpenMandatoryBefore && !_hasOpenMandatoryTasks) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
_showLastMandatoryCompletedDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _hasOpenMandatoryTasks {
|
||||||
|
for (final t in _visibleTasks) {
|
||||||
|
if (!t.optional && !_completedTasks.contains(t.id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showLastMandatoryCompletedDialog() {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Alle Pflichtaufgaben erledigt'),
|
||||||
|
content: const Text(
|
||||||
|
'Alle nicht optionalen Aufgaben dieser Station sind erledigt. '
|
||||||
|
'Möchten Sie die Station jetzt abschließen?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(ctx).pop(),
|
||||||
|
child: const Text('Später'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(ctx).pop();
|
||||||
|
_onCompleteStationPressed();
|
||||||
|
},
|
||||||
|
child: const Text('Station abschließen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _arePreviousTasksCompleted(int index) {
|
bool _arePreviousTasksCompleted(int index) {
|
||||||
if (index <= 0) return true;
|
if (index <= 0) return true;
|
||||||
for (int i = 0; i < index; i++) {
|
for (int i = 0; i < index; i++) {
|
||||||
final t = _visibleTasks[i];
|
final t = _visibleTasks[i];
|
||||||
if (!t.optional &&
|
if (!t.optional && !_completedTasks.contains(t.id)) {
|
||||||
!_completedTasks.contains(t.id) &&
|
|
||||||
!_skippedTasks.contains(t.id)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -730,6 +898,10 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
task.description != null
|
task.description != null
|
||||||
? localizeKnownText(context, task.description!)
|
? localizeKnownText(context, task.description!)
|
||||||
: null;
|
: null;
|
||||||
|
final String? signatureNote =
|
||||||
|
(task is SignatureTask && task.note != null && task.note!.trim().isNotEmpty)
|
||||||
|
? task.note!.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
if (displayName?.isNotEmpty == true) {
|
if (displayName?.isNotEmpty == true) {
|
||||||
return Column(
|
return Column(
|
||||||
@@ -740,14 +912,39 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(description!, style: subtitleStyle),
|
Text(description!, style: subtitleStyle),
|
||||||
],
|
],
|
||||||
|
if (signatureNote != null) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(signatureNote, style: subtitleStyle),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (description?.isNotEmpty == true) {
|
if (description?.isNotEmpty == true) {
|
||||||
|
if (signatureNote != null) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(description!, style: titleStyle),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(signatureNote, style: subtitleStyle),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
return Text(description!, style: titleStyle);
|
return Text(description!, style: titleStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (signatureNote != null) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(_getStandardTaskDisplayText(task), style: titleStyle),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(signatureNote, style: subtitleStyle),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Fall back to standard text based on task type
|
// Fall back to standard text based on task type
|
||||||
return Text(_getStandardTaskDisplayText(task), style: titleStyle);
|
return Text(_getStandardTaskDisplayText(task), style: titleStyle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ class BarcodeCaptureScreen extends StatefulWidget {
|
|||||||
final BarcodeTask task;
|
final BarcodeTask task;
|
||||||
final Function(List<String>) onBarcodesCompleted;
|
final Function(List<String>) onBarcodesCompleted;
|
||||||
|
|
||||||
const BarcodeCaptureScreen({super.key, required this.task, required this.onBarcodesCompleted});
|
const BarcodeCaptureScreen({
|
||||||
|
super.key,
|
||||||
|
required this.task,
|
||||||
|
required this.onBarcodesCompleted,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<BarcodeCaptureScreen> createState() => _BarcodeCaptureScreenState();
|
State<BarcodeCaptureScreen> createState() => _BarcodeCaptureScreenState();
|
||||||
@@ -70,7 +74,11 @@ class _BarcodeCaptureScreenState extends State<BarcodeCaptureScreen> {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${AppLocalizations.of(context).cameraError}: $e')));
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('${AppLocalizations.of(context).cameraError}: $e'),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,7 +150,28 @@ class _BarcodeCaptureScreenState extends State<BarcodeCaptureScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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()))]));
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(AppLocalizations.of(context).barcodeScan),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
const OfflineBanner(),
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
_isScannerInitialized
|
||||||
|
? (_isMobilePlatform
|
||||||
|
? _buildMobileView()
|
||||||
|
: _buildDesktopView())
|
||||||
|
: const Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMobileView() {
|
Widget _buildMobileView() {
|
||||||
@@ -153,9 +182,33 @@ class _BarcodeCaptureScreenState extends State<BarcodeCaptureScreen> {
|
|||||||
flex: 3,
|
flex: 3,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
MobileScanner(controller: _scannerController, onDetect: _onBarcodeDetected),
|
MobileScanner(
|
||||||
|
controller: _scannerController,
|
||||||
|
onDetect: _onBarcodeDetected,
|
||||||
|
),
|
||||||
// Overlay with scanning frame
|
// 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)))))),
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -167,20 +220,47 @@ class _BarcodeCaptureScreenState extends State<BarcodeCaptureScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('${AppLocalizations.of(context).scannedBarcodes} (${_scannedBarcodes.length}/${widget.task.maxBarcodeCount})', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
Text(
|
||||||
|
'${AppLocalizations.of(context).scannedBarcodes} (${_scannedBarcodes.length}/${widget.task.maxBarcodeCount})',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text('${AppLocalizations.of(context).minBarcodes} ${widget.task.minBarcodeCount} ${AppLocalizations.of(context).barcodesRequired}', style: TextStyle(fontSize: 14, color: Colors.grey[600])),
|
Text(
|
||||||
|
'${AppLocalizations.of(context).minBarcodes} ${widget.task.minBarcodeCount} ${AppLocalizations.of(context).barcodesRequired}',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: _scannedBarcodes.length,
|
itemCount: _scannedBarcodes.length,
|
||||||
itemBuilder: (context, index) {
|
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))));
|
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),
|
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))),
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -195,9 +275,15 @@ class _BarcodeCaptureScreenState extends State<BarcodeCaptureScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(AppLocalizations.of(context).enterBarcode, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
Text(
|
||||||
|
AppLocalizations.of(context).enterBarcode,
|
||||||
|
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text('${AppLocalizations.of(context).barcodeEnterDescription} (${widget.task.minBarcodeCount}-${widget.task.maxBarcodeCount})', style: TextStyle(fontSize: 16, color: Colors.grey[600])),
|
Text(
|
||||||
|
'${AppLocalizations.of(context).barcodeEnterDescription} (${widget.task.minBarcodeCount}-${widget.task.maxBarcodeCount})',
|
||||||
|
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
@@ -207,7 +293,18 @@ class _BarcodeCaptureScreenState extends State<BarcodeCaptureScreen> {
|
|||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _textControllers[index],
|
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)),
|
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) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
// Trigger rebuild to update button state
|
// Trigger rebuild to update button state
|
||||||
@@ -219,7 +316,16 @@ class _BarcodeCaptureScreenState extends State<BarcodeCaptureScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
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))),
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:camera/camera.dart';
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:file_selector/file_selector.dart' as fsel;
|
import 'package:file_selector/file_selector.dart' as fsel;
|
||||||
import 'package:votianlt_app/services/developer.dart' as developer;
|
import 'package:votianlt_app/services/developer.dart' as developer;
|
||||||
|
import '../app_theme.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../models/tasks/photo_task.dart';
|
import '../models/tasks/photo_task.dart';
|
||||||
import '../widgets/offline_banner.dart';
|
import '../widgets/offline_banner.dart';
|
||||||
@@ -91,11 +92,16 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
developer.log('Error initializing camera: $e', name: 'PhotoCaptureScreen');
|
developer.log(
|
||||||
|
'Error initializing camera: $e',
|
||||||
|
name: 'PhotoCaptureScreen',
|
||||||
|
);
|
||||||
developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen');
|
developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('${AppLocalizations.of(context).cameraError}: $e')),
|
SnackBar(
|
||||||
|
content: Text('${AppLocalizations.of(context).cameraError}: $e'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +124,9 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
} else {
|
} else {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(AppLocalizations.of(context).cameraNotReady)),
|
SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context).cameraNotReady),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +135,9 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen');
|
developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('${AppLocalizations.of(context).photoError}: $e')),
|
SnackBar(
|
||||||
|
content: Text('${AppLocalizations.of(context).photoError}: $e'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +146,8 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
Future<void> _pickPhotoFromFile() async {
|
Future<void> _pickPhotoFromFile() async {
|
||||||
try {
|
try {
|
||||||
// Use file_selector for desktop and web for robust platform support
|
// Use file_selector for desktop and web for robust platform support
|
||||||
final bool useFileSelector = kIsWeb ||
|
final bool useFileSelector =
|
||||||
|
kIsWeb ||
|
||||||
defaultTargetPlatform == TargetPlatform.macOS ||
|
defaultTargetPlatform == TargetPlatform.macOS ||
|
||||||
defaultTargetPlatform == TargetPlatform.windows ||
|
defaultTargetPlatform == TargetPlatform.windows ||
|
||||||
defaultTargetPlatform == TargetPlatform.linux;
|
defaultTargetPlatform == TargetPlatform.linux;
|
||||||
@@ -146,7 +157,9 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
label: 'images',
|
label: 'images',
|
||||||
extensions: ['jpg', 'jpeg', 'png', 'heic', 'bmp', 'gif', 'webp'],
|
extensions: ['jpg', 'jpeg', 'png', 'heic', 'bmp', 'gif', 'webp'],
|
||||||
);
|
);
|
||||||
final fsel.XFile? picked = await fsel.openFile(acceptedTypeGroups: [typeGroup]);
|
final fsel.XFile? picked = await fsel.openFile(
|
||||||
|
acceptedTypeGroups: [typeGroup],
|
||||||
|
);
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
final data = await picked.readAsBytes();
|
final data = await picked.readAsBytes();
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -187,11 +200,16 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
developer.log('Error picking photo from file: $e', name: 'PhotoCaptureScreen');
|
developer.log(
|
||||||
|
'Error picking photo from file: $e',
|
||||||
|
name: 'PhotoCaptureScreen',
|
||||||
|
);
|
||||||
developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen');
|
developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('${AppLocalizations.of(context).photoError}: $e')),
|
SnackBar(
|
||||||
|
content: Text('${AppLocalizations.of(context).photoError}: $e'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,7 +248,10 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||||
child: Text(AppLocalizations.of(context).delete, style: const TextStyle(color: Colors.white)),
|
child: Text(
|
||||||
|
AppLocalizations.of(context).delete,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -240,7 +261,7 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
|
|
||||||
bool get _canComplete {
|
bool get _canComplete {
|
||||||
return _capturedPhotos.length >= widget.task.minPhotoCount &&
|
return _capturedPhotos.length >= widget.task.minPhotoCount &&
|
||||||
_capturedPhotos.length <= widget.task.maxPhotoCount;
|
_capturedPhotos.length <= widget.task.maxPhotoCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _canTakeMore {
|
bool get _canTakeMore {
|
||||||
@@ -276,7 +297,10 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
);
|
);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
developer.log('Error animating to page: $e', name: 'PhotoCaptureScreen');
|
developer.log(
|
||||||
|
'Error animating to page: $e',
|
||||||
|
name: 'PhotoCaptureScreen',
|
||||||
|
);
|
||||||
developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen');
|
developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen');
|
||||||
_pageController.jumpToPage(clamped);
|
_pageController.jumpToPage(clamped);
|
||||||
}
|
}
|
||||||
@@ -304,7 +328,7 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(AppLocalizations.of(context).photoCapture),
|
title: Text(AppLocalizations.of(context).photoCapture),
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: AppColors.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
actions: [
|
actions: [
|
||||||
if (_canComplete)
|
if (_canComplete)
|
||||||
@@ -315,7 +339,10 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).finish,
|
AppLocalizations.of(context).finish,
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -327,7 +354,7 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: EdgeInsets.all(16),
|
padding: EdgeInsets.all(16),
|
||||||
color: Colors.grey[100],
|
color: AppColors.surfaceMuted,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -337,7 +364,10 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${AppLocalizations.of(context).photosTaken}: ${_capturedPhotos.length}',
|
'${AppLocalizations.of(context).photosTaken}: ${_capturedPhotos.length}',
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -345,19 +375,20 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
|
|
||||||
// Camera preview, photo gallery or empty state
|
// Camera preview, photo gallery or empty state
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _capturedPhotos.isEmpty
|
child:
|
||||||
? _buildCameraOrEmptyState()
|
_capturedPhotos.isEmpty
|
||||||
: _buildPhotoGallery(),
|
? _buildCameraOrEmptyState()
|
||||||
|
: _buildPhotoGallery(),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Bottom controls
|
// Bottom controls
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.all(16),
|
padding: EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: AppColors.surface,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.grey.withValues(alpha: 0.3),
|
color: AppColors.textStrong.withValues(alpha: 0.12),
|
||||||
spreadRadius: 1,
|
spreadRadius: 1,
|
||||||
blurRadius: 5,
|
blurRadius: 5,
|
||||||
offset: Offset(0, -3),
|
offset: Offset(0, -3),
|
||||||
@@ -372,23 +403,50 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
// Camera or file select button
|
// Camera or file select button
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: _canTakeMore && _isCameraSupportedOnThisPlatform
|
onPressed:
|
||||||
? (_useFilePickerMode
|
_canTakeMore && _isCameraSupportedOnThisPlatform
|
||||||
? _pickPhotoFromFile
|
? (_useFilePickerMode
|
||||||
: (_isCameraInitialized ? _capturePhoto : null))
|
? _pickPhotoFromFile
|
||||||
: null,
|
: (_isCameraInitialized
|
||||||
icon: Icon(_useFilePickerMode ? Icons.photo_library : Icons.camera_alt),
|
? _capturePhoto
|
||||||
|
: null))
|
||||||
|
: null,
|
||||||
|
icon: Icon(
|
||||||
|
_useFilePickerMode
|
||||||
|
? Icons.photo_library
|
||||||
|
: Icons.camera_alt,
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
!_isCameraSupportedOnThisPlatform
|
!_isCameraSupportedOnThisPlatform
|
||||||
? AppLocalizations.of(context).cameraNotSupportedOnPlatform
|
? AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
).cameraNotSupportedOnPlatform
|
||||||
: (!_canTakeMore
|
: (!_canTakeMore
|
||||||
? AppLocalizations.of(context).maxPhotosReached
|
? AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
).maxPhotosReached
|
||||||
: (_useFilePickerMode
|
: (_useFilePickerMode
|
||||||
? AppLocalizations.of(context).selectPhoto
|
? AppLocalizations.of(context).selectPhoto
|
||||||
: (_isCameraInitialized ? AppLocalizations.of(context).takePhoto : (defaultTargetPlatform == TargetPlatform.macOS ? AppLocalizations.of(context).cameraReadyNoPreview : AppLocalizations.of(context).cameraLoading)))),
|
: (_isCameraInitialized
|
||||||
|
? AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
).takePhoto
|
||||||
|
: (defaultTargetPlatform ==
|
||||||
|
TargetPlatform.macOS
|
||||||
|
? AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
).cameraReadyNoPreview
|
||||||
|
: AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
).cameraLoading)))),
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: _canTakeMore && (_useFilePickerMode || _isCameraInitialized) ? Colors.blue : Colors.grey,
|
backgroundColor:
|
||||||
|
_canTakeMore &&
|
||||||
|
(_useFilePickerMode ||
|
||||||
|
_isCameraInitialized)
|
||||||
|
? AppColors.primary
|
||||||
|
: AppColors.borderStrong,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: EdgeInsets.symmetric(vertical: 12),
|
padding: EdgeInsets.symmetric(vertical: 12),
|
||||||
),
|
),
|
||||||
@@ -405,7 +463,10 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: 12,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -416,18 +477,23 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _canComplete
|
onPressed:
|
||||||
? () {
|
_canComplete
|
||||||
widget.onPhotosCompleted(_capturedPhotos);
|
? () {
|
||||||
Navigator.of(context).pop();
|
widget.onPhotosCompleted(_capturedPhotos);
|
||||||
}
|
Navigator.of(context).pop();
|
||||||
: null,
|
}
|
||||||
|
: null,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: _canComplete ? Colors.green : Colors.grey,
|
backgroundColor:
|
||||||
|
_canComplete ? Colors.green : Colors.grey,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: EdgeInsets.symmetric(vertical: 14),
|
padding: EdgeInsets.symmetric(vertical: 14),
|
||||||
),
|
),
|
||||||
child: Text(AppLocalizations.of(context).finish, style: const TextStyle(fontWeight: FontWeight.bold)),
|
child: Text(
|
||||||
|
AppLocalizations.of(context).finish,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -451,7 +517,11 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).cameraNotAvailable,
|
AppLocalizations.of(context).cameraNotAvailable,
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[700]),
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
@@ -477,7 +547,11 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).addPhotos,
|
AppLocalizations.of(context).addPhotos,
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[700]),
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
@@ -518,11 +592,7 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Icons.camera_alt, size: 80, color: Colors.grey[400]),
|
||||||
Icons.camera_alt,
|
|
||||||
size: 80,
|
|
||||||
color: Colors.grey[400],
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).cameraInitializing,
|
AppLocalizations.of(context).cameraInitializing,
|
||||||
@@ -535,10 +605,7 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).cameraLoadingMessage,
|
AppLocalizations.of(context).cameraLoadingMessage,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey[500],
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -601,7 +668,11 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.error, size: 50, color: Colors.grey[600]),
|
Icon(
|
||||||
|
Icons.error,
|
||||||
|
size: 50,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
Text(AppLocalizations.of(context).photoError),
|
Text(AppLocalizations.of(context).photoError),
|
||||||
],
|
],
|
||||||
@@ -621,9 +692,8 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: _currentPhotoIndex > 0
|
onPressed:
|
||||||
? _goToPreviousPhoto
|
_currentPhotoIndex > 0 ? _goToPreviousPhoto : null,
|
||||||
: null,
|
|
||||||
icon: Icon(Icons.chevron_left, size: 36),
|
icon: Icon(Icons.chevron_left, size: 36),
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.white.withValues(alpha: 0.7),
|
backgroundColor: Colors.white.withValues(alpha: 0.7),
|
||||||
@@ -638,9 +708,10 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: _currentPhotoIndex < _capturedPhotos.length - 1
|
onPressed:
|
||||||
? _goToNextPhoto
|
_currentPhotoIndex < _capturedPhotos.length - 1
|
||||||
: null,
|
? _goToNextPhoto
|
||||||
|
: null,
|
||||||
icon: Icon(Icons.chevron_right, size: 36),
|
icon: Icon(Icons.chevron_right, size: 36),
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.white.withValues(alpha: 0.7),
|
backgroundColor: Colors.white.withValues(alpha: 0.7),
|
||||||
@@ -658,19 +729,21 @@ class _PhotoCaptureScreenState extends State<PhotoCaptureScreen> {
|
|||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: _capturedPhotos.asMap().entries.map((entry) {
|
children:
|
||||||
return Container(
|
_capturedPhotos.asMap().entries.map((entry) {
|
||||||
width: 8,
|
return Container(
|
||||||
height: 8,
|
width: 8,
|
||||||
margin: EdgeInsets.symmetric(horizontal: 4),
|
height: 8,
|
||||||
decoration: BoxDecoration(
|
margin: EdgeInsets.symmetric(horizontal: 4),
|
||||||
shape: BoxShape.circle,
|
decoration: BoxDecoration(
|
||||||
color: _currentPhotoIndex == entry.key
|
shape: BoxShape.circle,
|
||||||
? Colors.blue
|
color:
|
||||||
: Colors.grey[400],
|
_currentPhotoIndex == entry.key
|
||||||
),
|
? AppColors.primary
|
||||||
);
|
: Colors.grey[400],
|
||||||
}).toList(),
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:signature/signature.dart';
|
import 'package:signature/signature.dart';
|
||||||
|
import '../app_theme.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../models/tasks/signature_task.dart';
|
import '../models/tasks/signature_task.dart';
|
||||||
import '../widgets/offline_banner.dart';
|
import '../widgets/offline_banner.dart';
|
||||||
|
|
||||||
class SignatureCaptureScreen extends StatefulWidget {
|
class SignatureCaptureScreen extends StatefulWidget {
|
||||||
final SignatureTask task;
|
final SignatureTask task;
|
||||||
final void Function(String svg) onSignatureCompleted;
|
final void Function(String svg, String note) onSignatureCompleted;
|
||||||
|
|
||||||
const SignatureCaptureScreen({
|
const SignatureCaptureScreen({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -23,6 +23,7 @@ class SignatureCaptureScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
|
class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
|
||||||
late final SignatureController _controller;
|
late final SignatureController _controller;
|
||||||
|
final TextEditingController _noteController = TextEditingController();
|
||||||
bool _hasSignature = false;
|
bool _hasSignature = false;
|
||||||
bool _isMobilePlatform = false;
|
bool _isMobilePlatform = false;
|
||||||
|
|
||||||
@@ -84,11 +85,16 @@ class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.removeListener(_onSignatureChanged);
|
_controller.removeListener(_onSignatureChanged);
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
|
_noteController.dispose();
|
||||||
_restoreOrientation();
|
_restoreOrientation();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _buildSvgFromPoints(List<Point?> points, {double strokeWidth = 3.0, String strokeColor = '#000000'}) {
|
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
|
// Convert collected signature points (with null separators for stroke breaks) into an SVG string
|
||||||
// Determine bounds
|
// Determine bounds
|
||||||
double? minX, minY, maxX, maxY;
|
double? minX, minY, maxX, maxY;
|
||||||
@@ -130,7 +136,8 @@ class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>';
|
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;
|
return svg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,25 +148,30 @@ class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
|
|||||||
if (!hasAnyPoint) {
|
if (!hasAnyPoint) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(AppLocalizations.of(context).signatureRequired)),
|
SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context).signatureRequired),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build SVG from the captured signature points
|
// Build SVG from the captured signature points
|
||||||
final String svg = _buildSvgFromPoints(_controller.points);
|
final String svg = _buildSvgFromPoints(_controller.points);
|
||||||
|
final String note = _noteController.text.trim();
|
||||||
|
|
||||||
// Close this screen first to show the updated TaskView quickly
|
// Close this screen first to show the updated TaskView quickly
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_restoreOrientation();
|
_restoreOrientation();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
// Then notify the caller (SVG only)
|
// Then notify the caller (SVG + Bemerkung)
|
||||||
widget.onSignatureCompleted(svg);
|
widget.onSignatureCompleted(svg, note);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('${AppLocalizations.of(context).signatureError}: $e')),
|
SnackBar(
|
||||||
|
content: Text('${AppLocalizations.of(context).signatureError}: $e'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,7 +181,6 @@ class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(AppLocalizations.of(context).signatureCapture),
|
title: Text(AppLocalizations.of(context).signatureCapture),
|
||||||
backgroundColor: Colors.deepPurple[100],
|
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -197,61 +208,71 @@ class _SignatureCaptureScreenState extends State<SignatureCaptureScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).signatureInstruction,
|
AppLocalizations.of(context).signatureInstruction,
|
||||||
style: TextStyle(color: Colors.grey[700]),
|
style: const TextStyle(color: AppColors.textMuted),
|
||||||
),
|
|
||||||
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: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.surface,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: AppColors.borderStrong),
|
||||||
|
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: AppColors.surface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: _noteController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).completeTaskNote,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
minLines: 1,
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:file_selector/file_selector.dart' as file_selector;
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:votianlt_app/services/developer.dart' as developer;
|
import 'package:votianlt_app/services/developer.dart' as developer;
|
||||||
|
import '../app_theme.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
|
|
||||||
class ChatPhotoDialog extends StatefulWidget {
|
class ChatPhotoDialog extends StatefulWidget {
|
||||||
@@ -278,7 +279,7 @@ class _ChatPhotoDialogState extends State<ChatPhotoDialog> {
|
|||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.warning, color: Colors.orange[700], size: 40),
|
const Icon(Icons.warning, color: AppColors.warning, size: 40),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(_errorMessage!, textAlign: TextAlign.center),
|
Text(_errorMessage!, textAlign: TextAlign.center),
|
||||||
],
|
],
|
||||||
@@ -330,11 +331,7 @@ class _ChatPhotoDialogState extends State<ChatPhotoDialog> {
|
|||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Icons.photo_camera_back, color: AppColors.primary, size: 48),
|
||||||
Icons.photo_camera_back,
|
|
||||||
color: Colors.deepPurple[400],
|
|
||||||
size: 48,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text(
|
const Text(
|
||||||
'Wähle ein Foto von deinem Gerät aus.',
|
'Wähle ein Foto von deinem Gerät aus.',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:votianlt_app/services/developer.dart' as developer;
|
import 'package:votianlt_app/services/developer.dart' as developer;
|
||||||
import 'package:votianlt_app/services/websocket_service.dart';
|
import 'package:votianlt_app/services/websocket_service.dart';
|
||||||
import 'package:votianlt_app/services/dart_mq.dart';
|
import 'package:votianlt_app/services/dart_mq.dart';
|
||||||
|
import '../app_theme.dart';
|
||||||
|
|
||||||
class OfflineBanner extends StatefulWidget {
|
class OfflineBanner extends StatefulWidget {
|
||||||
const OfflineBanner({super.key});
|
const OfflineBanner({super.key});
|
||||||
@@ -24,8 +25,13 @@ class _OfflineBannerState extends State<OfflineBanner> {
|
|||||||
// Check if we're already connected (e.g., coming back to this screen)
|
// Check if we're already connected (e.g., coming back to this screen)
|
||||||
_hadConnection = _stompService.isConnected && _stompService.isAuthenticated;
|
_hadConnection = _stompService.isConnected && _stompService.isAuthenticated;
|
||||||
// Initialize countdown based on current connection state
|
// Initialize countdown based on current connection state
|
||||||
_onConnectionChange(_stompService.isConnected && _stompService.isAuthenticated);
|
_onConnectionChange(
|
||||||
_connSub = DartMQ().subscribe<bool>(MQTopics.connectionStatus, _onConnectionChange);
|
_stompService.isConnected && _stompService.isAuthenticated,
|
||||||
|
);
|
||||||
|
_connSub = DartMQ().subscribe<bool>(
|
||||||
|
MQTopics.connectionStatus,
|
||||||
|
_onConnectionChange,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onConnectionChange(bool isConnected) {
|
void _onConnectionChange(bool isConnected) {
|
||||||
@@ -68,7 +74,10 @@ class _OfflineBannerState extends State<OfflineBanner> {
|
|||||||
// Only auto-reconnect if we already know the target; discovery remains user-initiated
|
// Only auto-reconnect if we already know the target; discovery remains user-initiated
|
||||||
await _stompService.connect();
|
await _stompService.connect();
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
developer.log('Auto-reconnect attempt failed: $e', name: 'OfflineBanner');
|
developer.log(
|
||||||
|
'Auto-reconnect attempt failed: $e',
|
||||||
|
name: 'OfflineBanner',
|
||||||
|
);
|
||||||
developer.log('Stack trace: $stackTrace', name: 'OfflineBanner');
|
developer.log('Stack trace: $stackTrace', name: 'OfflineBanner');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,19 +123,19 @@ class _OfflineBannerState extends State<OfflineBanner> {
|
|||||||
title = 'Offline – Verbindung verloren';
|
title = 'Offline – Verbindung verloren';
|
||||||
subtitle = 'Verbindung wird wiederhergestellt.';
|
subtitle = 'Verbindung wird wiederhergestellt.';
|
||||||
icon = Icons.wifi_off;
|
icon = Icons.wifi_off;
|
||||||
bgColor = Colors.red[50];
|
bgColor = AppColors.dangerSoft;
|
||||||
iconColor = Colors.red[700];
|
iconColor = AppColors.danger;
|
||||||
titleColor = Colors.red[900];
|
titleColor = AppColors.danger;
|
||||||
subtitleColor = Colors.red[800];
|
subtitleColor = AppColors.danger.withValues(alpha: 0.85);
|
||||||
} else {
|
} else {
|
||||||
// Initial connection attempt
|
// Initial connection attempt
|
||||||
title = 'Verbinde mit Server...';
|
title = 'Verbinde mit Server...';
|
||||||
subtitle = 'Bitte warten.';
|
subtitle = 'Bitte warten.';
|
||||||
icon = Icons.sync;
|
icon = Icons.sync;
|
||||||
bgColor = Colors.orange[50];
|
bgColor = AppColors.warningSoft;
|
||||||
iconColor = Colors.orange[700];
|
iconColor = AppColors.warning;
|
||||||
titleColor = Colors.orange[900];
|
titleColor = AppColors.warning;
|
||||||
subtitleColor = Colors.orange[800];
|
subtitleColor = AppColors.warning.withValues(alpha: 0.85);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
@@ -152,10 +161,7 @@ class _OfflineBannerState extends State<OfflineBanner> {
|
|||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
style: TextStyle(
|
style: TextStyle(color: subtitleColor, fontSize: 12),
|
||||||
color: subtitleColor,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -165,4 +171,3 @@ class _OfflineBannerState extends State<OfflineBanner> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.9.15+1
|
version: 0.9.16+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.0
|
sdk: ^3.7.0
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
readonly SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
readonly SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
readonly REGISTRY_IMAGE="registry.assecutor.org/votianlt"
|
readonly REGISTRY_IMAGE="registry.assecutor.org/votianlt"
|
||||||
readonly BACKEND_DIR="${SCRIPT_DIR}/backend"
|
readonly BACKEND_DIR="${SCRIPT_DIR}"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>0.9.15</revision>
|
<revision>0.9.16</revision>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
|||||||
Binary file not shown.
@@ -346,11 +346,14 @@ window.initProfileInvoiceGenerator = function() {
|
|||||||
// Nettobetrag column header (right-aligned)
|
// Nettobetrag column header (right-aligned)
|
||||||
ctx.fillText('Nettobetrag', colNetX + colNetWidth - padding, y + rowHeight / 2);
|
ctx.fillText('Nettobetrag', colNetX + colNetWidth - padding, y + rowHeight / 2);
|
||||||
|
|
||||||
|
var vatRate = (window.profileInvoiceVatRate != null) ? window.profileInvoiceVatRate : 0.19;
|
||||||
|
var vatPctLabel = (Math.round(vatRate * 10000) / 100).toString().replace('.', ',') + '%';
|
||||||
|
|
||||||
// Sample data rows (placeholder)
|
// Sample data rows (placeholder)
|
||||||
var sampleData = [
|
var sampleData = [
|
||||||
{ name: 'Umzugsleistung inkl. Verpackung', vat: '19%', net: '450,00 €' },
|
{ name: 'Umzugsleistung inkl. Verpackung', vat: vatPctLabel, net: '450,00 €' },
|
||||||
{ name: 'Entsorgung Möbel', vat: '19%', net: '85,00 €' },
|
{ name: 'Entsorgung Möbel', vat: vatPctLabel, net: '85,00 €' },
|
||||||
{ name: 'Montage/De-Montage', vat: '19%', net: '120,00 €' }
|
{ name: 'Montage/De-Montage', vat: vatPctLabel, net: '120,00 €' }
|
||||||
];
|
];
|
||||||
|
|
||||||
var currentY = y + rowHeight;
|
var currentY = y + rowHeight;
|
||||||
@@ -415,9 +418,8 @@ window.initProfileInvoiceGenerator = function() {
|
|||||||
|
|
||||||
// Calculate totals from sample data
|
// Calculate totals from sample data
|
||||||
var netTotal = 655.00; // 450 + 85 + 120
|
var netTotal = 655.00; // 450 + 85 + 120
|
||||||
var vatRate = 0.19;
|
var vatTotal = netTotal * vatRate;
|
||||||
var vatTotal = 124.45; // 655 * 0.19
|
var grossTotal = netTotal + vatTotal;
|
||||||
var grossTotal = 779.45; // 655 + 124.45
|
|
||||||
|
|
||||||
// Draw summary lines
|
// Draw summary lines
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
@@ -435,7 +437,7 @@ window.initProfileInvoiceGenerator = function() {
|
|||||||
// Umsatzsteuer - label left, value right
|
// Umsatzsteuer - label left, value right
|
||||||
ctx.font = fontSize + 'px Arial';
|
ctx.font = fontSize + 'px Arial';
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.fillText('zzgl. 19% USt:', labelX, summaryY + summaryRowHeight / 2);
|
ctx.fillText('zzgl. ' + vatPctLabel + ' USt:', labelX, summaryY + summaryRowHeight / 2);
|
||||||
ctx.font = 'bold ' + fontSize + 'px Arial';
|
ctx.font = 'bold ' + fontSize + 'px Arial';
|
||||||
ctx.textAlign = 'right';
|
ctx.textAlign = 'right';
|
||||||
ctx.fillText(vatTotal.toFixed(2).replace('.', ',') + ' €', valueX, summaryY + summaryRowHeight / 2);
|
ctx.fillText(vatTotal.toFixed(2).replace('.', ',') + ' €', valueX, summaryY + summaryRowHeight / 2);
|
||||||
@@ -1135,6 +1137,12 @@ window.initProfileInvoiceGenerator = function() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.updateProfileVatRate = function(rate) {
|
||||||
|
if (rate == null || isNaN(rate)) return;
|
||||||
|
window.profileInvoiceVatRate = rate;
|
||||||
|
draw();
|
||||||
|
};
|
||||||
|
|
||||||
window.updateProfileMasterdataValue = function(key, value) {
|
window.updateProfileMasterdataValue = function(key, value) {
|
||||||
if (!window.masterdataValues) window.masterdataValues = {};
|
if (!window.masterdataValues) window.masterdataValues = {};
|
||||||
window.masterdataValues[key] = value;
|
window.masterdataValues[key] = value;
|
||||||
|
|||||||
@@ -1068,6 +1068,7 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background: rgba(255, 255, 255, 0.84);
|
background: rgba(255, 255, 255, 0.84);
|
||||||
box-shadow: var(--app-shadow-sm);
|
box-shadow: var(--app-shadow-sm);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.route-card,
|
.route-card,
|
||||||
@@ -1095,6 +1096,7 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
box-shadow: var(--app-shadow-sm);
|
box-shadow: var(--app-shadow-sm);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-card,
|
.detail-card,
|
||||||
@@ -1143,6 +1145,8 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
|
|||||||
|
|
||||||
.dialog-task-card {
|
.dialog-task-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding-top: calc(var(--lumo-space-m) + 5px) !important;
|
||||||
|
gap: calc(var(--lumo-space-m) / 16) !important;
|
||||||
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1152,14 +1156,112 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
|
|||||||
border-color: rgba(37, 99, 235, 0.24);
|
border-color: rgba(37, 99, 235, 0.24);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-floating-delete {
|
.dialog-task-card.drag-over-top::before,
|
||||||
|
.dialog-task-card.drag-over-bottom::after {
|
||||||
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.65rem;
|
left: 0;
|
||||||
right: 0.65rem;
|
right: 0;
|
||||||
z-index: 10;
|
height: 3px;
|
||||||
|
background: var(--lumo-primary-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 0 6px rgba(37, 99, 235, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-task-card.drag-over-top::before {
|
||||||
|
top: calc(-0.5 * var(--lumo-space-m) - 1.5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-task-card.drag-over-bottom::after {
|
||||||
|
bottom: calc(-0.5 * var(--lumo-space-m) - 1.5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-task-card.dragging {
|
||||||
|
opacity: 0.35;
|
||||||
|
transform: scale(0.96);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compressed cards during reorder drag */
|
||||||
|
.tasks-reordering .dialog-task-card {
|
||||||
|
padding: 0.4rem 0.8rem !important;
|
||||||
|
transition: padding 0.2s ease, max-height 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks-reordering .dialog-task-card:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks-reordering .dialog-task-config,
|
||||||
|
.tasks-reordering .dialog-floating-delete,
|
||||||
|
.tasks-reordering .dialog-task-drag-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks-reordering .dialog-task-card vaadin-combo-box {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-task-summary {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: var(--lumo-font-size-s);
|
||||||
|
color: var(--lumo-body-text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-task-summary .task-type-label {
|
||||||
|
color: var(--lumo-primary-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-task-summary .task-desc-label {
|
||||||
|
color: var(--lumo-secondary-text-color);
|
||||||
|
font-weight: 400;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks-reordering .dialog-task-summary {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-task-card[draggable="true"] {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-task-drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
color: var(--lumo-secondary-text-color);
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
min-width: 1.7rem;
|
min-width: 1.7rem;
|
||||||
min-height: 1.7rem;
|
min-height: 1.7rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
left: 15px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-task-drag-handle:hover {
|
||||||
|
color: var(--lumo-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-floating-delete {
|
||||||
|
padding: 0.2rem;
|
||||||
|
min-width: 1.7rem;
|
||||||
|
min-height: 1.7rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
right: 15px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-caption,
|
.inline-caption,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import de.assecutor.votianlt.model.AppUser;
|
|||||||
import de.assecutor.votianlt.model.CargoItem;
|
import de.assecutor.votianlt.model.CargoItem;
|
||||||
import de.assecutor.votianlt.model.Job;
|
import de.assecutor.votianlt.model.Job;
|
||||||
import de.assecutor.votianlt.model.task.BaseTask;
|
import de.assecutor.votianlt.model.task.BaseTask;
|
||||||
|
import de.assecutor.votianlt.model.task.SignatureTask;
|
||||||
import de.assecutor.votianlt.pages.service.AppUserService;
|
import de.assecutor.votianlt.pages.service.AppUserService;
|
||||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||||
import de.assecutor.votianlt.repository.CargoItemRepository;
|
import de.assecutor.votianlt.repository.CargoItemRepository;
|
||||||
@@ -133,6 +134,14 @@ public class MessageController {
|
|||||||
List<JobWithRelatedDataDTO> jobsWithRelatedData = assignedJobs.stream().map(job -> {
|
List<JobWithRelatedDataDTO> jobsWithRelatedData = assignedJobs.stream().map(job -> {
|
||||||
List<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId());
|
List<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId());
|
||||||
List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);
|
List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);
|
||||||
|
for (BaseTask task : tasks) {
|
||||||
|
if (task instanceof SignatureTask signatureTask && task.getId() != null) {
|
||||||
|
List<Signature> signatures = signatureRepository.findByTaskIdOrderByCreatedAtDesc(task.getId());
|
||||||
|
if (!signatures.isEmpty()) {
|
||||||
|
signatureTask.setNote(signatures.get(0).getNote());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return new JobWithRelatedDataDTO(job, cargoItems, tasks);
|
return new JobWithRelatedDataDTO(job, cargoItems, tasks);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
@@ -246,13 +255,18 @@ public class MessageController {
|
|||||||
Object extra = payload.get("extraData");
|
Object extra = payload.get("extraData");
|
||||||
if (extra instanceof Map<?, ?> extraData) {
|
if (extra instanceof Map<?, ?> extraData) {
|
||||||
Object signatureSvgObj = extraData.get("signatureSvg");
|
Object signatureSvgObj = extraData.get("signatureSvg");
|
||||||
|
Object signatureNoteObj = extraData.get("signatureNote");
|
||||||
|
String signatureNote = signatureNoteObj instanceof String s ? s : null;
|
||||||
if (signatureSvgObj instanceof String signatureSvg) {
|
if (signatureSvgObj instanceof String signatureSvg) {
|
||||||
if (!signatureSvg.isBlank()) {
|
if (!signatureSvg.isBlank()) {
|
||||||
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
|
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
|
||||||
Signature signatureEntry = new Signature(new ObjectId(taskId.toString()), signatureSvg,
|
Signature signatureEntry = new Signature(new ObjectId(taskId.toString()), signatureSvg,
|
||||||
completedBy);
|
signatureNote, completedBy);
|
||||||
signatureRepository.save(signatureEntry);
|
signatureRepository.save(signatureEntry);
|
||||||
extraDataSummary = "Unterschrift erfasst (SVG, " + signatureSvg.length() + " Zeichen)";
|
extraDataSummary = "Unterschrift erfasst (SVG, " + signatureSvg.length() + " Zeichen)";
|
||||||
|
if (signatureNote != null && !signatureNote.isBlank()) {
|
||||||
|
extraDataSummary += ", Bemerkung: " + signatureNote;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
extraDataSummary = "Leere Unterschrift";
|
extraDataSummary = "Leere Unterschrift";
|
||||||
}
|
}
|
||||||
@@ -375,7 +389,9 @@ public class MessageController {
|
|||||||
String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown";
|
String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown";
|
||||||
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
|
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
|
||||||
emailService.sendTaskCompletionNotification(jobId, taskType, taskIdStr, completedBy);
|
emailService.sendTaskCompletionNotification(jobId, taskType, taskIdStr, completedBy);
|
||||||
checkAndHandleJobCompletion(jobId, completedBy);
|
// Job completion is no longer auto-triggered by task completion.
|
||||||
|
// It is now driven by explicit station_completed messages from the app
|
||||||
|
// (see handleStationCompleted).
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Ignore email notification errors
|
// Ignore email notification errors
|
||||||
}
|
}
|
||||||
@@ -430,6 +446,47 @@ public class MessageController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle station completion message from app. Client sends to
|
||||||
|
* /server/station_completed with payload:
|
||||||
|
* {
|
||||||
|
* "jobId": "jobnum:ABC123",
|
||||||
|
* "jobNumber": "ABC123",
|
||||||
|
* "stationOrder": 0,
|
||||||
|
* "completedAt": "2026-04-13T12:34:56.789Z",
|
||||||
|
* "hasIncompleteOptionalTasks": false
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* The job is marked as completed once this message is received and all
|
||||||
|
* mandatory tasks across all stations are completed.
|
||||||
|
*/
|
||||||
|
public void handleStationCompleted(String appUserId, Map<String, Object> payload) {
|
||||||
|
try {
|
||||||
|
String jobNumber = payload.get("jobNumber") != null ? payload.get("jobNumber").toString() : null;
|
||||||
|
if (jobNumber == null || jobNumber.isBlank()) {
|
||||||
|
log.warn("[STATION] station_completed without jobNumber");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<Job> jobOpt = jobRepository.findByJobNumber(jobNumber);
|
||||||
|
if (jobOpt.isEmpty()) {
|
||||||
|
log.warn("[STATION] Job with jobNumber {} not found", jobNumber);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectId jobId = jobOpt.get().getId();
|
||||||
|
String completedBy = appUserId != null ? appUserId : "Unknown";
|
||||||
|
|
||||||
|
log.info("[STATION] station_completed received for jobNumber={}, stationOrder={}", jobNumber,
|
||||||
|
payload.get("stationOrder"));
|
||||||
|
|
||||||
|
checkAndHandleJobCompletion(jobId, completedBy);
|
||||||
|
jobUpdateBroadcaster.broadcast(jobId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[STATION] Error handling station_completed: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle incoming message from a client via WebSocket. Client sends to
|
* Handle incoming message from a client via WebSocket. Client sends to
|
||||||
* /server/message with payload: { "content": "message payload", "contentType":
|
* /server/message with payload: { "content": "message payload", "contentType":
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ public class MessagingConfig {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Station completion handler — marks a job as completed once all mandatory
|
||||||
|
// tasks have been finished and the app confirms the station is done.
|
||||||
|
webSocketService.registerMessageHandler("station_completed", (appUserId, payload) -> {
|
||||||
|
handlePayload(payload, payloadMap -> {
|
||||||
|
messageController.handleStationCompleted(appUserId, payloadMap);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Chat message handler
|
// Chat message handler
|
||||||
webSocketService.registerMessageHandler("message", (appUserId, payload) -> {
|
webSocketService.registerMessageHandler("message", (appUserId, payload) -> {
|
||||||
handlePayload(payload, payloadMap -> {
|
handlePayload(payload, payloadMap -> {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package de.assecutor.votianlt.messaging;
|
package de.assecutor.votianlt.messaging;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||||
|
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
|
||||||
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
|
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,6 +25,12 @@ public class WebSocketConfig implements WebSocketConfigurer {
|
|||||||
@Value("${app.messaging.websocket.allowed-origins:*}")
|
@Value("${app.messaging.websocket.allowed-origins:*}")
|
||||||
private String allowedOrigins;
|
private String allowedOrigins;
|
||||||
|
|
||||||
|
@Value("${app.messaging.websocket.max-text-message-size:10485760}")
|
||||||
|
private int maxTextMessageSize;
|
||||||
|
|
||||||
|
@Value("${app.messaging.websocket.max-session-idle-timeout:300000}")
|
||||||
|
private long maxSessionIdleTimeout;
|
||||||
|
|
||||||
public WebSocketConfig(WebSocketService webSocketService) {
|
public WebSocketConfig(WebSocketService webSocketService) {
|
||||||
this.webSocketService = webSocketService;
|
this.webSocketService = webSocketService;
|
||||||
}
|
}
|
||||||
@@ -32,4 +40,13 @@ public class WebSocketConfig implements WebSocketConfigurer {
|
|||||||
registry.addHandler(webSocketService, wsPath).setAllowedOrigins(allowedOrigins.split(","))
|
registry.addHandler(webSocketService, wsPath).setAllowedOrigins(allowedOrigins.split(","))
|
||||||
.addInterceptors(new HttpSessionHandshakeInterceptor());
|
.addInterceptors(new HttpSessionHandshakeInterceptor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ServletServerContainerFactoryBean createWebSocketContainer() {
|
||||||
|
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
|
||||||
|
container.setMaxTextMessageBufferSize(maxTextMessageSize);
|
||||||
|
container.setMaxBinaryMessageBufferSize(maxTextMessageSize);
|
||||||
|
container.setMaxSessionIdleTimeout(maxSessionIdleTimeout);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package de.assecutor.votianlt.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Document(collection = "misc")
|
||||||
|
public class Counter {
|
||||||
|
@Id
|
||||||
|
private String id;
|
||||||
|
private long sequence;
|
||||||
|
}
|
||||||
@@ -53,4 +53,10 @@ public class Customer {
|
|||||||
|
|
||||||
@Field("owner")
|
@Field("owner")
|
||||||
private ObjectId owner;
|
private ObjectId owner;
|
||||||
|
|
||||||
|
@Field("internal")
|
||||||
|
private boolean internal;
|
||||||
|
|
||||||
|
@Field("usrId")
|
||||||
|
private Integer usrId;
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@ public class Signature {
|
|||||||
|
|
||||||
private ObjectId taskId;
|
private ObjectId taskId;
|
||||||
private String signatureSvg;
|
private String signatureSvg;
|
||||||
|
private String note;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private String completedBy;
|
private String completedBy;
|
||||||
|
|
||||||
@@ -35,4 +36,9 @@ public class Signature {
|
|||||||
this.signatureSvg = signatureSvg;
|
this.signatureSvg = signatureSvg;
|
||||||
this.completedBy = completedBy;
|
this.completedBy = completedBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Signature(ObjectId taskId, String signatureSvg, String note, String completedBy) {
|
||||||
|
this(taskId, signatureSvg, completedBy);
|
||||||
|
this.note = note;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import org.springframework.data.mongodb.core.mapping.Document;
|
|||||||
import org.springframework.data.mongodb.core.mapping.Field;
|
import org.springframework.data.mongodb.core.mapping.Field;
|
||||||
import org.springframework.data.mongodb.core.index.Indexed;
|
import org.springframework.data.mongodb.core.index.Indexed;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -68,4 +69,7 @@ public class User {
|
|||||||
// Spracheinstellung (standardmäßig Deutsch)
|
// Spracheinstellung (standardmäßig Deutsch)
|
||||||
@Field("language")
|
@Field("language")
|
||||||
private Language language = Language.DE;
|
private Language language = Language.DE;
|
||||||
|
|
||||||
|
// Umsatzsteuer-Satz (als Dezimalwert, z.B. 0.19 für 19 %)
|
||||||
|
private BigDecimal vatRate = new BigDecimal("0.19");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
package de.assecutor.votianlt.model.task;
|
package de.assecutor.votianlt.model.task;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.Transient;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class SignatureTask extends BaseTask {
|
public class SignatureTask extends BaseTask {
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
@JsonIgnore
|
||||||
|
private String note;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getTaskType() {
|
public String getTaskType() {
|
||||||
return "SIGNATURE";
|
return "SIGNATURE";
|
||||||
@@ -21,11 +27,17 @@ public class SignatureTask extends BaseTask {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getTaskSpecificData() {
|
public Object getTaskSpecificData() {
|
||||||
return new TaskSpecificData();
|
return new TaskSpecificData(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
public class TaskSpecificData {
|
public class TaskSpecificData {
|
||||||
public String taskType = getTaskType();
|
public String taskType = getTaskType();
|
||||||
// No specific data for signature task
|
public String note;
|
||||||
|
|
||||||
|
public TaskSpecificData(String note) {
|
||||||
|
this.note = note;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import com.vaadin.flow.component.button.ButtonVariant;
|
|||||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||||
import com.vaadin.flow.component.combobox.ComboBox;
|
import com.vaadin.flow.component.combobox.ComboBox;
|
||||||
import com.vaadin.flow.component.dialog.Dialog;
|
import com.vaadin.flow.component.dialog.Dialog;
|
||||||
|
import com.vaadin.flow.component.dnd.DragSource;
|
||||||
|
import com.vaadin.flow.component.dnd.DropEffect;
|
||||||
|
import com.vaadin.flow.component.dnd.DropTarget;
|
||||||
|
import com.vaadin.flow.component.dnd.EffectAllowed;
|
||||||
import com.vaadin.flow.component.html.Div;
|
import com.vaadin.flow.component.html.Div;
|
||||||
import com.vaadin.flow.component.html.H3;
|
import com.vaadin.flow.component.html.H3;
|
||||||
import com.vaadin.flow.component.html.Span;
|
import com.vaadin.flow.component.html.Span;
|
||||||
@@ -52,10 +56,28 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
private String zip;
|
private String zip;
|
||||||
private String city;
|
private String city;
|
||||||
private boolean saveAddress;
|
private boolean saveAddress;
|
||||||
|
private boolean addressDiffersFromCustomer;
|
||||||
|
private org.bson.types.ObjectId customerId;
|
||||||
|
|
||||||
|
public boolean isAddressDiffersFromCustomer() {
|
||||||
|
return addressDiffersFromCustomer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddressDiffersFromCustomer(boolean addressDiffersFromCustomer) {
|
||||||
|
this.addressDiffersFromCustomer = addressDiffersFromCustomer;
|
||||||
|
}
|
||||||
private List<BaseTask> tasks = new ArrayList<>();
|
private List<BaseTask> tasks = new ArrayList<>();
|
||||||
private boolean addressValidatedByGoogle;
|
private boolean addressValidatedByGoogle;
|
||||||
private AddressValidationResult addressValidationResult;
|
private AddressValidationResult addressValidationResult;
|
||||||
|
|
||||||
|
public org.bson.types.ObjectId getCustomerId() {
|
||||||
|
return customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomerId(org.bson.types.ObjectId customerId) {
|
||||||
|
this.customerId = customerId;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isAddressValidatedByGoogle() {
|
public boolean isAddressValidatedByGoogle() {
|
||||||
return addressValidatedByGoogle;
|
return addressValidatedByGoogle;
|
||||||
}
|
}
|
||||||
@@ -203,12 +225,14 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
|
|
||||||
private final List<BaseTask> tasksState = new ArrayList<>();
|
private final List<BaseTask> tasksState = new ArrayList<>();
|
||||||
private VerticalLayout tasksList;
|
private VerticalLayout tasksList;
|
||||||
|
private VerticalLayout draggedTaskContainer;
|
||||||
|
|
||||||
private Span addressTabError;
|
private Span addressTabError;
|
||||||
private Span tasksTabError;
|
private Span tasksTabError;
|
||||||
|
|
||||||
private final DeliveryStationTile.TranslationHelper translationHelper;
|
private final DeliveryStationTile.TranslationHelper translationHelper;
|
||||||
private final java.util.Map<String, Customer> companyAddressOptions = new java.util.LinkedHashMap<>();
|
private final java.util.Map<String, Customer> companyAddressOptions = new java.util.LinkedHashMap<>();
|
||||||
|
private org.bson.types.ObjectId selectedCustomerId;
|
||||||
|
|
||||||
public DeliveryStationDialog(String dialogTitle, List<Customer> customers,
|
public DeliveryStationDialog(String dialogTitle, List<Customer> customers,
|
||||||
DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener,
|
DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener,
|
||||||
@@ -507,6 +531,13 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
zip.setValue(data.getZip());
|
zip.setValue(data.getZip());
|
||||||
if (data.getCity() != null)
|
if (data.getCity() != null)
|
||||||
city.setValue(data.getCity());
|
city.setValue(data.getCity());
|
||||||
|
selectedCustomerId = data.getCustomerId();
|
||||||
|
if (selectedCustomerId == null && customerSelectedFromOptions) {
|
||||||
|
Customer matched = companyAddressOptions.get(companyOption);
|
||||||
|
if (matched != null) {
|
||||||
|
selectedCustomerId = matched.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
saveAddress.setValue(customerSelectedFromOptions ? false : data.isSaveAddress());
|
saveAddress.setValue(customerSelectedFromOptions ? false : data.isSaveAddress());
|
||||||
updateSaveAddressState();
|
updateSaveAddressState();
|
||||||
|
|
||||||
@@ -543,10 +574,43 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
data.setZip(zip.getValue());
|
data.setZip(zip.getValue());
|
||||||
data.setCity(city.getValue());
|
data.setCity(city.getValue());
|
||||||
data.setSaveAddress(saveAddress.getValue());
|
data.setSaveAddress(saveAddress.getValue());
|
||||||
|
data.setCustomerId(selectedCustomerId);
|
||||||
|
data.setAddressDiffersFromCustomer(computeAddressDiffers());
|
||||||
data.setTasks(new ArrayList<>(tasksState));
|
data.setTasks(new ArrayList<>(tasksState));
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean computeAddressDiffers() {
|
||||||
|
boolean hasAnyValue = !isBlank(resolveCompanyValue(company.getValue())) || !isBlank(firstName.getValue())
|
||||||
|
|| !isBlank(lastName.getValue()) || !isBlank(phone.getValue()) || !isBlank(mail.getValue())
|
||||||
|
|| !isBlank(street.getValue()) || !isBlank(houseNumber.getValue())
|
||||||
|
|| !isBlank(addressAddition.getValue()) || !isBlank(zip.getValue()) || !isBlank(city.getValue());
|
||||||
|
if (!hasAnyValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (selectedCustomerId == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Customer linked = findCustomerById(selectedCustomerId);
|
||||||
|
return linked == null || !matchesCurrentCustomer(linked);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Customer findCustomerById(org.bson.types.ObjectId id) {
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (Customer c : companyAddressOptions.values()) {
|
||||||
|
if (c != null && id.equals(c.getId())) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBlank(String value) {
|
||||||
|
return value == null || value.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
private boolean validateRequiredFields() {
|
private boolean validateRequiredFields() {
|
||||||
// Address tab validation
|
// Address tab validation
|
||||||
boolean addressValid = true;
|
boolean addressValid = true;
|
||||||
@@ -596,11 +660,9 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
String value = mail.getValue();
|
String value = mail.getValue();
|
||||||
String normalizedValue = value == null ? "" : value.trim();
|
String normalizedValue = value == null ? "" : value.trim();
|
||||||
boolean empty = normalizedValue.isEmpty();
|
boolean empty = normalizedValue.isEmpty();
|
||||||
boolean required = Boolean.TRUE.equals(saveAddress.getValue());
|
|
||||||
boolean invalid = !empty && !normalizedValue.contains("@");
|
boolean invalid = !empty && !normalizedValue.contains("@");
|
||||||
boolean hasError = invalid || (required && empty);
|
applyErrorStyling(mail, invalid);
|
||||||
applyErrorStyling(mail, hasError);
|
return !invalid;
|
||||||
return !hasError;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyErrorStyling(com.vaadin.flow.component.Component field, boolean error) {
|
private void applyErrorStyling(com.vaadin.flow.component.Component field, boolean error) {
|
||||||
@@ -643,10 +705,12 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
companyField.addValueChangeListener(event -> {
|
companyField.addValueChangeListener(event -> {
|
||||||
Customer customer = companyAddressOptions.get(event.getValue());
|
Customer customer = companyAddressOptions.get(event.getValue());
|
||||||
if (customer == null) {
|
if (customer == null) {
|
||||||
|
selectedCustomerId = null;
|
||||||
updateSaveAddressState();
|
updateSaveAddressState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectedCustomerId = customer.getId();
|
||||||
if (customer.getTitle() != null
|
if (customer.getTitle() != null
|
||||||
&& ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle())
|
&& ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle())
|
||||||
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
|
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
|
||||||
@@ -675,27 +739,34 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
|
|
||||||
companyField.addCustomValueSetListener(event -> {
|
companyField.addCustomValueSetListener(event -> {
|
||||||
companyField.setValue(event.getDetail());
|
companyField.setValue(event.getDetail());
|
||||||
|
selectedCustomerId = null;
|
||||||
updateSaveAddressState();
|
updateSaveAddressState();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSaveAddressState() {
|
private void updateSaveAddressState() {
|
||||||
Customer selectedCustomer = companyAddressOptions.get(company.getValue());
|
Customer selectedCustomer = companyAddressOptions.get(company.getValue());
|
||||||
boolean customerSelectedFromOptions = selectedCustomer != null && matchesCurrentCustomer(selectedCustomer);
|
boolean customerDataMatches = selectedCustomer != null && matchesCurrentCustomer(selectedCustomer);
|
||||||
|
|
||||||
if (customerSelectedFromOptions) {
|
if (customerDataMatches) {
|
||||||
saveAddress.setValue(false);
|
saveAddress.setValue(false);
|
||||||
saveAddress.setEnabled(false);
|
saveAddress.setEnabled(false);
|
||||||
|
saveAddress.setLabel(translationHelper.getTranslation("addjob.address.save"));
|
||||||
updateMailRequirement();
|
updateMailRequirement();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveAddress.setEnabled(true);
|
saveAddress.setEnabled(true);
|
||||||
|
if (selectedCustomerId != null) {
|
||||||
|
saveAddress.setLabel(translationHelper.getTranslation("addjob.address.update"));
|
||||||
|
} else {
|
||||||
|
saveAddress.setLabel(translationHelper.getTranslation("addjob.address.save"));
|
||||||
|
}
|
||||||
updateMailRequirement();
|
updateMailRequirement();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMailRequirement() {
|
private void updateMailRequirement() {
|
||||||
mail.setRequiredIndicatorVisible(Boolean.TRUE.equals(saveAddress.getValue()));
|
mail.setRequiredIndicatorVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildCompanyAddressLabel(Customer customer) {
|
private String buildCompanyAddressLabel(Customer customer) {
|
||||||
@@ -866,6 +937,15 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
taskContainer.setSpacing(true);
|
taskContainer.setSpacing(true);
|
||||||
taskContainer.addClassName("dialog-task-card");
|
taskContainer.addClassName("dialog-task-card");
|
||||||
|
|
||||||
|
// Drag handle
|
||||||
|
Button dragHandle = new Button(new Icon(VaadinIcon.GRID_SMALL));
|
||||||
|
dragHandle.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
dragHandle.addClassName("dialog-task-drag-handle");
|
||||||
|
|
||||||
|
// Compact summary shown during drag
|
||||||
|
HorizontalLayout summaryRow = createDragSummary("", "");
|
||||||
|
summaryRow.addClassName("dialog-task-summary");
|
||||||
|
|
||||||
// Task type selection
|
// Task type selection
|
||||||
ComboBox<TaskType> taskTypeCombo = new ComboBox<>(translationHelper.getTranslation("addjob.tasks.tasktype"));
|
ComboBox<TaskType> taskTypeCombo = new ComboBox<>(translationHelper.getTranslation("addjob.tasks.tasktype"));
|
||||||
taskTypeCombo.setItems(TaskType.values());
|
taskTypeCombo.setItems(TaskType.values());
|
||||||
@@ -877,6 +957,7 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
VerticalLayout configContainer = new VerticalLayout();
|
VerticalLayout configContainer = new VerticalLayout();
|
||||||
configContainer.setPadding(false);
|
configContainer.setPadding(false);
|
||||||
configContainer.setSpacing(true);
|
configContainer.setSpacing(true);
|
||||||
|
configContainer.addClassName("dialog-task-config");
|
||||||
|
|
||||||
// Red X button positioned in top-right corner
|
// Red X button positioned in top-right corner
|
||||||
Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
|
Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
|
||||||
@@ -884,8 +965,14 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
deleteXButton.addClassName("dialog-floating-delete");
|
deleteXButton.addClassName("dialog-floating-delete");
|
||||||
deleteXButton.addClickListener(e -> removeTaskRow(taskContainer));
|
deleteXButton.addClickListener(e -> removeTaskRow(taskContainer));
|
||||||
|
|
||||||
taskContainer.add(taskTypeCombo, configContainer);
|
HorizontalLayout headerRow = new HorizontalLayout(dragHandle, summaryRow, taskTypeCombo, deleteXButton);
|
||||||
taskContainer.add(deleteXButton);
|
headerRow.setAlignItems(FlexComponent.Alignment.START);
|
||||||
|
headerRow.setWidthFull();
|
||||||
|
headerRow.setFlexGrow(1, taskTypeCombo);
|
||||||
|
|
||||||
|
taskContainer.add(headerRow, configContainer);
|
||||||
|
|
||||||
|
setupDragAndDrop(taskContainer);
|
||||||
|
|
||||||
// Create Task and add to state with correct order
|
// Create Task and add to state with correct order
|
||||||
BaseTask task = new ConfirmationTask("");
|
BaseTask task = new ConfirmationTask("");
|
||||||
@@ -896,6 +983,7 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
|
|
||||||
taskTypeCombo.setValue(TaskType.CONFIRMATION);
|
taskTypeCombo.setValue(TaskType.CONFIRMATION);
|
||||||
updateTaskConfiguration(configContainer, currentTask[0]);
|
updateTaskConfiguration(configContainer, currentTask[0]);
|
||||||
|
updateDragSummary(summaryRow, TaskType.CONFIRMATION, task);
|
||||||
|
|
||||||
taskTypeCombo.addValueChangeListener(ev -> {
|
taskTypeCombo.addValueChangeListener(ev -> {
|
||||||
TaskType selectedType = ev.getValue();
|
TaskType selectedType = ev.getValue();
|
||||||
@@ -940,6 +1028,7 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateTaskConfiguration(configContainer, newTask);
|
updateTaskConfiguration(configContainer, newTask);
|
||||||
|
updateDragSummary(summaryRow, selectedType, newTask);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -953,6 +1042,18 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
taskContainer.setSpacing(true);
|
taskContainer.setSpacing(true);
|
||||||
taskContainer.addClassName("dialog-task-card");
|
taskContainer.addClassName("dialog-task-card");
|
||||||
|
|
||||||
|
// Drag handle
|
||||||
|
Button dragHandle = new Button(new Icon(VaadinIcon.GRID_SMALL));
|
||||||
|
dragHandle.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
dragHandle.addClassName("dialog-task-drag-handle");
|
||||||
|
|
||||||
|
// Compact summary shown during drag
|
||||||
|
TaskType initialTaskType = getTaskTypeFromTask(task);
|
||||||
|
HorizontalLayout summaryRow = createDragSummary(
|
||||||
|
initialTaskType != null ? initialTaskType.getDisplayName() : "",
|
||||||
|
task.getDescription() != null ? task.getDescription() : "");
|
||||||
|
summaryRow.addClassName("dialog-task-summary");
|
||||||
|
|
||||||
ComboBox<TaskType> taskTypeCombo = new ComboBox<>(translationHelper.getTranslation("addjob.tasks.tasktype"));
|
ComboBox<TaskType> taskTypeCombo = new ComboBox<>(translationHelper.getTranslation("addjob.tasks.tasktype"));
|
||||||
taskTypeCombo.setItems(TaskType.values());
|
taskTypeCombo.setItems(TaskType.values());
|
||||||
taskTypeCombo.setItemLabelGenerator(TaskType::getDisplayName);
|
taskTypeCombo.setItemLabelGenerator(TaskType::getDisplayName);
|
||||||
@@ -962,21 +1063,27 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
VerticalLayout configContainer = new VerticalLayout();
|
VerticalLayout configContainer = new VerticalLayout();
|
||||||
configContainer.setPadding(false);
|
configContainer.setPadding(false);
|
||||||
configContainer.setSpacing(true);
|
configContainer.setSpacing(true);
|
||||||
|
configContainer.addClassName("dialog-task-config");
|
||||||
|
|
||||||
Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
|
Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
|
||||||
deleteXButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
|
deleteXButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
|
||||||
deleteXButton.addClassName("dialog-floating-delete");
|
deleteXButton.addClassName("dialog-floating-delete");
|
||||||
deleteXButton.addClickListener(e -> removeTaskRow(taskContainer));
|
deleteXButton.addClickListener(e -> removeTaskRow(taskContainer));
|
||||||
|
|
||||||
taskContainer.add(taskTypeCombo, configContainer);
|
HorizontalLayout headerRow = new HorizontalLayout(dragHandle, summaryRow, taskTypeCombo, deleteXButton);
|
||||||
taskContainer.add(deleteXButton);
|
headerRow.setAlignItems(FlexComponent.Alignment.START);
|
||||||
|
headerRow.setWidthFull();
|
||||||
|
headerRow.setFlexGrow(1, taskTypeCombo);
|
||||||
|
|
||||||
|
taskContainer.add(headerRow, configContainer);
|
||||||
|
|
||||||
|
setupDragAndDrop(taskContainer);
|
||||||
|
|
||||||
final BaseTask[] currentTask = { task };
|
final BaseTask[] currentTask = { task };
|
||||||
|
|
||||||
// Set the combo value BEFORE registering the listener
|
// Set the combo value BEFORE registering the listener
|
||||||
TaskType taskType = getTaskTypeFromTask(task);
|
if (initialTaskType != null) {
|
||||||
if (taskType != null) {
|
taskTypeCombo.setValue(initialTaskType);
|
||||||
taskTypeCombo.setValue(taskType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the listener for user-initiated type changes only
|
// Register the listener for user-initiated type changes only
|
||||||
@@ -1021,11 +1128,13 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateTaskConfiguration(configContainer, newTask);
|
updateTaskConfiguration(configContainer, newTask);
|
||||||
|
updateDragSummary(summaryRow, selectedType, newTask);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render the UI with the loaded task
|
// Render the UI with the loaded task
|
||||||
updateTaskConfiguration(configContainer, task);
|
updateTaskConfiguration(configContainer, task);
|
||||||
|
updateDragSummary(summaryRow, initialTaskType, task);
|
||||||
|
|
||||||
tasksList.add(taskContainer);
|
tasksList.add(taskContainer);
|
||||||
updateTaskDeleteAvailability();
|
updateTaskDeleteAvailability();
|
||||||
@@ -1042,6 +1151,160 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HorizontalLayout createDragSummary(String typeName, String description) {
|
||||||
|
Span typeLabel = new Span(typeName);
|
||||||
|
typeLabel.addClassName("task-type-label");
|
||||||
|
Span descLabel = new Span(description != null && !description.isBlank() ? " — " + description : "");
|
||||||
|
descLabel.addClassName("task-desc-label");
|
||||||
|
HorizontalLayout layout = new HorizontalLayout(typeLabel, descLabel);
|
||||||
|
layout.setSpacing(false);
|
||||||
|
layout.setPadding(false);
|
||||||
|
layout.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDragSummary(HorizontalLayout summaryRow, TaskType taskType, BaseTask task) {
|
||||||
|
summaryRow.getChildren()
|
||||||
|
.filter(Span.class::isInstance)
|
||||||
|
.map(Span.class::cast)
|
||||||
|
.forEach(span -> {
|
||||||
|
if (span.getClassNames().contains("task-type-label")) {
|
||||||
|
span.setText(taskType != null ? taskType.getDisplayName() : "");
|
||||||
|
} else if (span.getClassNames().contains("task-desc-label")) {
|
||||||
|
String desc = task.getDescription();
|
||||||
|
span.setText(desc != null && !desc.isBlank() ? " — " + desc : "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAllDropIndicators() {
|
||||||
|
if (tasksList == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tasksList.removeClassName("tasks-reordering");
|
||||||
|
tasksList.getChildren()
|
||||||
|
.filter(VerticalLayout.class::isInstance)
|
||||||
|
.map(VerticalLayout.class::cast)
|
||||||
|
.forEach(c -> {
|
||||||
|
c.removeClassName("drag-over-top");
|
||||||
|
c.removeClassName("drag-over-bottom");
|
||||||
|
c.removeClassName("dragging");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupDragAndDrop(VerticalLayout taskContainer) {
|
||||||
|
DragSource<VerticalLayout> dragSource = DragSource.create(taskContainer);
|
||||||
|
dragSource.setEffectAllowed(EffectAllowed.MOVE);
|
||||||
|
dragSource.addDragStartListener(e -> {
|
||||||
|
draggedTaskContainer = taskContainer;
|
||||||
|
taskContainer.addClassName("dragging");
|
||||||
|
if (tasksList != null) {
|
||||||
|
// Update all summaries with latest description values before compressing
|
||||||
|
List<com.vaadin.flow.component.Component> rows = tasksList.getChildren().toList();
|
||||||
|
for (int i = 0; i < rows.size() && i < tasksState.size(); i++) {
|
||||||
|
if (rows.get(i) instanceof VerticalLayout row) {
|
||||||
|
row.getChildren()
|
||||||
|
.filter(HorizontalLayout.class::isInstance)
|
||||||
|
.map(HorizontalLayout.class::cast)
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(headerRow -> headerRow.getChildren()
|
||||||
|
.filter(HorizontalLayout.class::isInstance)
|
||||||
|
.map(HorizontalLayout.class::cast)
|
||||||
|
.filter(c -> c.getClassNames().contains("dialog-task-summary"))
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(summary -> {
|
||||||
|
int idx = rows.indexOf(row);
|
||||||
|
if (idx >= 0 && idx < tasksState.size()) {
|
||||||
|
BaseTask t = tasksState.get(idx);
|
||||||
|
TaskType tt = getTaskTypeFromTask(t);
|
||||||
|
updateDragSummary(summary, tt, t);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tasksList.addClassName("tasks-reordering");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dragSource.addDragEndListener(e -> {
|
||||||
|
draggedTaskContainer = null;
|
||||||
|
clearAllDropIndicators();
|
||||||
|
});
|
||||||
|
|
||||||
|
DropTarget<VerticalLayout> dropTarget = DropTarget.create(taskContainer);
|
||||||
|
dropTarget.setDropEffect(DropEffect.MOVE);
|
||||||
|
dropTarget.addDropListener(e -> {
|
||||||
|
if (draggedTaskContainer != null && draggedTaskContainer != taskContainer) {
|
||||||
|
moveTaskRow(draggedTaskContainer, taskContainer);
|
||||||
|
}
|
||||||
|
clearAllDropIndicators();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client-side drag events with position detection and no-op suppression
|
||||||
|
taskContainer.getElement().executeJs(
|
||||||
|
"const el = this;"
|
||||||
|
+ "function clearIndicators() {"
|
||||||
|
+ " var p = el.parentElement;"
|
||||||
|
+ " if (p) p.querySelectorAll('.drag-over-top, .drag-over-bottom').forEach("
|
||||||
|
+ " function(c) { c.classList.remove('drag-over-top', 'drag-over-bottom'); });"
|
||||||
|
+ "}"
|
||||||
|
+ "function getSiblings() {"
|
||||||
|
+ " return Array.from(el.parentElement.children).filter("
|
||||||
|
+ " function(c) { return c.classList.contains('dialog-task-card'); });"
|
||||||
|
+ "}"
|
||||||
|
+ "el.addEventListener('dragstart', function() {"
|
||||||
|
+ " el.parentElement.__draggedEl = el;"
|
||||||
|
+ "});"
|
||||||
|
+ "el.addEventListener('dragend', function() {"
|
||||||
|
+ " el.parentElement.__draggedEl = null;"
|
||||||
|
+ "});"
|
||||||
|
+ "el.addEventListener('dragover', function(e) {"
|
||||||
|
+ " e.preventDefault();"
|
||||||
|
+ " var dragged = el.parentElement.__draggedEl;"
|
||||||
|
+ " if (!dragged || dragged === el) { clearIndicators(); return; }"
|
||||||
|
+ " var cards = getSiblings();"
|
||||||
|
+ " var dragIdx = cards.indexOf(dragged);"
|
||||||
|
+ " var myIdx = cards.indexOf(el);"
|
||||||
|
+ " clearIndicators();"
|
||||||
|
+ " var rect = el.getBoundingClientRect();"
|
||||||
|
+ " var midY = rect.top + rect.height / 2;"
|
||||||
|
+ " if (e.clientY < midY) {"
|
||||||
|
+ " if (myIdx !== dragIdx + 1) el.classList.add('drag-over-top');"
|
||||||
|
+ " } else {"
|
||||||
|
+ " if (myIdx !== dragIdx - 1) el.classList.add('drag-over-bottom');"
|
||||||
|
+ " }"
|
||||||
|
+ "});"
|
||||||
|
+ "el.addEventListener('dragleave', function() {"
|
||||||
|
+ " el.classList.remove('drag-over-top', 'drag-over-bottom');"
|
||||||
|
+ "});"
|
||||||
|
+ "el.addEventListener('drop', function() {"
|
||||||
|
+ " clearIndicators();"
|
||||||
|
+ "});");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveTaskRow(VerticalLayout source, VerticalLayout target) {
|
||||||
|
List<com.vaadin.flow.component.Component> rows = tasksList.getChildren().toList();
|
||||||
|
int fromIndex = rows.indexOf(source);
|
||||||
|
int toIndex = rows.indexOf(target);
|
||||||
|
if (fromIndex < 0 || toIndex < 0 || fromIndex == toIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder tasksState
|
||||||
|
BaseTask movedTask = tasksState.remove(fromIndex);
|
||||||
|
tasksState.add(toIndex, movedTask);
|
||||||
|
|
||||||
|
// Reorder UI: remove all, re-add in new order
|
||||||
|
List<com.vaadin.flow.component.Component> rowList = new ArrayList<>(rows);
|
||||||
|
com.vaadin.flow.component.Component movedRow = rowList.remove(fromIndex);
|
||||||
|
rowList.add(toIndex, movedRow);
|
||||||
|
|
||||||
|
tasksList.removeAll();
|
||||||
|
rowList.forEach(tasksList::add);
|
||||||
|
|
||||||
|
// Update taskOrder values
|
||||||
|
reorderTasksAfterDeletion();
|
||||||
|
}
|
||||||
|
|
||||||
private void reorderTasksAfterDeletion() {
|
private void reorderTasksAfterDeletion() {
|
||||||
for (int i = 0; i < tasksState.size(); i++) {
|
for (int i = 0; i < tasksState.size(); i++) {
|
||||||
BaseTask task = tasksState.get(i);
|
BaseTask task = tasksState.get(i);
|
||||||
@@ -1094,11 +1357,15 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
.filter(VerticalLayout.class::isInstance)
|
.filter(VerticalLayout.class::isInstance)
|
||||||
.map(VerticalLayout.class::cast)
|
.map(VerticalLayout.class::cast)
|
||||||
.forEach(taskContainer -> taskContainer.getChildren()
|
.forEach(taskContainer -> taskContainer.getChildren()
|
||||||
.filter(Button.class::isInstance)
|
.filter(HorizontalLayout.class::isInstance)
|
||||||
.map(Button.class::cast)
|
.map(HorizontalLayout.class::cast)
|
||||||
.filter(button -> button.getClassNames().contains("dialog-floating-delete"))
|
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.ifPresent(button -> button.setEnabled(deletable)));
|
.ifPresent(headerRow -> headerRow.getChildren()
|
||||||
|
.filter(Button.class::isInstance)
|
||||||
|
.map(Button.class::cast)
|
||||||
|
.filter(button -> button.getClassNames().contains("dialog-floating-delete"))
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(button -> button.setEnabled(deletable))));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTaskConfiguration(VerticalLayout configContainer, BaseTask task) {
|
private void updateTaskConfiguration(VerticalLayout configContainer, BaseTask task) {
|
||||||
@@ -1142,10 +1409,14 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case SIGNATURE:
|
case SIGNATURE:
|
||||||
Span info = new Span(translationHelper.getTranslation("addjob.tasks.signature.noconfig"));
|
TextField signatureNoteField = new TextField(
|
||||||
info.getStyle().set("color", "var(--lumo-secondary-text-color)");
|
translationHelper.getTranslation("addjob.tasks.signature.notelabel"));
|
||||||
info.getStyle().set("font-style", "italic");
|
signatureNoteField.setPlaceholder(
|
||||||
configContainer.add(info);
|
translationHelper.getTranslation("addjob.tasks.signature.notelabel.placeholder"));
|
||||||
|
signatureNoteField.setWidthFull();
|
||||||
|
signatureNoteField.setValue(task.getDescription() != null ? task.getDescription() : "");
|
||||||
|
signatureNoteField.addValueChangeListener(ev -> task.setDescription(ev.getValue()));
|
||||||
|
configContainer.add(signatureNoteField);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TODOLIST:
|
case TODOLIST:
|
||||||
|
|||||||
@@ -228,6 +228,25 @@ public class PickupStationDialog extends Dialog {
|
|||||||
public void setCargoItems(List<CargoItem> cargoItems) {
|
public void setCargoItems(List<CargoItem> cargoItems) {
|
||||||
this.cargoItems = cargoItems != null ? cargoItems : new ArrayList<>();
|
this.cargoItems = cargoItems != null ? cargoItems : new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private org.bson.types.ObjectId customerId;
|
||||||
|
private boolean addressDiffersFromCustomer;
|
||||||
|
|
||||||
|
public org.bson.types.ObjectId getCustomerId() {
|
||||||
|
return customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomerId(org.bson.types.ObjectId customerId) {
|
||||||
|
this.customerId = customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAddressDiffersFromCustomer() {
|
||||||
|
return addressDiffersFromCustomer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddressDiffersFromCustomer(boolean addressDiffersFromCustomer) {
|
||||||
|
this.addressDiffersFromCustomer = addressDiffersFromCustomer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface SaveListener {
|
public interface SaveListener {
|
||||||
@@ -250,6 +269,7 @@ public class PickupStationDialog extends Dialog {
|
|||||||
private final ComboBox<String> customerComboBox;
|
private final ComboBox<String> customerComboBox;
|
||||||
private final Map<String, Customer> customerLabelMap = new LinkedHashMap<>();
|
private final Map<String, Customer> customerLabelMap = new LinkedHashMap<>();
|
||||||
private final Map<String, Customer> companyCustomerMap = new LinkedHashMap<>();
|
private final Map<String, Customer> companyCustomerMap = new LinkedHashMap<>();
|
||||||
|
private org.bson.types.ObjectId selectedCustomerId;
|
||||||
private DatePicker appointmentDatePicker;
|
private DatePicker appointmentDatePicker;
|
||||||
private TimePicker appointmentTimePicker;
|
private TimePicker appointmentTimePicker;
|
||||||
private Checkbox digitalProcessingCheckbox;
|
private Checkbox digitalProcessingCheckbox;
|
||||||
@@ -431,14 +451,17 @@ public class PickupStationDialog extends Dialog {
|
|||||||
customerComboBox.addValueChangeListener(ev -> {
|
customerComboBox.addValueChangeListener(ev -> {
|
||||||
String selected = ev.getValue();
|
String selected = ev.getValue();
|
||||||
if (selected == null) {
|
if (selected == null) {
|
||||||
|
selectedCustomerId = null;
|
||||||
updateSaveAddressState();
|
updateSaveAddressState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Customer c = customerLabelMap.get(selected);
|
Customer c = customerLabelMap.get(selected);
|
||||||
if (c == null) {
|
if (c == null) {
|
||||||
|
selectedCustomerId = null;
|
||||||
updateSaveAddressState();
|
updateSaveAddressState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
selectedCustomerId = c.getId();
|
||||||
if (c.getCompanyName() != null)
|
if (c.getCompanyName() != null)
|
||||||
company.setValue(c.getCompanyName());
|
company.setValue(c.getCompanyName());
|
||||||
else
|
else
|
||||||
@@ -663,6 +686,15 @@ public class PickupStationDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.getCustomerId() != null) {
|
||||||
|
selectedCustomerId = data.getCustomerId();
|
||||||
|
} else {
|
||||||
|
Customer matched = customerLabelMap.get(data.getCustomerSelection());
|
||||||
|
if (matched == null) {
|
||||||
|
matched = companyCustomerMap.get(normalizeValue(data.getCompany()));
|
||||||
|
}
|
||||||
|
selectedCustomerId = matched != null ? matched.getId() : null;
|
||||||
|
}
|
||||||
saveAddress.setValue(data.isSaveAddress());
|
saveAddress.setValue(data.isSaveAddress());
|
||||||
updateSaveAddressState();
|
updateSaveAddressState();
|
||||||
}
|
}
|
||||||
@@ -681,6 +713,8 @@ public class PickupStationDialog extends Dialog {
|
|||||||
data.setZip(zip.getValue());
|
data.setZip(zip.getValue());
|
||||||
data.setCity(city.getValue());
|
data.setCity(city.getValue());
|
||||||
data.setSaveAddress(saveAddress.getValue());
|
data.setSaveAddress(saveAddress.getValue());
|
||||||
|
data.setCustomerId(selectedCustomerId);
|
||||||
|
data.setAddressDiffersFromCustomer(computeAddressDiffers());
|
||||||
data.setCustomerSelection(customerComboBox.getValue());
|
data.setCustomerSelection(customerComboBox.getValue());
|
||||||
if (appointmentDatePicker != null) {
|
if (appointmentDatePicker != null) {
|
||||||
data.setAppointmentDate(appointmentDatePicker.getValue());
|
data.setAppointmentDate(appointmentDatePicker.getValue());
|
||||||
@@ -820,6 +854,7 @@ public class PickupStationDialog extends Dialog {
|
|||||||
companyField.addValueChangeListener(event -> {
|
companyField.addValueChangeListener(event -> {
|
||||||
String selectedCompany = event.getValue();
|
String selectedCompany = event.getValue();
|
||||||
if (selectedCompany == null || selectedCompany.trim().isEmpty()) {
|
if (selectedCompany == null || selectedCompany.trim().isEmpty()) {
|
||||||
|
selectedCustomerId = null;
|
||||||
updateSaveAddressState();
|
updateSaveAddressState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -829,6 +864,7 @@ public class PickupStationDialog extends Dialog {
|
|||||||
|
|
||||||
if (matchingCustomer.isPresent()) {
|
if (matchingCustomer.isPresent()) {
|
||||||
Customer customer = matchingCustomer.get();
|
Customer customer = matchingCustomer.get();
|
||||||
|
selectedCustomerId = customer.getId();
|
||||||
if (customer.getTitle() != null
|
if (customer.getTitle() != null
|
||||||
&& ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle())
|
&& ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle())
|
||||||
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
|
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
|
||||||
@@ -859,6 +895,7 @@ public class PickupStationDialog extends Dialog {
|
|||||||
|
|
||||||
companyField.addCustomValueSetListener(event -> {
|
companyField.addCustomValueSetListener(event -> {
|
||||||
companyField.setValue(event.getDetail());
|
companyField.setValue(event.getDetail());
|
||||||
|
selectedCustomerId = null;
|
||||||
updateSaveAddressState();
|
updateSaveAddressState();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -866,17 +903,23 @@ public class PickupStationDialog extends Dialog {
|
|||||||
private void updateSaveAddressState() {
|
private void updateSaveAddressState() {
|
||||||
Customer selectedCustomer = customerLabelMap.get(customerComboBox.getValue());
|
Customer selectedCustomer = customerLabelMap.get(customerComboBox.getValue());
|
||||||
Customer selectedCompanyCustomer = companyCustomerMap.get(normalizeValue(company.getValue()));
|
Customer selectedCompanyCustomer = companyCustomerMap.get(normalizeValue(company.getValue()));
|
||||||
boolean existingCustomerSelected = selectedCustomer != null && matchesCustomer(selectedCustomer);
|
boolean customerDataMatches = selectedCustomer != null && matchesCustomer(selectedCustomer);
|
||||||
boolean existingCompanySelected = selectedCompanyCustomer != null && matchesCustomer(selectedCompanyCustomer);
|
boolean companyDataMatches = selectedCompanyCustomer != null && matchesCustomer(selectedCompanyCustomer);
|
||||||
|
|
||||||
if (existingCustomerSelected || existingCompanySelected) {
|
if (customerDataMatches || companyDataMatches) {
|
||||||
saveAddress.setValue(false);
|
saveAddress.setValue(false);
|
||||||
saveAddress.setEnabled(false);
|
saveAddress.setEnabled(false);
|
||||||
|
saveAddress.setLabel(translationHelper.getTranslation("addjob.address.save"));
|
||||||
updateMailRequirement();
|
updateMailRequirement();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveAddress.setEnabled(true);
|
saveAddress.setEnabled(true);
|
||||||
|
if (selectedCustomerId != null) {
|
||||||
|
saveAddress.setLabel(translationHelper.getTranslation("addjob.address.update"));
|
||||||
|
} else {
|
||||||
|
saveAddress.setLabel(translationHelper.getTranslation("addjob.address.save"));
|
||||||
|
}
|
||||||
updateMailRequirement();
|
updateMailRequirement();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,6 +949,40 @@ public class PickupStationDialog extends Dialog {
|
|||||||
return value == null ? "" : value.trim();
|
return value == null ? "" : value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean computeAddressDiffers() {
|
||||||
|
boolean hasAnyValue = !normalizeValue(company.getValue()).isEmpty()
|
||||||
|
|| !normalizeValue(firstName.getValue()).isEmpty() || !normalizeValue(lastName.getValue()).isEmpty()
|
||||||
|
|| !normalizeValue(phone.getValue()).isEmpty() || !normalizeValue(mail.getValue()).isEmpty()
|
||||||
|
|| !normalizeValue(street.getValue()).isEmpty() || !normalizeValue(houseNumber.getValue()).isEmpty()
|
||||||
|
|| !normalizeValue(addressAddition.getValue()).isEmpty() || !normalizeValue(zip.getValue()).isEmpty()
|
||||||
|
|| !normalizeValue(city.getValue()).isEmpty();
|
||||||
|
if (!hasAnyValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (selectedCustomerId == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Customer linked = findCustomerById(selectedCustomerId);
|
||||||
|
return linked == null || !matchesCustomer(linked);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Customer findCustomerById(org.bson.types.ObjectId id) {
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (Customer c : customerLabelMap.values()) {
|
||||||
|
if (c != null && id.equals(c.getId())) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Customer c : companyCustomerMap.values()) {
|
||||||
|
if (c != null && id.equals(c.getId())) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Appointments & Processing Tab
|
// Appointments & Processing Tab
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ public interface CustomerRepository extends MongoRepository<Customer, ObjectId>
|
|||||||
Slice<Customer> findAllBy(Pageable pageable);
|
Slice<Customer> findAllBy(Pageable pageable);
|
||||||
|
|
||||||
List<Customer> findByOwner(ObjectId owner);
|
List<Customer> findByOwner(ObjectId owner);
|
||||||
|
|
||||||
|
List<Customer> findByOwnerAndInternalFalse(ObjectId owner);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
public class AddCustomerService {
|
public class AddCustomerService {
|
||||||
private final AddCustomerRepository addCustomerRepository;
|
private final AddCustomerRepository addCustomerRepository;
|
||||||
private final SecurityService securityService;
|
private final SecurityService securityService;
|
||||||
|
private final SequenceGeneratorService sequenceGeneratorService;
|
||||||
|
|
||||||
AddCustomerService(AddCustomerRepository addCustomerRepository, SecurityService securityService) {
|
AddCustomerService(AddCustomerRepository addCustomerRepository, SecurityService securityService,
|
||||||
|
SequenceGeneratorService sequenceGeneratorService) {
|
||||||
this.addCustomerRepository = addCustomerRepository;
|
this.addCustomerRepository = addCustomerRepository;
|
||||||
this.securityService = securityService;
|
this.securityService = securityService;
|
||||||
|
this.sequenceGeneratorService = sequenceGeneratorService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addCustomer(Customer customer) {
|
public void addCustomer(Customer customer) {
|
||||||
@@ -25,6 +28,35 @@ public class AddCustomerService {
|
|||||||
de.assecutor.votianlt.model.User currentUser = securityService.getCurrentDatabaseUser();
|
de.assecutor.votianlt.model.User currentUser = securityService.getCurrentDatabaseUser();
|
||||||
customer.setCreatedBy(currentUser.getId());
|
customer.setCreatedBy(currentUser.getId());
|
||||||
customer.setOwner(currentUser.getId());
|
customer.setOwner(currentUser.getId());
|
||||||
|
if (customer.getUsrId() == null) {
|
||||||
|
customer.setUsrId(sequenceGeneratorService.nextCustomerNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
addCustomerRepository.save(customer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addInternalCustomer(Customer customer) {
|
||||||
|
if (customer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
customer.setId(null);
|
||||||
|
customer.setInternal(true);
|
||||||
|
addCustomer(customer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateCustomer(Customer customer) {
|
||||||
|
if (customer == null || customer.getId() == null) {
|
||||||
|
throw new IllegalArgumentException("Kunden-ID fehlt");
|
||||||
|
}
|
||||||
|
validateCustomer(customer);
|
||||||
|
|
||||||
|
Customer existing = addCustomerRepository.findById(customer.getId())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Kunde nicht gefunden"));
|
||||||
|
customer.setCreatedBy(existing.getCreatedBy());
|
||||||
|
customer.setOwner(existing.getOwner());
|
||||||
|
if (customer.getUsrId() == null) {
|
||||||
|
customer.setUsrId(existing.getUsrId());
|
||||||
|
}
|
||||||
|
|
||||||
addCustomerRepository.save(customer);
|
addCustomerRepository.save(customer);
|
||||||
}
|
}
|
||||||
@@ -35,13 +67,10 @@ public class AddCustomerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String mail = customer.getMail() != null ? customer.getMail().trim() : "";
|
String mail = customer.getMail() != null ? customer.getMail().trim() : "";
|
||||||
if (mail.isEmpty()) {
|
if (!mail.isEmpty() && !mail.contains("@")) {
|
||||||
throw new IllegalArgumentException("E-Mail-Adresse ist ein Pflichtfeld");
|
|
||||||
}
|
|
||||||
if (!mail.contains("@")) {
|
|
||||||
throw new IllegalArgumentException("Bitte geben Sie eine gültige E-Mail-Adresse ein");
|
throw new IllegalArgumentException("Bitte geben Sie eine gültige E-Mail-Adresse ein");
|
||||||
}
|
}
|
||||||
|
|
||||||
customer.setMail(mail);
|
customer.setMail(mail.isEmpty() ? null : mail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public class CustomerService {
|
|||||||
|
|
||||||
public List<Customer> findAllForCurrentOwner() {
|
public List<Customer> findAllForCurrentOwner() {
|
||||||
ObjectId ownerId = securityService.getCurrentUserId();
|
ObjectId ownerId = securityService.getCurrentUserId();
|
||||||
return todoRepository.findByOwner(ownerId);
|
return todoRepository.findByOwnerAndInternalFalse(ownerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Customer save(Customer customer) {
|
public Customer save(Customer customer) {
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package de.assecutor.votianlt.pages.service;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.Counter;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
|
import org.springframework.data.mongodb.core.FindAndModifyOptions;
|
||||||
|
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||||
|
import org.springframework.data.mongodb.core.query.Criteria;
|
||||||
|
import org.springframework.data.mongodb.core.query.Query;
|
||||||
|
import org.springframework.data.mongodb.core.query.Update;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SequenceGeneratorService {
|
||||||
|
|
||||||
|
public static final String CUSTOMER_NUMBER_SEQ = "customerNumber";
|
||||||
|
public static final int CUSTOMER_NUMBER_START = 10000;
|
||||||
|
|
||||||
|
private final MongoTemplate mongoTemplate;
|
||||||
|
|
||||||
|
public SequenceGeneratorService(MongoTemplate mongoTemplate) {
|
||||||
|
this.mongoTemplate = mongoTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int nextCustomerNumber() {
|
||||||
|
return (int) nextSequence(CUSTOMER_NUMBER_SEQ, CUSTOMER_NUMBER_START);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long nextSequence(String sequenceId, long startValue) {
|
||||||
|
ensureInitialized(sequenceId, startValue - 1);
|
||||||
|
Counter updated = mongoTemplate.findAndModify(
|
||||||
|
Query.query(Criteria.where("_id").is(sequenceId)),
|
||||||
|
new Update().inc("sequence", 1),
|
||||||
|
FindAndModifyOptions.options().returnNew(true),
|
||||||
|
Counter.class);
|
||||||
|
return updated != null ? updated.getSequence() : startValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureInitialized(String sequenceId, long initialValue) {
|
||||||
|
boolean exists = mongoTemplate.exists(
|
||||||
|
Query.query(Criteria.where("_id").is(sequenceId)),
|
||||||
|
Counter.class);
|
||||||
|
if (exists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Counter counter = new Counter();
|
||||||
|
counter.setId(sequenceId);
|
||||||
|
counter.setSequence(initialValue);
|
||||||
|
try {
|
||||||
|
mongoTemplate.insert(counter);
|
||||||
|
} catch (DuplicateKeyException ignored) {
|
||||||
|
// Ein anderer Thread hat den Counter gleichzeitig angelegt — passt.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,9 +81,8 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
|
|||||||
fax = new TextField(getTranslation("profile.fax"));
|
fax = new TextField(getTranslation("profile.fax"));
|
||||||
fax.setWidthFull();
|
fax.setWidthFull();
|
||||||
|
|
||||||
// E-Mail (Pflichtfeld)
|
// E-Mail (optional)
|
||||||
mail = new TextField(getTranslation("profile.email"));
|
mail = new TextField(getTranslation("profile.email"));
|
||||||
mail.setRequiredIndicatorVisible(true);
|
|
||||||
mail.setWidthFull();
|
mail.setWidthFull();
|
||||||
mail.addBlurListener(e -> validateEmail());
|
mail.addBlurListener(e -> validateEmail());
|
||||||
|
|
||||||
@@ -179,8 +178,9 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
|
|||||||
|
|
||||||
binder.forField(fax).bind(Customer::getFax, Customer::setFax);
|
binder.forField(fax).bind(Customer::getFax, Customer::setFax);
|
||||||
|
|
||||||
binder.forField(mail).asRequired(getTranslation("profile.validation.email.required"))
|
binder.forField(mail)
|
||||||
.withValidator(email -> email.contains("@"), getTranslation("profile.validation.email.invalid"))
|
.withValidator(email -> email == null || email.isBlank() || email.contains("@"),
|
||||||
|
getTranslation("profile.validation.email.invalid"))
|
||||||
.bind(Customer::getMail, Customer::setMail);
|
.bind(Customer::getMail, Customer::setMail);
|
||||||
|
|
||||||
binder.forField(street).asRequired(getTranslation("profile.validation.street.required"))
|
binder.forField(street).asRequired(getTranslation("profile.validation.street.required"))
|
||||||
@@ -247,10 +247,7 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
|
|||||||
|
|
||||||
private void validateEmail() {
|
private void validateEmail() {
|
||||||
String value = mail.getValue();
|
String value = mail.getValue();
|
||||||
if (value == null || value.trim().isEmpty()) {
|
if (value != null && !value.trim().isEmpty() && !value.contains("@")) {
|
||||||
mail.setInvalid(true);
|
|
||||||
mail.setErrorMessage(getTranslation("profile.validation.email.required"));
|
|
||||||
} else if (!value.contains("@")) {
|
|
||||||
mail.setInvalid(true);
|
mail.setInvalid(true);
|
||||||
mail.setErrorMessage(getTranslation("profile.validation.email.invalid"));
|
mail.setErrorMessage(getTranslation("profile.validation.email.invalid"));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -137,12 +137,16 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
private TextField pickupZip;
|
private TextField pickupZip;
|
||||||
private TextField pickupCity;
|
private TextField pickupCity;
|
||||||
private Checkbox savePickupAddress;
|
private Checkbox savePickupAddress;
|
||||||
|
private org.bson.types.ObjectId pickupCustomerId;
|
||||||
|
private boolean pickupAddressDiffers;
|
||||||
|
|
||||||
// Delivery stations as tiles in a 3x3 grid (max 7 delivery + 1 pickup + 1 plus
|
// Delivery stations as tiles in a 3x3 grid (max 7 delivery + 1 pickup + 1 plus
|
||||||
// = 9)
|
// = 9)
|
||||||
private final List<StationTile> deliveryStationTilesList = new ArrayList<>();
|
private final List<StationTile> deliveryStationTilesList = new ArrayList<>();
|
||||||
private final List<DeliveryStation> deliveryStationsState = new ArrayList<>();
|
private final List<DeliveryStation> deliveryStationsState = new ArrayList<>();
|
||||||
private final List<Boolean> deliveryStationsSaveAddress = new ArrayList<>();
|
private final List<Boolean> deliveryStationsSaveAddress = new ArrayList<>();
|
||||||
|
private final List<org.bson.types.ObjectId> deliveryStationsCustomerId = new ArrayList<>();
|
||||||
|
private final List<Boolean> deliveryStationsAddressDiffers = new ArrayList<>();
|
||||||
private final List<String> deliveryStationsMailState = new ArrayList<>();
|
private final List<String> deliveryStationsMailState = new ArrayList<>();
|
||||||
private final List<Div> deliveryStationSlotList = new ArrayList<>();
|
private final List<Div> deliveryStationSlotList = new ArrayList<>();
|
||||||
private final List<Span> deliveryStationDistanceChips = new ArrayList<>();
|
private final List<Span> deliveryStationDistanceChips = new ArrayList<>();
|
||||||
@@ -721,6 +725,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
// Add empty state for this station
|
// Add empty state for this station
|
||||||
deliveryStationsState.add(new DeliveryStation());
|
deliveryStationsState.add(new DeliveryStation());
|
||||||
deliveryStationsSaveAddress.add(true);
|
deliveryStationsSaveAddress.add(true);
|
||||||
|
deliveryStationsCustomerId.add(null);
|
||||||
|
deliveryStationsAddressDiffers.add(false);
|
||||||
deliveryStationsMailState.add(null);
|
deliveryStationsMailState.add(null);
|
||||||
deliveryStationsValidatedByGoogle.add(false);
|
deliveryStationsValidatedByGoogle.add(false);
|
||||||
|
|
||||||
@@ -769,6 +775,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
deliveryStationTilesList.remove(removeIdx);
|
deliveryStationTilesList.remove(removeIdx);
|
||||||
deliveryStationsState.remove(removeIdx);
|
deliveryStationsState.remove(removeIdx);
|
||||||
deliveryStationsSaveAddress.remove(removeIdx);
|
deliveryStationsSaveAddress.remove(removeIdx);
|
||||||
|
deliveryStationsCustomerId.remove(removeIdx);
|
||||||
|
deliveryStationsAddressDiffers.remove(removeIdx);
|
||||||
deliveryStationsMailState.remove(removeIdx);
|
deliveryStationsMailState.remove(removeIdx);
|
||||||
deliveryStationsValidatedByGoogle.remove(removeIdx);
|
deliveryStationsValidatedByGoogle.remove(removeIdx);
|
||||||
deliveryStationTasksState.remove(removeIdx);
|
deliveryStationTasksState.remove(removeIdx);
|
||||||
@@ -867,6 +875,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
pickupZip.setValue(data.getZip() != null ? data.getZip() : "");
|
pickupZip.setValue(data.getZip() != null ? data.getZip() : "");
|
||||||
pickupCity.setValue(data.getCity() != null ? data.getCity() : "");
|
pickupCity.setValue(data.getCity() != null ? data.getCity() : "");
|
||||||
savePickupAddress.setValue(data.isSaveAddress());
|
savePickupAddress.setValue(data.isSaveAddress());
|
||||||
|
pickupCustomerId = data.getCustomerId();
|
||||||
|
pickupAddressDiffers = data.isAddressDiffersFromCustomer();
|
||||||
|
|
||||||
// Sync appointment fields for binder/submit
|
// Sync appointment fields for binder/submit
|
||||||
pickupDate.setValue(data.getAppointmentDate());
|
pickupDate.setValue(data.getAppointmentDate());
|
||||||
@@ -913,6 +923,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
currentData.setZip(pickupZip.getValue());
|
currentData.setZip(pickupZip.getValue());
|
||||||
currentData.setCity(pickupCity.getValue());
|
currentData.setCity(pickupCity.getValue());
|
||||||
currentData.setSaveAddress(savePickupAddress.getValue());
|
currentData.setSaveAddress(savePickupAddress.getValue());
|
||||||
|
currentData.setCustomerId(pickupCustomerId);
|
||||||
currentData.setCustomerSelection(customerSelection.getValue());
|
currentData.setCustomerSelection(customerSelection.getValue());
|
||||||
// Pre-fill pickup-specific fields
|
// Pre-fill pickup-specific fields
|
||||||
currentData.setAppointmentDate(pickupDate.getValue());
|
currentData.setAppointmentDate(pickupDate.getValue());
|
||||||
@@ -1137,6 +1148,14 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
station.setCity(data.getCity());
|
station.setCity(data.getCity());
|
||||||
station.setTasks(data.getTasks() != null ? new ArrayList<>(data.getTasks()) : new ArrayList<>());
|
station.setTasks(data.getTasks() != null ? new ArrayList<>(data.getTasks()) : new ArrayList<>());
|
||||||
deliveryStationsSaveAddress.set(idx, data.isSaveAddress());
|
deliveryStationsSaveAddress.set(idx, data.isSaveAddress());
|
||||||
|
while (deliveryStationsCustomerId.size() <= idx) {
|
||||||
|
deliveryStationsCustomerId.add(null);
|
||||||
|
}
|
||||||
|
deliveryStationsCustomerId.set(idx, data.getCustomerId());
|
||||||
|
while (deliveryStationsAddressDiffers.size() <= idx) {
|
||||||
|
deliveryStationsAddressDiffers.add(false);
|
||||||
|
}
|
||||||
|
deliveryStationsAddressDiffers.set(idx, data.isAddressDiffersFromCustomer());
|
||||||
deliveryStationsMailState.set(idx, trimToNull(data.getMail()));
|
deliveryStationsMailState.set(idx, trimToNull(data.getMail()));
|
||||||
|
|
||||||
// Store tasks for this delivery station
|
// Store tasks for this delivery station
|
||||||
@@ -1182,6 +1201,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
currentData.setZip(station.getZip());
|
currentData.setZip(station.getZip());
|
||||||
currentData.setCity(station.getCity());
|
currentData.setCity(station.getCity());
|
||||||
currentData.setSaveAddress(deliveryStationsSaveAddress.get(actualIndex));
|
currentData.setSaveAddress(deliveryStationsSaveAddress.get(actualIndex));
|
||||||
|
if (actualIndex < deliveryStationsCustomerId.size()) {
|
||||||
|
currentData.setCustomerId(deliveryStationsCustomerId.get(actualIndex));
|
||||||
|
}
|
||||||
if (actualIndex < deliveryStationsValidatedByGoogle.size()) {
|
if (actualIndex < deliveryStationsValidatedByGoogle.size()) {
|
||||||
currentData.setAddressValidatedByGoogle(deliveryStationsValidatedByGoogle.get(actualIndex));
|
currentData.setAddressValidatedByGoogle(deliveryStationsValidatedByGoogle.get(actualIndex));
|
||||||
}
|
}
|
||||||
@@ -1817,39 +1839,60 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEU: Kunden anlegen, wenn Checkboxen aktiviert
|
// Kunden anlegen/aktualisieren bzw. intern sichern
|
||||||
|
Customer pickupCustomer = new Customer();
|
||||||
|
pickupCustomer.setCompanyName(pickupCompany.getValue());
|
||||||
|
pickupCustomer.setTitle(pickupSalutation.getValue());
|
||||||
|
pickupCustomer.setFirstname(pickupFirstName.getValue());
|
||||||
|
pickupCustomer.setLastName(pickupLastName.getValue());
|
||||||
|
pickupCustomer.setTelephone(pickupPhone.getValue());
|
||||||
|
pickupCustomer.setMail(pickupMail);
|
||||||
|
pickupCustomer.setStreet(pickupStreet.getValue());
|
||||||
|
pickupCustomer.setHouseNumber(pickupHouseNumber.getValue());
|
||||||
|
pickupCustomer.setAddressAddition(pickupAddressAddition.getValue());
|
||||||
|
pickupCustomer.setZip(pickupZip.getValue());
|
||||||
|
pickupCustomer.setCity(pickupCity.getValue());
|
||||||
if (savePickupAddress.getValue()) {
|
if (savePickupAddress.getValue()) {
|
||||||
Customer pickupCustomer = new Customer();
|
if (pickupCustomerId != null) {
|
||||||
pickupCustomer.setCompanyName(pickupCompany.getValue());
|
pickupCustomer.setId(pickupCustomerId);
|
||||||
pickupCustomer.setTitle(pickupSalutation.getValue());
|
addCustomerService.updateCustomer(pickupCustomer);
|
||||||
pickupCustomer.setFirstname(pickupFirstName.getValue());
|
} else {
|
||||||
pickupCustomer.setLastName(pickupLastName.getValue());
|
addCustomerService.addCustomer(pickupCustomer);
|
||||||
pickupCustomer.setTelephone(pickupPhone.getValue());
|
}
|
||||||
pickupCustomer.setMail(pickupMail);
|
} else if (pickupAddressDiffers) {
|
||||||
pickupCustomer.setStreet(pickupStreet.getValue());
|
addCustomerService.addInternalCustomer(pickupCustomer);
|
||||||
pickupCustomer.setHouseNumber(pickupHouseNumber.getValue());
|
|
||||||
pickupCustomer.setAddressAddition(pickupAddressAddition.getValue());
|
|
||||||
pickupCustomer.setZip(pickupZip.getValue());
|
|
||||||
pickupCustomer.setCity(pickupCity.getValue());
|
|
||||||
addCustomerService.addCustomer(pickupCustomer);
|
|
||||||
}
|
}
|
||||||
// Save delivery station addresses as customers if checkbox is checked
|
// Delivery-Stationen: anlegen, aktualisieren oder als intern sichern
|
||||||
for (int i = 0; i < deliveryStationsState.size(); i++) {
|
for (int i = 0; i < deliveryStationsState.size(); i++) {
|
||||||
if (i < deliveryStationsSaveAddress.size() && deliveryStationsSaveAddress.get(i)) {
|
DeliveryStation ds = deliveryStationsState.get(i);
|
||||||
DeliveryStation ds = deliveryStationsState.get(i);
|
Customer deliveryCustomer = new Customer();
|
||||||
Customer deliveryCustomer = new Customer();
|
deliveryCustomer.setCompanyName(ds.getCompany());
|
||||||
deliveryCustomer.setCompanyName(ds.getCompany());
|
deliveryCustomer.setTitle(ds.getSalutation());
|
||||||
deliveryCustomer.setTitle(ds.getSalutation());
|
deliveryCustomer.setFirstname(ds.getFirstName());
|
||||||
deliveryCustomer.setFirstname(ds.getFirstName());
|
deliveryCustomer.setLastName(ds.getLastName());
|
||||||
deliveryCustomer.setLastName(ds.getLastName());
|
deliveryCustomer.setTelephone(ds.getPhone());
|
||||||
deliveryCustomer.setTelephone(ds.getPhone());
|
deliveryCustomer.setMail(i < deliveryStationsMailState.size() ? deliveryStationsMailState.get(i) : null);
|
||||||
deliveryCustomer.setMail(i < deliveryStationsMailState.size() ? deliveryStationsMailState.get(i) : null);
|
deliveryCustomer.setStreet(ds.getStreet());
|
||||||
deliveryCustomer.setStreet(ds.getStreet());
|
deliveryCustomer.setHouseNumber(ds.getHouseNumber());
|
||||||
deliveryCustomer.setHouseNumber(ds.getHouseNumber());
|
deliveryCustomer.setAddressAddition(ds.getAddressAddition());
|
||||||
deliveryCustomer.setAddressAddition(ds.getAddressAddition());
|
deliveryCustomer.setZip(ds.getZip());
|
||||||
deliveryCustomer.setZip(ds.getZip());
|
deliveryCustomer.setCity(ds.getCity());
|
||||||
deliveryCustomer.setCity(ds.getCity());
|
boolean saveRequested = i < deliveryStationsSaveAddress.size()
|
||||||
addCustomerService.addCustomer(deliveryCustomer);
|
&& deliveryStationsSaveAddress.get(i);
|
||||||
|
org.bson.types.ObjectId existingId = i < deliveryStationsCustomerId.size()
|
||||||
|
? deliveryStationsCustomerId.get(i)
|
||||||
|
: null;
|
||||||
|
boolean addressDiffers = i < deliveryStationsAddressDiffers.size()
|
||||||
|
&& deliveryStationsAddressDiffers.get(i);
|
||||||
|
if (saveRequested) {
|
||||||
|
if (existingId != null) {
|
||||||
|
deliveryCustomer.setId(existingId);
|
||||||
|
addCustomerService.updateCustomer(deliveryCustomer);
|
||||||
|
} else {
|
||||||
|
addCustomerService.addCustomer(deliveryCustomer);
|
||||||
|
}
|
||||||
|
} else if (addressDiffers) {
|
||||||
|
addCustomerService.addInternalCustomer(deliveryCustomer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2093,7 +2136,105 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt alle leeren (nicht editierten) Lieferstationen, sofern mindestens
|
||||||
|
* eine valide Lieferstation übrig bleibt. Wird aufgerufen bevor die Stationen
|
||||||
|
* übernommen werden.
|
||||||
|
*/
|
||||||
|
private void removeEmptyDeliveryStations() {
|
||||||
|
// Indizes der leeren Stationen sammeln (absteigend, damit beim Entfernen die Indizes stabil bleiben)
|
||||||
|
List<Integer> emptyIndices = new ArrayList<>();
|
||||||
|
for (int i = 0; i < deliveryStationsState.size(); i++) {
|
||||||
|
if (hasDeliveryStationValidationErrors(deliveryStationsState.get(i))) {
|
||||||
|
emptyIndices.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mindestens eine valide Station muss übrig bleiben
|
||||||
|
if (emptyIndices.size() >= deliveryStationsState.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Von hinten nach vorne entfernen, damit Indizes stabil bleiben
|
||||||
|
for (int k = emptyIndices.size() - 1; k >= 0; k--) {
|
||||||
|
int idx = emptyIndices.get(k);
|
||||||
|
|
||||||
|
deliveryStationTilesList.remove(idx);
|
||||||
|
deliveryStationsState.remove(idx);
|
||||||
|
deliveryStationsSaveAddress.remove(idx);
|
||||||
|
deliveryStationsCustomerId.remove(idx);
|
||||||
|
deliveryStationsAddressDiffers.remove(idx);
|
||||||
|
deliveryStationsMailState.remove(idx);
|
||||||
|
deliveryStationsValidatedByGoogle.remove(idx);
|
||||||
|
deliveryStationTasksState.remove(idx);
|
||||||
|
Div removedSlot = deliveryStationSlotList.remove(idx);
|
||||||
|
deliveryStationDistanceChips.remove(idx);
|
||||||
|
pickupToDeliveryRouteResults.remove(idx);
|
||||||
|
stationsGridContainer.remove(removedSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasks und Routen-Maps re-indizieren
|
||||||
|
Map<Integer, List<BaseTask>> reindexedTasks = new HashMap<>();
|
||||||
|
for (Map.Entry<Integer, List<BaseTask>> entry : deliveryStationTasksState.entrySet()) {
|
||||||
|
int oldIdx = entry.getKey();
|
||||||
|
int newIdx = oldIdx - (int) emptyIndices.stream().filter(ei -> ei < oldIdx).count();
|
||||||
|
reindexedTasks.put(newIdx, entry.getValue());
|
||||||
|
}
|
||||||
|
deliveryStationTasksState.clear();
|
||||||
|
deliveryStationTasksState.putAll(reindexedTasks);
|
||||||
|
|
||||||
|
Map<Integer, RouteCalculationResult> reindexedRoutes = new HashMap<>();
|
||||||
|
for (Map.Entry<Integer, RouteCalculationResult> entry : pickupToDeliveryRouteResults.entrySet()) {
|
||||||
|
int oldIdx = entry.getKey();
|
||||||
|
int newIdx = oldIdx - (int) emptyIndices.stream().filter(ei -> ei < oldIdx).count();
|
||||||
|
reindexedRoutes.put(newIdx, entry.getValue());
|
||||||
|
}
|
||||||
|
pickupToDeliveryRouteResults.clear();
|
||||||
|
pickupToDeliveryRouteResults.putAll(reindexedRoutes);
|
||||||
|
|
||||||
|
// Service-Zuordnungen anpassen
|
||||||
|
for (SelectedServiceEntry selectedService : selectedServices) {
|
||||||
|
Integer stationOrder = selectedService.getDeliveryStationOrder();
|
||||||
|
if (stationOrder == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (emptyIndices.contains(stationOrder)) {
|
||||||
|
selectedService.setDeliveryStationOrder(deliveryStationsState.isEmpty() ? null : 0);
|
||||||
|
} else {
|
||||||
|
int newOrder = stationOrder - (int) emptyIndices.stream().filter(ei -> ei < stationOrder).count();
|
||||||
|
selectedService.setDeliveryStationOrder(newOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tiles neu nummerieren und Click-Listener aktualisieren
|
||||||
|
for (int i = 0; i < deliveryStationTilesList.size(); i++) {
|
||||||
|
StationTile t = deliveryStationTilesList.get(i);
|
||||||
|
int newNumber = i + 1;
|
||||||
|
t.updateStationNumber(newNumber);
|
||||||
|
t.updateTitle(getTranslation("addjob.station.delivery", newNumber));
|
||||||
|
final int newIdx = i;
|
||||||
|
t.setClickListener(tt -> openDeliveryDialog(tt, newIdx));
|
||||||
|
if (i == 0) {
|
||||||
|
t.setDeleteListener(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "+" Button wieder anzeigen falls unter Maximum
|
||||||
|
if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS
|
||||||
|
&& addStationButtonSlot.getParent().isEmpty()) {
|
||||||
|
stationsGridContainer.add(addStationButtonSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (servicesGrid != null) {
|
||||||
|
servicesGrid.getDataProvider().refreshAll();
|
||||||
|
}
|
||||||
|
updatePriceSummary();
|
||||||
|
triggerValidation();
|
||||||
|
updateTabLabels();
|
||||||
|
}
|
||||||
|
|
||||||
private void handleApplyStations() {
|
private void handleApplyStations() {
|
||||||
|
removeEmptyDeliveryStations();
|
||||||
revealPriceAndDetailsSection();
|
revealPriceAndDetailsSection();
|
||||||
|
|
||||||
if (!areAllStationsValidatedByGoogle()) {
|
if (!areAllStationsValidatedByGoogle()) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.vaadin.flow.component.html.Span;
|
|||||||
import com.vaadin.flow.component.notification.Notification;
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
import com.vaadin.flow.component.textfield.NumberField;
|
||||||
|
|
||||||
import com.vaadin.flow.router.HasDynamicTitle;
|
import com.vaadin.flow.router.HasDynamicTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
@@ -68,6 +69,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
private List<ServiceRow> gridRows = new ArrayList<>();
|
private List<ServiceRow> gridRows = new ArrayList<>();
|
||||||
private Grid<ServiceRow> servicesGrid;
|
private Grid<ServiceRow> servicesGrid;
|
||||||
private Div servicesSection;
|
private Div servicesSection;
|
||||||
|
private Div summarySection;
|
||||||
|
private NumberField vatField;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class to represent a row in the services grid
|
* Helper class to represent a row in the services grid
|
||||||
@@ -176,6 +179,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentUser = securityService.getAuthenticatedUser()
|
||||||
|
.flatMap(auth -> userRepository.findByEmail(auth.getUsername())).orElse(null);
|
||||||
|
|
||||||
createInvoiceView();
|
createInvoiceView();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,8 +209,12 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
Div servicesSection = createServicesSelectionSection();
|
Div servicesSection = createServicesSelectionSection();
|
||||||
add(servicesSection);
|
add(servicesSection);
|
||||||
|
|
||||||
|
// VAT Section (must exist before summary so effectiveVatRate() can read the field)
|
||||||
|
Div vatSection = createVatSection();
|
||||||
|
add(vatSection);
|
||||||
|
|
||||||
// Summary Section
|
// Summary Section
|
||||||
Div summarySection = createSummarySection();
|
summarySection = createSummarySection();
|
||||||
add(summarySection);
|
add(summarySection);
|
||||||
|
|
||||||
// Create Invoice Button
|
// Create Invoice Button
|
||||||
@@ -336,13 +346,16 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
section.setWidthFull();
|
section.setWidthFull();
|
||||||
section.addClassName("invoice-section-card");
|
section.addClassName("invoice-section-card");
|
||||||
section.getStyle().set("margin-bottom", "var(--lumo-space-m)");
|
section.getStyle().set("margin-bottom", "var(--lumo-space-m)");
|
||||||
|
populateSummarySection(section);
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateSummarySection(Div section) {
|
||||||
H3 sectionTitle = new H3(getTranslation("createinvoice.section.summary"));
|
H3 sectionTitle = new H3(getTranslation("createinvoice.section.summary"));
|
||||||
section.add(sectionTitle);
|
section.add(sectionTitle);
|
||||||
|
|
||||||
// Calculate totals
|
|
||||||
BigDecimal netAmount = calculateNetAmount();
|
BigDecimal netAmount = calculateNetAmount();
|
||||||
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
BigDecimal vatRate = effectiveVatRate();
|
||||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||||
|
|
||||||
@@ -355,9 +368,40 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
totalAmount.setScale(2, RoundingMode.HALF_UP) + " €", true));
|
totalAmount.setScale(2, RoundingMode.HALF_UP) + " €", true));
|
||||||
|
|
||||||
section.add(priceTable);
|
section.add(priceTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Div createVatSection() {
|
||||||
|
Div section = new Div();
|
||||||
|
section.setWidthFull();
|
||||||
|
section.addClassName("invoice-section-card");
|
||||||
|
section.getStyle().set("margin-bottom", "var(--lumo-space-m)");
|
||||||
|
|
||||||
|
H3 sectionTitle = new H3(getTranslation("createinvoice.section.vat"));
|
||||||
|
section.add(sectionTitle);
|
||||||
|
|
||||||
|
vatField = new NumberField();
|
||||||
|
vatField.setLabel(getTranslation("createinvoice.field.vatrate"));
|
||||||
|
vatField.setSuffixComponent(new Span("%"));
|
||||||
|
vatField.setStep(0.01);
|
||||||
|
vatField.setMin(0);
|
||||||
|
BigDecimal initialRate = currentUser != null && currentUser.getVatRate() != null
|
||||||
|
? currentUser.getVatRate()
|
||||||
|
: Service.FIXED_VAT_RATE;
|
||||||
|
vatField.setValue(initialRate.multiply(new BigDecimal("100")).doubleValue());
|
||||||
|
vatField.addValueChangeListener(e -> refreshSummarySection());
|
||||||
|
|
||||||
|
section.add(vatField);
|
||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void refreshSummarySection() {
|
||||||
|
if (summarySection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
summarySection.removeAll();
|
||||||
|
populateSummarySection(summarySection);
|
||||||
|
}
|
||||||
|
|
||||||
private Div createPriceRow(String label, String value, boolean bold) {
|
private Div createPriceRow(String label, String value, boolean bold) {
|
||||||
Div row = new Div();
|
Div row = new Div();
|
||||||
row.addClassName("price-row");
|
row.addClassName("price-row");
|
||||||
@@ -427,6 +471,17 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
return units;
|
return units;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BigDecimal effectiveVatRate() {
|
||||||
|
if (vatField != null && vatField.getValue() != null) {
|
||||||
|
return new BigDecimal(Double.toString(vatField.getValue()))
|
||||||
|
.divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
if (currentUser != null && currentUser.getVatRate() != null) {
|
||||||
|
return currentUser.getVatRate();
|
||||||
|
}
|
||||||
|
return Service.FIXED_VAT_RATE;
|
||||||
|
}
|
||||||
|
|
||||||
private BigDecimal calculateNetAmount() {
|
private BigDecimal calculateNetAmount() {
|
||||||
BigDecimal total = BigDecimal.ZERO;
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
|
|
||||||
@@ -514,7 +569,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(user.getId());
|
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(user.getId());
|
||||||
|
|
||||||
BigDecimal netAmount = calculateNetAmount();
|
BigDecimal netAmount = calculateNetAmount();
|
||||||
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
BigDecimal vatRate = effectiveVatRate();
|
||||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||||
|
|
||||||
@@ -553,7 +608,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
throws Exception {
|
throws Exception {
|
||||||
// Calculate totals
|
// Calculate totals
|
||||||
BigDecimal netAmount = calculateNetAmount();
|
BigDecimal netAmount = calculateNetAmount();
|
||||||
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
BigDecimal vatRate = effectiveVatRate();
|
||||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ public class CustomersView extends Main implements HasDynamicTitle {
|
|||||||
createBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
createBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
|
||||||
todoGrid = new Grid<>();
|
todoGrid = new Grid<>();
|
||||||
todoGrid.setItems(query -> todoService.list(toSpringPageRequest(query)).stream());
|
todoGrid.setItems(query -> todoService.list(toSpringPageRequest(query)).stream()
|
||||||
|
.filter(c -> !c.isInternal()));
|
||||||
todoGrid.addColumn(Customer::getCompanyName).setHeader(getTranslation("customers.column.company"));
|
todoGrid.addColumn(Customer::getCompanyName).setHeader(getTranslation("customers.column.company"));
|
||||||
todoGrid.setSizeFull();
|
todoGrid.setSizeFull();
|
||||||
todoGrid.addClassName("data-grid");
|
todoGrid.addClassName("data-grid");
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
private final InvoiceTemplateService invoiceTemplateService;
|
private final InvoiceTemplateService invoiceTemplateService;
|
||||||
private UserInvoiceData currentInvoiceData;
|
private UserInvoiceData currentInvoiceData;
|
||||||
private Checkbox billingEnabled;
|
private Checkbox billingEnabled;
|
||||||
|
private NumberField vatRateField;
|
||||||
private VerticalLayout propertiesPanelProfile;
|
private VerticalLayout propertiesPanelProfile;
|
||||||
|
|
||||||
private final ServiceRepository serviceRepository;
|
private final ServiceRepository serviceRepository;
|
||||||
@@ -330,10 +331,9 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
termsTextArea = new TextArea();
|
termsTextArea = new TextArea();
|
||||||
pdfFrame = new IFrame();
|
pdfFrame = new IFrame();
|
||||||
|
|
||||||
// Nur die Checkbox "Rechnungslegung über votianLT"
|
// Checkbox "Rechnungslegung über votianLT"
|
||||||
billingEnabled = new Checkbox(getTranslation("profile.billing.enabled"));
|
billingEnabled = new Checkbox(getTranslation("profile.billing.enabled"));
|
||||||
billingEnabled.setValue(true); // Standardmäßig aktiviert
|
billingEnabled.setValue(true); // Standardmäßig aktiviert
|
||||||
billingTab.add(billingEnabled);
|
|
||||||
|
|
||||||
prefixField.setLabel(getTranslation("profile.billing.prefix"));
|
prefixField.setLabel(getTranslation("profile.billing.prefix"));
|
||||||
prefixField.setPlaceholder("z.B. RE-2024-");
|
prefixField.setPlaceholder("z.B. RE-2024-");
|
||||||
@@ -347,7 +347,30 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
"if (window.updateProfileMasterdataValue) { window.updateProfileMasterdataValue('masterdata.invoice_number', '"
|
"if (window.updateProfileMasterdataValue) { window.updateProfileMasterdataValue('masterdata.invoice_number', '"
|
||||||
+ invNr.replace("'", "\\'") + "'); }");
|
+ invNr.replace("'", "\\'") + "'); }");
|
||||||
});
|
});
|
||||||
billingTab.add(prefixField);
|
|
||||||
|
vatRateField = new NumberField();
|
||||||
|
vatRateField.setLabel(getTranslation("profile.settings.vatrate"));
|
||||||
|
vatRateField.setSuffixComponent(new Span("%"));
|
||||||
|
vatRateField.setStep(0.01);
|
||||||
|
vatRateField.setMin(0);
|
||||||
|
vatRateField.setMaxWidth("200px");
|
||||||
|
if (currentUser.getVatRate() != null) {
|
||||||
|
vatRateField.setValue(currentUser.getVatRate().multiply(new java.math.BigDecimal("100")).doubleValue());
|
||||||
|
}
|
||||||
|
vatRateField.addValueChangeListener(e -> {
|
||||||
|
Double v = e.getValue();
|
||||||
|
if (v != null) {
|
||||||
|
currentUser.setVatRate(new java.math.BigDecimal(Double.toString(v))
|
||||||
|
.divide(new java.math.BigDecimal("100"), 4, java.math.RoundingMode.HALF_UP));
|
||||||
|
getElement().executeJs("if (window.updateProfileVatRate) { window.updateProfileVatRate($0); }",
|
||||||
|
v / 100.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
HorizontalLayout billingHeaderLayout = new HorizontalLayout(billingEnabled, prefixField, vatRateField);
|
||||||
|
billingHeaderLayout.setSpacing(true);
|
||||||
|
billingHeaderLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
|
||||||
|
billingTab.add(billingHeaderLayout);
|
||||||
|
|
||||||
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
|
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
|
||||||
final HorizontalLayout mainLayout = new HorizontalLayout();
|
final HorizontalLayout mainLayout = new HorizontalLayout();
|
||||||
@@ -451,6 +474,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
billingEnabled.addValueChangeListener(e -> {
|
billingEnabled.addValueChangeListener(e -> {
|
||||||
boolean visible = e.getValue();
|
boolean visible = e.getValue();
|
||||||
prefixField.setVisible(visible);
|
prefixField.setVisible(visible);
|
||||||
|
vatRateField.setVisible(visible);
|
||||||
mainLayout.setVisible(visible);
|
mainLayout.setVisible(visible);
|
||||||
actionLayout.setVisible(visible);
|
actionLayout.setVisible(visible);
|
||||||
});
|
});
|
||||||
@@ -843,6 +867,17 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
&& !houseNumberField.isInvalid() && !zipField.isInvalid() && !cityField.isInvalid();
|
&& !houseNumberField.isInvalid() && !zipField.isInvalid() && !cityField.isInvalid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BigDecimal getPreviewVatRate() {
|
||||||
|
if (vatRateField != null && vatRateField.getValue() != null) {
|
||||||
|
return new BigDecimal(Double.toString(vatRateField.getValue())).divide(new BigDecimal("100"), 4,
|
||||||
|
java.math.RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
if (currentUser != null && currentUser.getVatRate() != null) {
|
||||||
|
return currentUser.getVatRate();
|
||||||
|
}
|
||||||
|
return Service.FIXED_VAT_RATE;
|
||||||
|
}
|
||||||
|
|
||||||
// Methoden für den Rechnungsgenerator im Profil
|
// Methoden für den Rechnungsgenerator im Profil
|
||||||
private void generatePreviewPdfFromProfile() {
|
private void generatePreviewPdfFromProfile() {
|
||||||
try {
|
try {
|
||||||
@@ -862,8 +897,9 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
} else {
|
} else {
|
||||||
templateData = result.toString();
|
templateData = result.toString();
|
||||||
}
|
}
|
||||||
|
BigDecimal previewVatRate = getPreviewVatRate();
|
||||||
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData,
|
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData,
|
||||||
currentUser, prefixField.getValue());
|
currentUser, prefixField.getValue(), previewVatRate);
|
||||||
showPdfInDialog(pdfBytes);
|
showPdfInDialog(pdfBytes);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Notification.show(getTranslation("profile.invoice.pdf.preview.error", ex.getMessage()),
|
Notification.show(getTranslation("profile.invoice.pdf.preview.error", ex.getMessage()),
|
||||||
@@ -1424,9 +1460,14 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
+ city.replace("'", "\\'") + "'," + "'masterdata.email': '" + email.replace("'", "\\'")
|
+ city.replace("'", "\\'") + "'," + "'masterdata.email': '" + email.replace("'", "\\'")
|
||||||
+ "'," + "'masterdata.phone': '" + phone.replace("'", "\\'") + "',"
|
+ "'," + "'masterdata.phone': '" + phone.replace("'", "\\'") + "',"
|
||||||
+ "'masterdata.invoice_number': '" + invoiceNumber.replace("'", "\\'") + "'" + "}";
|
+ "'masterdata.invoice_number': '" + invoiceNumber.replace("'", "\\'") + "'" + "}";
|
||||||
|
BigDecimal rate = currentUser.getVatRate() != null ? currentUser.getVatRate()
|
||||||
|
: Service.FIXED_VAT_RATE;
|
||||||
|
double vatRateJs = rate.doubleValue();
|
||||||
getElement().executeJs("setTimeout(function() { "
|
getElement().executeJs("setTimeout(function() { "
|
||||||
+ " if (window.loadProfileTemplate && document.getElementById('invoice-canvas-container-profile')) { "
|
+ " if (window.loadProfileTemplate && document.getElementById('invoice-canvas-container-profile')) { "
|
||||||
+ " console.log('Loading template into canvas...'); " + " window.masterdataValues = "
|
+ " console.log('Loading template into canvas...'); "
|
||||||
|
+ " window.profileInvoiceVatRate = " + vatRateJs + "; "
|
||||||
|
+ " window.masterdataValues = "
|
||||||
+ masterdataJson + "; " + " var templateData = JSON.parse('" + escapedJson + "'); "
|
+ masterdataJson + "; " + " var templateData = JSON.parse('" + escapedJson + "'); "
|
||||||
+ " window.loadProfileTemplate(templateData); " + " } else { "
|
+ " window.loadProfileTemplate(templateData); " + " } else { "
|
||||||
+ " console.error('loadProfileTemplate or canvas not available'); " + " } "
|
+ " console.error('loadProfileTemplate or canvas not available'); " + " } "
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package de.assecutor.votianlt.pages.view;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
|
import com.vaadin.flow.component.html.Main;
|
||||||
|
import com.vaadin.flow.component.html.Span;
|
||||||
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
|
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
import com.vaadin.flow.component.textfield.TextArea;
|
||||||
|
import com.vaadin.flow.router.BeforeEvent;
|
||||||
|
import com.vaadin.flow.router.HasDynamicTitle;
|
||||||
|
import com.vaadin.flow.router.HasUrlParameter;
|
||||||
|
import com.vaadin.flow.router.Route;
|
||||||
|
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||||
|
import de.assecutor.votianlt.model.Job;
|
||||||
|
import de.assecutor.votianlt.model.JobHistoryType;
|
||||||
|
import de.assecutor.votianlt.model.JobStatus;
|
||||||
|
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||||
|
import de.assecutor.votianlt.repository.JobRepository;
|
||||||
|
import de.assecutor.votianlt.security.SecurityService;
|
||||||
|
import de.assecutor.votianlt.service.JobHistoryService;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Route(value = "job_manual_complete", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||||
|
@RolesAllowed("USER")
|
||||||
|
@Slf4j
|
||||||
|
public class JobManualCompleteView extends Main implements HasUrlParameter<String>, HasDynamicTitle {
|
||||||
|
|
||||||
|
private final JobRepository jobRepository;
|
||||||
|
private final JobHistoryService jobHistoryService;
|
||||||
|
private final SecurityService securityService;
|
||||||
|
private final VerticalLayout content;
|
||||||
|
|
||||||
|
public JobManualCompleteView(JobRepository jobRepository, JobHistoryService jobHistoryService,
|
||||||
|
SecurityService securityService) {
|
||||||
|
this.jobRepository = jobRepository;
|
||||||
|
this.jobHistoryService = jobHistoryService;
|
||||||
|
this.securityService = securityService;
|
||||||
|
|
||||||
|
setSizeFull();
|
||||||
|
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
|
||||||
|
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
|
||||||
|
addClassName("data-view");
|
||||||
|
|
||||||
|
add(new ViewToolbar(getTranslation("jobsummary.dialog.manualcomplete.title")));
|
||||||
|
|
||||||
|
content = new VerticalLayout();
|
||||||
|
content.setSpacing(true);
|
||||||
|
content.setPadding(true);
|
||||||
|
content.setWidthFull();
|
||||||
|
content.addClassNames("form-shell", "form-card");
|
||||||
|
add(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPageTitle() {
|
||||||
|
return getTranslation("jobsummary.dialog.manualcomplete.title");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setParameter(BeforeEvent event, String parameter) {
|
||||||
|
content.removeAll();
|
||||||
|
|
||||||
|
if (parameter == null || parameter.isBlank()) {
|
||||||
|
content.add(new Span(getTranslation("jobhistory.error.no.id")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectId jobId;
|
||||||
|
try {
|
||||||
|
jobId = new ObjectId(parameter);
|
||||||
|
} catch (Exception e) {
|
||||||
|
content.add(new Span(getTranslation("jobhistory.error.invalid.id", parameter)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Job job = jobRepository.findById(jobId).orElse(null);
|
||||||
|
if (job == null) {
|
||||||
|
content.add(new Span(getTranslation("jobhistory.error.not.found", parameter)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void render(Job job) {
|
||||||
|
Span warningText = new Span(getTranslation("jobsummary.dialog.manualcomplete.text", job.getJobNumber()));
|
||||||
|
warningText.getStyle().set("color", "var(--lumo-error-text-color)");
|
||||||
|
|
||||||
|
TextArea reasonField = new TextArea(getTranslation("jobsummary.dialog.manualcomplete.reason"));
|
||||||
|
reasonField.setWidthFull();
|
||||||
|
reasonField.setMinHeight("100px");
|
||||||
|
reasonField.setRequired(true);
|
||||||
|
|
||||||
|
content.add(warningText, reasonField);
|
||||||
|
|
||||||
|
HorizontalLayout buttonBar = new HorizontalLayout();
|
||||||
|
buttonBar.setWidthFull();
|
||||||
|
buttonBar.setJustifyContentMode(HorizontalLayout.JustifyContentMode.END);
|
||||||
|
buttonBar.setSpacing(true);
|
||||||
|
|
||||||
|
Button cancelButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.cancel"),
|
||||||
|
e -> getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString())));
|
||||||
|
|
||||||
|
Button confirmButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.confirm"));
|
||||||
|
confirmButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
|
||||||
|
confirmButton.addClickListener(e -> {
|
||||||
|
String reason = reasonField.getValue();
|
||||||
|
if (reason == null || reason.trim().isEmpty()) {
|
||||||
|
reasonField.setInvalid(true);
|
||||||
|
reasonField.setErrorMessage(getTranslation("jobsummary.dialog.manualcomplete.reason.required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
JobStatus oldStatus = job.getStatus();
|
||||||
|
job.setStatus(JobStatus.COMPLETED);
|
||||||
|
job.setUpdatedAt(LocalDateTime.now());
|
||||||
|
jobRepository.save(job);
|
||||||
|
|
||||||
|
String currentUser = securityService.getCurrentUsername();
|
||||||
|
jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, currentUser);
|
||||||
|
|
||||||
|
String description = String.format("Auftrag manuell beendet von %s. Begründung: %s",
|
||||||
|
currentUser, reason.trim());
|
||||||
|
jobHistoryService.logCustomEvent(job.getId(),
|
||||||
|
getTranslation("jobsummary.history.manualcomplete.reason"),
|
||||||
|
description, currentUser, JobHistoryType.STATUS_CHANGE);
|
||||||
|
|
||||||
|
Notification
|
||||||
|
.show(getTranslation("jobsummary.notification.completed", job.getJobNumber()), 3000,
|
||||||
|
Notification.Position.BOTTOM_END)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||||
|
getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString()));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Notification
|
||||||
|
.show(getTranslation("jobsummary.notification.complete.error", ex.getMessage()), 5000,
|
||||||
|
Notification.Position.BOTTOM_END)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonBar.add(cancelButton, confirmButton);
|
||||||
|
content.add(buttonBar);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -212,6 +212,16 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
getUI().ifPresent(ui -> ui.navigate("message-details/" + appUserId + "/" + conversationId));
|
getUI().ifPresent(ui -> ui.navigate("message-details/" + appUserId + "/" + conversationId));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create Manual Completion Button for app jobs (digital processing)
|
||||||
|
Button manualCompleteButton = null;
|
||||||
|
if (job.isDigitalProcessing() && job.getStatus() != JobStatus.COMPLETED
|
||||||
|
&& job.getStatus() != JobStatus.CANCELLED) {
|
||||||
|
manualCompleteButton = new Button(getTranslation("jobsummary.button.manualcomplete"));
|
||||||
|
manualCompleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
|
||||||
|
manualCompleteButton.addClickListener(e -> getUI()
|
||||||
|
.ifPresent(ui -> ui.navigate("job_manual_complete/" + job.getId().toHexString())));
|
||||||
|
}
|
||||||
|
|
||||||
// Create Job History Button for toolbar
|
// Create Job History Button for toolbar
|
||||||
Button jobHistoryButton = new Button(getTranslation("jobsummary.button.jobhistory"));
|
Button jobHistoryButton = new Button(getTranslation("jobsummary.button.jobhistory"));
|
||||||
jobHistoryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
jobHistoryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
@@ -219,8 +229,13 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
getUI().ifPresent(ui -> ui.navigate("job_history/" + job.getId().toHexString()));
|
getUI().ifPresent(ui -> ui.navigate("job_history/" + job.getId().toHexString()));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add toolbar with both buttons in top right (Send Message button on the left)
|
// Add toolbar with buttons
|
||||||
add(new ViewToolbar(getTranslation("jobsummary.title"), sendMessageButton, jobHistoryButton));
|
if (manualCompleteButton != null) {
|
||||||
|
add(new ViewToolbar(getTranslation("jobsummary.title"), manualCompleteButton, sendMessageButton,
|
||||||
|
jobHistoryButton));
|
||||||
|
} else {
|
||||||
|
add(new ViewToolbar(getTranslation("jobsummary.title"), sendMessageButton, jobHistoryButton));
|
||||||
|
}
|
||||||
|
|
||||||
List<CargoItem> cargo = cargoItemRepository.findByJobId(currentJobId);
|
List<CargoItem> cargo = cargoItemRepository.findByJobId(currentJobId);
|
||||||
List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);
|
List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ public class ShowCustomersView extends VerticalLayout implements HasDynamicTitle
|
|||||||
var customers = customerService.findAll();
|
var customers = customerService.findAll();
|
||||||
var currentUserId = securityService.getCurrentUserId();
|
var currentUserId = securityService.getCurrentUserId();
|
||||||
var ownCustomers = customers.stream()
|
var ownCustomers = customers.stream()
|
||||||
.filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId)).toList();
|
.filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId))
|
||||||
|
.filter(c -> !c.isInternal()).toList();
|
||||||
grid.setItems(ownCustomers);
|
grid.setItems(ownCustomers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ import java.util.List;
|
|||||||
@Repository
|
@Repository
|
||||||
public interface SignatureRepository extends MongoRepository<Signature, ObjectId> {
|
public interface SignatureRepository extends MongoRepository<Signature, ObjectId> {
|
||||||
List<Signature> findByTaskId(ObjectId taskId);
|
List<Signature> findByTaskId(ObjectId taskId);
|
||||||
|
|
||||||
|
List<Signature> findByTaskIdOrderByCreatedAtDesc(ObjectId taskId);
|
||||||
}
|
}
|
||||||
@@ -258,6 +258,13 @@ public class CustomerInvoiceService {
|
|||||||
|
|
||||||
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user,
|
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user,
|
||||||
String invoicePrefix) throws Exception {
|
String invoicePrefix) throws Exception {
|
||||||
|
return generatePdfFromCanvasTemplate(jsonTemplateData, user, invoicePrefix, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user,
|
||||||
|
String invoicePrefix, BigDecimal vatRate) throws Exception {
|
||||||
|
BigDecimal effectiveVatRate = vatRate != null ? vatRate
|
||||||
|
: (user != null && user.getVatRate() != null ? user.getVatRate() : new BigDecimal("0.19"));
|
||||||
// Parse the JSON template data
|
// Parse the JSON template data
|
||||||
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData);
|
com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData);
|
||||||
@@ -458,7 +465,7 @@ public class CustomerInvoiceService {
|
|||||||
}
|
}
|
||||||
} else if ("services.list".equals(variable)) {
|
} else if ("services.list".equals(variable)) {
|
||||||
// Render services list as a table
|
// Render services list as a table
|
||||||
htmlBuilder.append(generateServicesTableHtml(mmWidth));
|
htmlBuilder.append(generateServicesTableHtml(mmWidth, effectiveVatRate));
|
||||||
} else if (text.contains("<br>")) {
|
} else if (text.contains("<br>")) {
|
||||||
// Multi-line text: render without nowrap so <br> tags work
|
// Multi-line text: render without nowrap so <br> tags work
|
||||||
htmlBuilder.append("<span>").append(text).append("</span>");
|
htmlBuilder.append("<span>").append(text).append("</span>");
|
||||||
@@ -484,16 +491,23 @@ public class CustomerInvoiceService {
|
|||||||
/**
|
/**
|
||||||
* Generate HTML table for services list with summary section below.
|
* Generate HTML table for services list with summary section below.
|
||||||
*/
|
*/
|
||||||
private String generateServicesTableHtml(double widthMm) {
|
private String generateServicesTableHtml(double widthMm, BigDecimal vatRate) {
|
||||||
StringBuilder html = new StringBuilder();
|
StringBuilder html = new StringBuilder();
|
||||||
|
|
||||||
|
BigDecimal pct = vatRate.multiply(new BigDecimal("100")).setScale(2, java.math.RoundingMode.HALF_UP)
|
||||||
|
.stripTrailingZeros();
|
||||||
|
if (pct.scale() < 0) {
|
||||||
|
pct = pct.setScale(0);
|
||||||
|
}
|
||||||
|
String vatLabel = pct.toPlainString().replace('.', ',') + "%";
|
||||||
|
|
||||||
// Sample data for preview (will be replaced with actual job data later)
|
// Sample data for preview (will be replaced with actual job data later)
|
||||||
String[][] sampleData = { { "Umzugsleistung inkl. Verpackung", "19%", "450,00 €" },
|
String[][] sampleData = { { "Umzugsleistung inkl. Verpackung", vatLabel, "450,00 €" },
|
||||||
{ "Entsorgung Möbel", "19%", "85,00 €" }, { "Montage/De-Montage", "19%", "120,00 €" } };
|
{ "Entsorgung Möbel", vatLabel, "85,00 €" }, { "Montage/De-Montage", vatLabel, "120,00 €" } };
|
||||||
|
|
||||||
// Calculate totals
|
// Calculate totals
|
||||||
double netTotal = 655.00;
|
double netTotal = 655.00;
|
||||||
double grossTotal = 779.45;
|
double grossTotal = netTotal + (netTotal * vatRate.doubleValue());
|
||||||
|
|
||||||
// Wrapper div
|
// Wrapper div
|
||||||
html.append("<div style='width:100%;box-sizing:border-box;'>");
|
html.append("<div style='width:100%;box-sizing:border-box;'>");
|
||||||
@@ -797,7 +811,9 @@ public class CustomerInvoiceService {
|
|||||||
|
|
||||||
// Get invoice data from variables
|
// Get invoice data from variables
|
||||||
String netTotal = variables.getOrDefault("invoice.net_total", "0,00 €");
|
String netTotal = variables.getOrDefault("invoice.net_total", "0,00 €");
|
||||||
|
String vatTotal = variables.getOrDefault("invoice.vat_total", "0,00 €");
|
||||||
String grossTotal = variables.getOrDefault("invoice.gross_total", "0,00 €");
|
String grossTotal = variables.getOrDefault("invoice.gross_total", "0,00 €");
|
||||||
|
String vatRateLabel = variables.getOrDefault("invoice.vat_rate", "19%");
|
||||||
|
|
||||||
// Parse services JSON from variables
|
// Parse services JSON from variables
|
||||||
java.util.List<java.util.Map<String, String>> servicesData = new java.util.ArrayList<>();
|
java.util.List<java.util.Map<String, String>> servicesData = new java.util.ArrayList<>();
|
||||||
@@ -822,7 +838,9 @@ public class CustomerInvoiceService {
|
|||||||
// Header row
|
// Header row
|
||||||
html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>");
|
html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>");
|
||||||
html.append(
|
html.append(
|
||||||
"<th style='text-align:left;padding:4px 8px;font-weight:bold;width:75%;white-space:nowrap;'>Name</th>");
|
"<th style='text-align:left;padding:4px 8px;font-weight:bold;width:55%;white-space:nowrap;'>Name</th>");
|
||||||
|
html.append(
|
||||||
|
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:20%;white-space:nowrap;'>Steuersatz</th>");
|
||||||
html.append(
|
html.append(
|
||||||
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
|
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
@@ -832,7 +850,7 @@ public class CustomerInvoiceService {
|
|||||||
// Fallback: show a single row with no data
|
// Fallback: show a single row with no data
|
||||||
html.append("<tr style='border-bottom:1px solid #eeeeee;'>");
|
html.append("<tr style='border-bottom:1px solid #eeeeee;'>");
|
||||||
html.append(
|
html.append(
|
||||||
"<td colspan='2' style='text-align:center;padding:4px 8px;white-space:nowrap;'>Keine Leistungen vorhanden</td>");
|
"<td colspan='3' style='text-align:center;padding:4px 8px;white-space:nowrap;'>Keine Leistungen vorhanden</td>");
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
} else {
|
} else {
|
||||||
for (int i = 0; i < servicesData.size(); i++) {
|
for (int i = 0; i < servicesData.size(); i++) {
|
||||||
@@ -843,8 +861,10 @@ public class CustomerInvoiceService {
|
|||||||
String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : "";
|
String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : "";
|
||||||
html.append("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>");
|
html.append("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>");
|
||||||
html.append(
|
html.append(
|
||||||
"<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:75%;'>")
|
"<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:55%;'>")
|
||||||
.append(escapeHtml(name)).append("</td>");
|
.append(escapeHtml(name)).append("</td>");
|
||||||
|
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;width:20%;'>")
|
||||||
|
.append(escapeHtml(vatRateLabel)).append("</td>");
|
||||||
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;width:25%;'>")
|
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;width:25%;'>")
|
||||||
.append(netAmount).append(" €</td>");
|
.append(netAmount).append(" €</td>");
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
@@ -865,6 +885,15 @@ public class CustomerInvoiceService {
|
|||||||
.append(netTotal).append("</td>");
|
.append(netTotal).append("</td>");
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
|
|
||||||
|
// Umsatzsteuer
|
||||||
|
html.append("<tr>");
|
||||||
|
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
||||||
|
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>zzgl. ")
|
||||||
|
.append(escapeHtml(vatRateLabel)).append(" USt:</td>");
|
||||||
|
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>")
|
||||||
|
.append(vatTotal).append("</td>");
|
||||||
|
html.append("</tr>");
|
||||||
|
|
||||||
// Gesamtsumme
|
// Gesamtsumme
|
||||||
html.append("<tr>");
|
html.append("<tr>");
|
||||||
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package de.assecutor.votianlt.service;
|
package de.assecutor.votianlt.service;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.AppUser;
|
||||||
import de.assecutor.votianlt.model.Job;
|
import de.assecutor.votianlt.model.Job;
|
||||||
import de.assecutor.votianlt.model.User;
|
import de.assecutor.votianlt.model.User;
|
||||||
|
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||||
import de.assecutor.votianlt.repository.JobRepository;
|
import de.assecutor.votianlt.repository.JobRepository;
|
||||||
import de.assecutor.votianlt.repository.UserRepository;
|
import de.assecutor.votianlt.repository.UserRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -22,6 +24,7 @@ public class EmailService {
|
|||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final JobRepository jobRepository;
|
private final JobRepository jobRepository;
|
||||||
private final TaskAssignmentService taskAssignmentService;
|
private final TaskAssignmentService taskAssignmentService;
|
||||||
|
private final AppUserRepository appUserRepository;
|
||||||
private final JavaMailSender mailSender;
|
private final JavaMailSender mailSender;
|
||||||
|
|
||||||
@Value("${spring.mail.username}")
|
@Value("${spring.mail.username}")
|
||||||
@@ -52,8 +55,10 @@ public class EmailService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String completedByName = resolveCompletedByName(job, completedBy);
|
||||||
|
|
||||||
// Send email
|
// Send email
|
||||||
sendEmail(user, job, taskType);
|
sendEmail(user, job, taskType, completedByName);
|
||||||
log.info("Task completion notification sent to {} for job {} task {}", user.getEmail(), job.getJobNumber(),
|
log.info("Task completion notification sent to {} for job {} task {}", user.getEmail(), job.getJobNumber(),
|
||||||
taskId);
|
taskId);
|
||||||
|
|
||||||
@@ -63,7 +68,7 @@ public class EmailService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendEmail(User user, Job job, String taskType) {
|
private void sendEmail(User user, Job job, String taskType, String completedByName) {
|
||||||
SimpleMailMessage message = new SimpleMailMessage();
|
SimpleMailMessage message = new SimpleMailMessage();
|
||||||
message.setFrom(smtpUsername);
|
message.setFrom(smtpUsername);
|
||||||
message.setTo(user.getEmail());
|
message.setTo(user.getEmail());
|
||||||
@@ -71,18 +76,17 @@ public class EmailService {
|
|||||||
"Aufgabe abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
"Aufgabe abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
||||||
|
|
||||||
String fullName = buildFullName(user);
|
String fullName = buildFullName(user);
|
||||||
String appUserName = buildAppUserName(user);
|
|
||||||
String taskTypeName = getTaskTypeDisplayName(taskType);
|
String taskTypeName = getTaskTypeDisplayName(taskType);
|
||||||
|
|
||||||
StringBuilder body = new StringBuilder();
|
StringBuilder body = new StringBuilder();
|
||||||
body.append("Hallo ").append(fullName).append(",\n\n");
|
body.append("Hallo ").append(fullName).append(",\n\n");
|
||||||
body.append("eine Aufgabe wurde von ").append(appUserName).append(" abgeschlossen:\n\n");
|
body.append("eine Aufgabe wurde von ").append(completedByName).append(" abgeschlossen:\n\n");
|
||||||
body.append("Job: ").append(job.getJobNumber() != null ? job.getJobNumber() : "Unbekannt").append("\n");
|
body.append("Job: ").append(job.getJobNumber() != null ? job.getJobNumber() : "Unbekannt").append("\n");
|
||||||
if (job.getDeliveryCompany() != null) {
|
if (job.getDeliveryCompany() != null) {
|
||||||
body.append("Kunde: ").append(job.getDeliveryCompany()).append("\n");
|
body.append("Kunde: ").append(job.getDeliveryCompany()).append("\n");
|
||||||
}
|
}
|
||||||
body.append("Aufgabe: ").append(taskTypeName).append("\n");
|
body.append("Aufgabe: ").append(taskTypeName).append("\n");
|
||||||
body.append("Abgeschlossen von: ").append(appUserName).append("\n\n");
|
body.append("Abgeschlossen von: ").append(completedByName).append("\n\n");
|
||||||
|
|
||||||
String deliveryCities = job.getDeliveryCitiesDisplay();
|
String deliveryCities = job.getDeliveryCitiesDisplay();
|
||||||
if (job.getPickupCity() != null || deliveryCities != null) {
|
if (job.getPickupCity() != null || deliveryCities != null) {
|
||||||
@@ -121,16 +125,55 @@ public class EmailService {
|
|||||||
return fullName.isEmpty() ? "Benutzer" : fullName;
|
return fullName.isEmpty() ? "Benutzer" : fullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildAppUserName(User user) {
|
private String buildAppUserName(AppUser appUser) {
|
||||||
StringBuilder name = new StringBuilder();
|
StringBuilder name = new StringBuilder();
|
||||||
if (user.getFirstname() != null && !user.getFirstname().isBlank()) {
|
if (appUser.getVorname() != null && !appUser.getVorname().isBlank()) {
|
||||||
name.append(user.getFirstname()).append(" ");
|
name.append(appUser.getVorname()).append(" ");
|
||||||
}
|
}
|
||||||
if (user.getName() != null && !user.getName().isBlank()) {
|
if (appUser.getNachname() != null && !appUser.getNachname().isBlank()) {
|
||||||
name.append(user.getName());
|
name.append(appUser.getNachname());
|
||||||
}
|
}
|
||||||
|
|
||||||
String fullName = name.toString().trim();
|
String fullName = name.toString().trim();
|
||||||
return fullName.isEmpty() ? "App-Benutzer" : fullName;
|
if (!fullName.isEmpty()) {
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
if (appUser.getBezeichnung() != null && !appUser.getBezeichnung().isBlank()) {
|
||||||
|
return appUser.getBezeichnung().trim();
|
||||||
|
}
|
||||||
|
if (appUser.getEmail() != null && !appUser.getEmail().isBlank()) {
|
||||||
|
return appUser.getEmail().trim();
|
||||||
|
}
|
||||||
|
return "App-Benutzer";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveCompletedByName(Job job, String completedBy) {
|
||||||
|
Optional<AppUser> assignedAppUser = findAppUserById(job != null ? job.getAppUser() : null);
|
||||||
|
if (assignedAppUser.isPresent()) {
|
||||||
|
return buildAppUserName(assignedAppUser.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completedBy != null && !completedBy.isBlank() && !"Unknown".equalsIgnoreCase(completedBy)) {
|
||||||
|
Optional<AppUser> completingAppUser = findAppUserById(completedBy);
|
||||||
|
if (completingAppUser.isPresent()) {
|
||||||
|
return buildAppUserName(completingAppUser.get());
|
||||||
|
}
|
||||||
|
return completedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "App-Benutzer";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<AppUser> findAppUserById(String appUserId) {
|
||||||
|
if (appUserId == null || appUserId.isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return appUserRepository.findById(new ObjectId(appUserId));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getTaskTypeDisplayName(String taskType) {
|
private String getTaskTypeDisplayName(String taskType) {
|
||||||
@@ -173,8 +216,10 @@ public class EmailService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String completedByName = resolveCompletedByName(job, completedBy);
|
||||||
|
|
||||||
// Send job completion email
|
// Send job completion email
|
||||||
sendJobCompletionEmail(user, job);
|
sendJobCompletionEmail(user, job, completedByName);
|
||||||
log.info("Job completion notification sent to {} for job {}", user.getEmail(), job.getJobNumber());
|
log.info("Job completion notification sent to {} for job {}", user.getEmail(), job.getJobNumber());
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -182,7 +227,7 @@ public class EmailService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendJobCompletionEmail(User user, Job job) {
|
private void sendJobCompletionEmail(User user, Job job, String completedByName) {
|
||||||
SimpleMailMessage message = new SimpleMailMessage();
|
SimpleMailMessage message = new SimpleMailMessage();
|
||||||
message.setFrom(smtpUsername);
|
message.setFrom(smtpUsername);
|
||||||
message.setTo(user.getEmail());
|
message.setTo(user.getEmail());
|
||||||
@@ -190,7 +235,6 @@ public class EmailService {
|
|||||||
"Job abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
"Job abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
||||||
|
|
||||||
String fullName = buildFullName(user);
|
String fullName = buildFullName(user);
|
||||||
String appUserName = buildAppUserName(user);
|
|
||||||
|
|
||||||
// Count completed tasks
|
// Count completed tasks
|
||||||
var allTasks = taskAssignmentService.findTasksForJob(job);
|
var allTasks = taskAssignmentService.findTasksForJob(job);
|
||||||
@@ -220,7 +264,7 @@ public class EmailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.append("Anzahl erledigter Aufgaben: ").append(taskCount).append("\n");
|
body.append("Anzahl erledigter Aufgaben: ").append(taskCount).append("\n");
|
||||||
body.append("Abgeschlossen von: ").append(appUserName).append("\n\n");
|
body.append("Abgeschlossen von: ").append(completedByName).append("\n\n");
|
||||||
|
|
||||||
body.append("Der Job ist nun vollständig erledigt und kann weiterverarbeitet werden.\n\n");
|
body.append("Der Job ist nun vollständig erledigt und kann weiterverarbeitet werden.\n\n");
|
||||||
body.append("Mit freundlichen Grüßen,\n");
|
body.append("Mit freundlichen Grüßen,\n");
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ app.security.two-factor.enabled=true
|
|||||||
|
|
||||||
# WebSocket Configuration
|
# WebSocket Configuration
|
||||||
app.messaging.websocket.path=/ws/messaging
|
app.messaging.websocket.path=/ws/messaging
|
||||||
app.messaging.websocket.max-text-message-size=65536
|
app.messaging.websocket.max-text-message-size=10485760
|
||||||
app.messaging.websocket.max-session-idle-timeout=300000
|
app.messaging.websocket.max-session-idle-timeout=300000
|
||||||
app.messaging.websocket.allowed-origins=*
|
app.messaging.websocket.allowed-origins=*
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ dialog.confirm=Bestätigen
|
|||||||
# Navigation and Main Layout
|
# Navigation and Main Layout
|
||||||
nav.jobs=Aufträge
|
nav.jobs=Aufträge
|
||||||
nav.job.create=Auftragserstellung
|
nav.job.create=Auftragserstellung
|
||||||
nav.customers=Kunden
|
nav.customers=Adressbuch
|
||||||
nav.appusers=App-Nutzer
|
nav.appusers=App-Nutzer
|
||||||
nav.statistics=Statistiken
|
nav.statistics=Statistiken
|
||||||
nav.invoices=Rechnungen
|
nav.invoices=Rechnungen
|
||||||
@@ -31,7 +31,7 @@ profile.lastname=Nachname
|
|||||||
profile.phone=Telefonnummer
|
profile.phone=Telefonnummer
|
||||||
profile.fax=Telefon (Fax)
|
profile.fax=Telefon (Fax)
|
||||||
profile.mobile=Telefon (Mobil)
|
profile.mobile=Telefon (Mobil)
|
||||||
profile.email=E-Mail-Adresse (Login)*
|
profile.email=E-Mail-Adresse
|
||||||
profile.street=Straße
|
profile.street=Straße
|
||||||
profile.housenr=Hausnr
|
profile.housenr=Hausnr
|
||||||
profile.addressadd=Adresszusatz
|
profile.addressadd=Adresszusatz
|
||||||
@@ -46,6 +46,7 @@ profile.settings.digitalprocess=Digitale Abwicklung
|
|||||||
profile.settings.digitalprocess.info=Aufträge werden digital über die App abgewickelt
|
profile.settings.digitalprocess.info=Aufträge werden digital über die App abgewickelt
|
||||||
profile.settings.locateappuser=App-Nutzer orten
|
profile.settings.locateappuser=App-Nutzer orten
|
||||||
profile.settings.locateappuser.info=Standort der App-Nutzer wird regelmäßig übertragen
|
profile.settings.locateappuser.info=Standort der App-Nutzer wird regelmäßig übertragen
|
||||||
|
profile.settings.vatrate=Umsatzsteuer
|
||||||
profile.account=Konto
|
profile.account=Konto
|
||||||
profile.security=Sicherheit
|
profile.security=Sicherheit
|
||||||
profile.security.twofactor=Zwei-Faktor-Authentifizierung
|
profile.security.twofactor=Zwei-Faktor-Authentifizierung
|
||||||
@@ -447,7 +448,8 @@ addjob.address.city.placeholder.pickup=Ort (Abholung)
|
|||||||
addjob.address.city.placeholder.delivery=Ort (Lieferung)
|
addjob.address.city.placeholder.delivery=Ort (Lieferung)
|
||||||
addjob.address.delivery.street.placeholder=Straße (Lieferung)
|
addjob.address.delivery.street.placeholder=Straße (Lieferung)
|
||||||
addjob.address.delivery.addition.placeholder=Adresszusatz (Lieferung)
|
addjob.address.delivery.addition.placeholder=Adresszusatz (Lieferung)
|
||||||
addjob.address.save=Adresse speichern
|
addjob.address.save=Adresse in Adressbuch übernehmen
|
||||||
|
addjob.address.update=Adresse im Adressbuch aktualisieren
|
||||||
addjob.section.pickup=Abholung
|
addjob.section.pickup=Abholung
|
||||||
addjob.section.delivery=Lieferung
|
addjob.section.delivery=Lieferung
|
||||||
addjob.stations.apply=Stationen \u00fcbernehmen
|
addjob.stations.apply=Stationen \u00fcbernehmen
|
||||||
@@ -513,7 +515,8 @@ addjob.tasks.photo.min=Min. Fotos
|
|||||||
addjob.tasks.photo.max=Max. Fotos
|
addjob.tasks.photo.max=Max. Fotos
|
||||||
addjob.tasks.barcode.min=Min. Barcodes
|
addjob.tasks.barcode.min=Min. Barcodes
|
||||||
addjob.tasks.barcode.max=Max. Barcodes
|
addjob.tasks.barcode.max=Max. Barcodes
|
||||||
addjob.tasks.signature.noconfig=Keine Konfiguration erforderlich
|
addjob.tasks.signature.notelabel=Bemerkung (optional)
|
||||||
|
addjob.tasks.signature.notelabel.placeholder=Hinweistext für die Bemerkung eingeben
|
||||||
addjob.tasks.todolist.title=To-Do Liste
|
addjob.tasks.todolist.title=To-Do Liste
|
||||||
addjob.tasks.todolist.item.placeholder=To-Do eingeben
|
addjob.tasks.todolist.item.placeholder=To-Do eingeben
|
||||||
addjob.tasks.todolist.add=To-Do hinzufügen
|
addjob.tasks.todolist.add=To-Do hinzufügen
|
||||||
@@ -610,6 +613,14 @@ jobsummary.task.photo.taken=Aufgenommene Fotos ({0})
|
|||||||
jobsummary.task.button.text=Button-Text
|
jobsummary.task.button.text=Button-Text
|
||||||
jobsummary.button.schliessen=Schließen
|
jobsummary.button.schliessen=Schließen
|
||||||
jobsummary.route.planned=Geplante Route
|
jobsummary.route.planned=Geplante Route
|
||||||
|
jobsummary.button.manualcomplete=Manuell beenden
|
||||||
|
jobsummary.dialog.manualcomplete.title=Auftrag manuell beenden
|
||||||
|
jobsummary.dialog.manualcomplete.text=Der Auftrag {0} wird jetzt manuell abgeschlossen. Danach kann er nicht mehr per App weiter bearbeitet werden.
|
||||||
|
jobsummary.dialog.manualcomplete.reason=Begründung
|
||||||
|
jobsummary.dialog.manualcomplete.reason.required=Bitte geben Sie eine Begründung ein
|
||||||
|
jobsummary.dialog.manualcomplete.cancel=Abbrechen
|
||||||
|
jobsummary.dialog.manualcomplete.confirm=Akzeptiert
|
||||||
|
jobsummary.history.manualcomplete.reason=Manuell beendet
|
||||||
|
|
||||||
# Jobs
|
# Jobs
|
||||||
jobs.title=Aufträge
|
jobs.title=Aufträge
|
||||||
@@ -652,6 +663,8 @@ createinvoice.section.job=Auftragsdetails
|
|||||||
createinvoice.section.route=Streckeninfo
|
createinvoice.section.route=Streckeninfo
|
||||||
createinvoice.section.services=Leistungen
|
createinvoice.section.services=Leistungen
|
||||||
createinvoice.section.summary=Zusammenfassung
|
createinvoice.section.summary=Zusammenfassung
|
||||||
|
createinvoice.section.vat=Umsatzsteuer
|
||||||
|
createinvoice.field.vatrate=USt-Satz
|
||||||
createinvoice.field.jobnumber=Auftragsnummer
|
createinvoice.field.jobnumber=Auftragsnummer
|
||||||
createinvoice.field.customer=Kunde
|
createinvoice.field.customer=Kunde
|
||||||
createinvoice.field.status=Status
|
createinvoice.field.status=Status
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ dialog.cancel=T\u00fchista
|
|||||||
dialog.confirm=Kinnita
|
dialog.confirm=Kinnita
|
||||||
nav.jobs=Tellimused
|
nav.jobs=Tellimused
|
||||||
nav.job.create=Tellimuse loomine
|
nav.job.create=Tellimuse loomine
|
||||||
nav.customers=Kliendid
|
nav.customers=Aadressiraamat
|
||||||
nav.appusers=\u00c4pikasutajad
|
nav.appusers=\u00c4pikasutajad
|
||||||
nav.statistics=Statistika
|
nav.statistics=Statistika
|
||||||
nav.invoices=Arved
|
nav.invoices=Arved
|
||||||
@@ -27,7 +27,7 @@ profile.lastname=Perekonnanimi
|
|||||||
profile.phone=Telefoninumber
|
profile.phone=Telefoninumber
|
||||||
profile.fax=Telefon (faks)
|
profile.fax=Telefon (faks)
|
||||||
profile.mobile=Telefon (mobiil)
|
profile.mobile=Telefon (mobiil)
|
||||||
profile.email=E-posti aadress (sisselogimine)*
|
profile.email=E-posti aadress
|
||||||
profile.street=T\u00e4nav
|
profile.street=T\u00e4nav
|
||||||
profile.housenr=Majanumber
|
profile.housenr=Majanumber
|
||||||
profile.addressadd=Aadressi t\u00e4iend
|
profile.addressadd=Aadressi t\u00e4iend
|
||||||
@@ -396,7 +396,8 @@ addjob.address.city.placeholder.pickup=Asukoht (pealekorje)
|
|||||||
addjob.address.city.placeholder.delivery=Asukoht (kohaletoimetamine)
|
addjob.address.city.placeholder.delivery=Asukoht (kohaletoimetamine)
|
||||||
addjob.address.delivery.street.placeholder=T\u00e4nav (kohaletoimetamine)
|
addjob.address.delivery.street.placeholder=T\u00e4nav (kohaletoimetamine)
|
||||||
addjob.address.delivery.addition.placeholder=Aadressi t\u00e4iend (kohaletoimetamine)
|
addjob.address.delivery.addition.placeholder=Aadressi t\u00e4iend (kohaletoimetamine)
|
||||||
addjob.address.save=Salvesta aadress
|
addjob.address.save=Lisa aadress aadressiraamatusse
|
||||||
|
addjob.address.update=Uuenda aadressi aadressiraamatus
|
||||||
addjob.section.pickup=Pealekorje
|
addjob.section.pickup=Pealekorje
|
||||||
addjob.section.delivery=Kohaletoimetamine
|
addjob.section.delivery=Kohaletoimetamine
|
||||||
addjob.stations.apply=Rakenda jaamad
|
addjob.stations.apply=Rakenda jaamad
|
||||||
@@ -462,7 +463,8 @@ addjob.tasks.photo.min=Min. fotosid
|
|||||||
addjob.tasks.photo.max=Max. fotosid
|
addjob.tasks.photo.max=Max. fotosid
|
||||||
addjob.tasks.barcode.min=Min. v\u00f6\u00f6tkoode
|
addjob.tasks.barcode.min=Min. v\u00f6\u00f6tkoode
|
||||||
addjob.tasks.barcode.max=Max. v\u00f6\u00f6tkoode
|
addjob.tasks.barcode.max=Max. v\u00f6\u00f6tkoode
|
||||||
addjob.tasks.signature.noconfig=Seadistamine pole vajalik
|
addjob.tasks.signature.notelabel=Märkus (valikuline)
|
||||||
|
addjob.tasks.signature.notelabel.placeholder=Sisestage vihje tekst märkusele
|
||||||
addjob.tasks.todolist.title=\u00dclesannete nimekiri
|
addjob.tasks.todolist.title=\u00dclesannete nimekiri
|
||||||
addjob.tasks.todolist.item.placeholder=Sisestage \u00fclesanne
|
addjob.tasks.todolist.item.placeholder=Sisestage \u00fclesanne
|
||||||
addjob.tasks.todolist.add=Lisa \u00fclesanne
|
addjob.tasks.todolist.add=Lisa \u00fclesanne
|
||||||
@@ -557,6 +559,14 @@ jobsummary.task.photo.taken=Tehtud fotod ({0})
|
|||||||
jobsummary.task.button.text=Nupu tekst
|
jobsummary.task.button.text=Nupu tekst
|
||||||
jobsummary.button.schliessen=Sulge
|
jobsummary.button.schliessen=Sulge
|
||||||
jobsummary.route.planned=Planeeritud marsruut
|
jobsummary.route.planned=Planeeritud marsruut
|
||||||
|
jobsummary.button.manualcomplete=Lõpeta käsitsi
|
||||||
|
jobsummary.dialog.manualcomplete.title=Lõpeta tellimus käsitsi
|
||||||
|
jobsummary.dialog.manualcomplete.text=Tellimus {0} lõpetatakse nüüd käsitsi. Pärast seda ei saa seda enam rakenduse kaudu töödelda.
|
||||||
|
jobsummary.dialog.manualcomplete.reason=Põhjendus
|
||||||
|
jobsummary.dialog.manualcomplete.reason.required=Palun sisestage põhjendus
|
||||||
|
jobsummary.dialog.manualcomplete.cancel=Tühista
|
||||||
|
jobsummary.dialog.manualcomplete.confirm=Nõustu
|
||||||
|
jobsummary.history.manualcomplete.reason=Käsitsi lõpetatud
|
||||||
jobs.title=Tellimused
|
jobs.title=Tellimused
|
||||||
jobs.filter.search=Otsi
|
jobs.filter.search=Otsi
|
||||||
jobs.filter.search.placeholder=Otsi tellimuse numbri j\u00e4rgi...
|
jobs.filter.search.placeholder=Otsi tellimuse numbri j\u00e4rgi...
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ dialog.confirm=Confirm
|
|||||||
# Navigation and Main Layout
|
# Navigation and Main Layout
|
||||||
nav.jobs=Jobs
|
nav.jobs=Jobs
|
||||||
nav.job.create=Create Job
|
nav.job.create=Create Job
|
||||||
nav.customers=Customers
|
nav.customers=Address Book
|
||||||
nav.appusers=App Users
|
nav.appusers=App Users
|
||||||
nav.statistics=Statistics
|
nav.statistics=Statistics
|
||||||
nav.invoices=Invoices
|
nav.invoices=Invoices
|
||||||
@@ -31,7 +31,7 @@ profile.lastname=Last Name
|
|||||||
profile.phone=Phone Number
|
profile.phone=Phone Number
|
||||||
profile.fax=Phone (Fax)
|
profile.fax=Phone (Fax)
|
||||||
profile.mobile=Phone (Mobile)
|
profile.mobile=Phone (Mobile)
|
||||||
profile.email=Email Address (Login)*
|
profile.email=Email Address
|
||||||
profile.street=Street
|
profile.street=Street
|
||||||
profile.housenr=House No.
|
profile.housenr=House No.
|
||||||
profile.addressadd=Address Suffix
|
profile.addressadd=Address Suffix
|
||||||
@@ -46,6 +46,7 @@ profile.settings.digitalprocess=Digital Processing
|
|||||||
profile.settings.digitalprocess.info=Jobs are processed digitally via the app
|
profile.settings.digitalprocess.info=Jobs are processed digitally via the app
|
||||||
profile.settings.locateappuser=Locate App Users
|
profile.settings.locateappuser=Locate App Users
|
||||||
profile.settings.locateappuser.info=App user location is transmitted regularly
|
profile.settings.locateappuser.info=App user location is transmitted regularly
|
||||||
|
profile.settings.vatrate=VAT rate
|
||||||
profile.account=Account
|
profile.account=Account
|
||||||
profile.security=Security
|
profile.security=Security
|
||||||
profile.security.twofactor=Two-Factor Authentication
|
profile.security.twofactor=Two-Factor Authentication
|
||||||
@@ -447,7 +448,8 @@ addjob.address.city.placeholder.pickup=City (Pickup)
|
|||||||
addjob.address.city.placeholder.delivery=City (Delivery)
|
addjob.address.city.placeholder.delivery=City (Delivery)
|
||||||
addjob.address.delivery.street.placeholder=Street (Delivery)
|
addjob.address.delivery.street.placeholder=Street (Delivery)
|
||||||
addjob.address.delivery.addition.placeholder=Address suffix (Delivery)
|
addjob.address.delivery.addition.placeholder=Address suffix (Delivery)
|
||||||
addjob.address.save=Save Address
|
addjob.address.save=Add address to address book
|
||||||
|
addjob.address.update=Update address in address book
|
||||||
addjob.section.pickup=Pickup
|
addjob.section.pickup=Pickup
|
||||||
addjob.section.delivery=Delivery
|
addjob.section.delivery=Delivery
|
||||||
addjob.stations.apply=Apply Stations
|
addjob.stations.apply=Apply Stations
|
||||||
@@ -513,7 +515,8 @@ addjob.tasks.photo.min=Min. Photos
|
|||||||
addjob.tasks.photo.max=Max. Photos
|
addjob.tasks.photo.max=Max. Photos
|
||||||
addjob.tasks.barcode.min=Min. Barcodes
|
addjob.tasks.barcode.min=Min. Barcodes
|
||||||
addjob.tasks.barcode.max=Max. Barcodes
|
addjob.tasks.barcode.max=Max. Barcodes
|
||||||
addjob.tasks.signature.noconfig=No configuration required
|
addjob.tasks.signature.notelabel=Note (optional)
|
||||||
|
addjob.tasks.signature.notelabel.placeholder=Enter hint text for the note
|
||||||
addjob.tasks.todolist.title=To-Do List
|
addjob.tasks.todolist.title=To-Do List
|
||||||
addjob.tasks.todolist.item.placeholder=Enter to-do
|
addjob.tasks.todolist.item.placeholder=Enter to-do
|
||||||
addjob.tasks.todolist.add=Add To-Do
|
addjob.tasks.todolist.add=Add To-Do
|
||||||
@@ -610,6 +613,14 @@ jobsummary.task.photo.taken=Photos taken ({0})
|
|||||||
jobsummary.task.button.text=Button Text
|
jobsummary.task.button.text=Button Text
|
||||||
jobsummary.button.schliessen=Close
|
jobsummary.button.schliessen=Close
|
||||||
jobsummary.route.planned=Planned Route
|
jobsummary.route.planned=Planned Route
|
||||||
|
jobsummary.button.manualcomplete=Complete manually
|
||||||
|
jobsummary.dialog.manualcomplete.title=Complete job manually
|
||||||
|
jobsummary.dialog.manualcomplete.text=Job {0} will now be completed manually. It can no longer be processed via the app afterwards.
|
||||||
|
jobsummary.dialog.manualcomplete.reason=Reason
|
||||||
|
jobsummary.dialog.manualcomplete.reason.required=Please enter a reason
|
||||||
|
jobsummary.dialog.manualcomplete.cancel=Cancel
|
||||||
|
jobsummary.dialog.manualcomplete.confirm=Accept
|
||||||
|
jobsummary.history.manualcomplete.reason=Manually completed
|
||||||
|
|
||||||
# Jobs
|
# Jobs
|
||||||
jobs.title=Jobs
|
jobs.title=Jobs
|
||||||
@@ -652,6 +663,8 @@ createinvoice.section.job=Job Details
|
|||||||
createinvoice.section.route=Route Info
|
createinvoice.section.route=Route Info
|
||||||
createinvoice.section.services=Services
|
createinvoice.section.services=Services
|
||||||
createinvoice.section.summary=Summary
|
createinvoice.section.summary=Summary
|
||||||
|
createinvoice.section.vat=VAT
|
||||||
|
createinvoice.field.vatrate=VAT rate
|
||||||
createinvoice.field.jobnumber=Job Number
|
createinvoice.field.jobnumber=Job Number
|
||||||
createinvoice.field.customer=Customer
|
createinvoice.field.customer=Customer
|
||||||
createinvoice.field.status=Status
|
createinvoice.field.status=Status
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ dialog.confirm=Confirmar
|
|||||||
# Navigation and Main Layout
|
# Navigation and Main Layout
|
||||||
nav.jobs=Pedidos
|
nav.jobs=Pedidos
|
||||||
nav.job.create=Crear pedido
|
nav.job.create=Crear pedido
|
||||||
nav.customers=Clientes
|
nav.customers=Libreta de direcciones
|
||||||
nav.appusers=Usuarios de la app
|
nav.appusers=Usuarios de la app
|
||||||
nav.statistics=Estad\u00edsticas
|
nav.statistics=Estad\u00edsticas
|
||||||
nav.invoices=Facturas
|
nav.invoices=Facturas
|
||||||
@@ -31,7 +31,7 @@ profile.lastname=Apellido
|
|||||||
profile.phone=N\u00famero de tel\u00e9fono
|
profile.phone=N\u00famero de tel\u00e9fono
|
||||||
profile.fax=Tel\u00e9fono (Fax)
|
profile.fax=Tel\u00e9fono (Fax)
|
||||||
profile.mobile=Tel\u00e9fono (M\u00f3vil)
|
profile.mobile=Tel\u00e9fono (M\u00f3vil)
|
||||||
profile.email=Direcci\u00f3n de correo electr\u00f3nico (Login)*
|
profile.email=Direcci\u00f3n de correo electr\u00f3nico
|
||||||
profile.street=Calle
|
profile.street=Calle
|
||||||
profile.housenr=N\u00famero
|
profile.housenr=N\u00famero
|
||||||
profile.addressadd=Complemento de direcci\u00f3n
|
profile.addressadd=Complemento de direcci\u00f3n
|
||||||
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Localidad (Recogida)
|
|||||||
addjob.address.city.placeholder.delivery=Localidad (Entrega)
|
addjob.address.city.placeholder.delivery=Localidad (Entrega)
|
||||||
addjob.address.delivery.street.placeholder=Calle (Entrega)
|
addjob.address.delivery.street.placeholder=Calle (Entrega)
|
||||||
addjob.address.delivery.addition.placeholder=Complemento de direcci\u00f3n (Entrega)
|
addjob.address.delivery.addition.placeholder=Complemento de direcci\u00f3n (Entrega)
|
||||||
addjob.address.save=Guardar direcci\u00f3n
|
addjob.address.save=A\u00f1adir direcci\u00f3n a la libreta de direcciones
|
||||||
|
addjob.address.update=Actualizar direcci\u00f3n en la libreta de direcciones
|
||||||
addjob.section.pickup=Recogida
|
addjob.section.pickup=Recogida
|
||||||
addjob.section.delivery=Entrega
|
addjob.section.delivery=Entrega
|
||||||
addjob.stations.apply=Aplicar estaciones
|
addjob.stations.apply=Aplicar estaciones
|
||||||
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=M\u00edn. fotos
|
|||||||
addjob.tasks.photo.max=M\u00e1x. fotos
|
addjob.tasks.photo.max=M\u00e1x. fotos
|
||||||
addjob.tasks.barcode.min=M\u00edn. c\u00f3digos de barras
|
addjob.tasks.barcode.min=M\u00edn. c\u00f3digos de barras
|
||||||
addjob.tasks.barcode.max=M\u00e1x. c\u00f3digos de barras
|
addjob.tasks.barcode.max=M\u00e1x. c\u00f3digos de barras
|
||||||
addjob.tasks.signature.noconfig=No se requiere configuraci\u00f3n
|
addjob.tasks.signature.notelabel=Nota (opcional)
|
||||||
|
addjob.tasks.signature.notelabel.placeholder=Introducir texto de sugerencia para la nota
|
||||||
addjob.tasks.todolist.title=Lista de tareas pendientes
|
addjob.tasks.todolist.title=Lista de tareas pendientes
|
||||||
addjob.tasks.todolist.item.placeholder=Introducir tarea pendiente
|
addjob.tasks.todolist.item.placeholder=Introducir tarea pendiente
|
||||||
addjob.tasks.todolist.add=A\u00f1adir tarea pendiente
|
addjob.tasks.todolist.add=A\u00f1adir tarea pendiente
|
||||||
@@ -610,6 +612,14 @@ jobsummary.task.photo.taken=Fotos tomadas ({0})
|
|||||||
jobsummary.task.button.text=Texto del bot\u00f3n
|
jobsummary.task.button.text=Texto del bot\u00f3n
|
||||||
jobsummary.button.schliessen=Cerrar
|
jobsummary.button.schliessen=Cerrar
|
||||||
jobsummary.route.planned=Ruta planificada
|
jobsummary.route.planned=Ruta planificada
|
||||||
|
jobsummary.button.manualcomplete=Finalizar manualmente
|
||||||
|
jobsummary.dialog.manualcomplete.title=Finalizar pedido manualmente
|
||||||
|
jobsummary.dialog.manualcomplete.text=El pedido {0} se completar\u00e1 manualmente. Despu\u00e9s ya no podr\u00e1 ser procesado a trav\u00e9s de la aplicaci\u00f3n.
|
||||||
|
jobsummary.dialog.manualcomplete.reason=Motivo
|
||||||
|
jobsummary.dialog.manualcomplete.reason.required=Por favor, introduzca un motivo
|
||||||
|
jobsummary.dialog.manualcomplete.cancel=Cancelar
|
||||||
|
jobsummary.dialog.manualcomplete.confirm=Aceptar
|
||||||
|
jobsummary.history.manualcomplete.reason=Finalizado manualmente
|
||||||
|
|
||||||
# Jobs
|
# Jobs
|
||||||
jobs.title=Pedidos
|
jobs.title=Pedidos
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ dialog.confirm=Confirmer
|
|||||||
# Navigation and Main Layout
|
# Navigation and Main Layout
|
||||||
nav.jobs=Missions
|
nav.jobs=Missions
|
||||||
nav.job.create=Cr\u00e9ation de mission
|
nav.job.create=Cr\u00e9ation de mission
|
||||||
nav.customers=Clients
|
nav.customers=Carnet d'adresses
|
||||||
nav.appusers=Utilisateurs d'app
|
nav.appusers=Utilisateurs d'app
|
||||||
nav.statistics=Statistiques
|
nav.statistics=Statistiques
|
||||||
nav.invoices=Factures
|
nav.invoices=Factures
|
||||||
@@ -31,7 +31,7 @@ profile.lastname=Nom
|
|||||||
profile.phone=Num\u00e9ro de t\u00e9l\u00e9phone
|
profile.phone=Num\u00e9ro de t\u00e9l\u00e9phone
|
||||||
profile.fax=T\u00e9l\u00e9phone (fax)
|
profile.fax=T\u00e9l\u00e9phone (fax)
|
||||||
profile.mobile=T\u00e9l\u00e9phone (mobile)
|
profile.mobile=T\u00e9l\u00e9phone (mobile)
|
||||||
profile.email=Adresse e-mail (connexion)*
|
profile.email=Adresse e-mail
|
||||||
profile.street=Rue
|
profile.street=Rue
|
||||||
profile.housenr=N\u00b0
|
profile.housenr=N\u00b0
|
||||||
profile.addressadd=Compl\u00e9ment d'adresse
|
profile.addressadd=Compl\u00e9ment d'adresse
|
||||||
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Ville (enl\u00e8vement)
|
|||||||
addjob.address.city.placeholder.delivery=Ville (livraison)
|
addjob.address.city.placeholder.delivery=Ville (livraison)
|
||||||
addjob.address.delivery.street.placeholder=Rue (livraison)
|
addjob.address.delivery.street.placeholder=Rue (livraison)
|
||||||
addjob.address.delivery.addition.placeholder=Compl\u00e9ment d'adresse (livraison)
|
addjob.address.delivery.addition.placeholder=Compl\u00e9ment d'adresse (livraison)
|
||||||
addjob.address.save=Enregistrer l'adresse
|
addjob.address.save=Ajouter l'adresse au carnet d'adresses
|
||||||
|
addjob.address.update=Mettre \u00e0 jour l'adresse dans le carnet d'adresses
|
||||||
addjob.section.pickup=Enl\u00e8vement
|
addjob.section.pickup=Enl\u00e8vement
|
||||||
addjob.section.delivery=Livraison
|
addjob.section.delivery=Livraison
|
||||||
addjob.stations.apply=Appliquer les stations
|
addjob.stations.apply=Appliquer les stations
|
||||||
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. photos
|
|||||||
addjob.tasks.photo.max=Max. photos
|
addjob.tasks.photo.max=Max. photos
|
||||||
addjob.tasks.barcode.min=Min. codes-barres
|
addjob.tasks.barcode.min=Min. codes-barres
|
||||||
addjob.tasks.barcode.max=Max. codes-barres
|
addjob.tasks.barcode.max=Max. codes-barres
|
||||||
addjob.tasks.signature.noconfig=Aucune configuration n\u00e9cessaire
|
addjob.tasks.signature.notelabel=Note (optionnelle)
|
||||||
|
addjob.tasks.signature.notelabel.placeholder=Saisir le texte d'indication pour la note
|
||||||
addjob.tasks.todolist.title=Liste de t\u00e2ches
|
addjob.tasks.todolist.title=Liste de t\u00e2ches
|
||||||
addjob.tasks.todolist.item.placeholder=Saisir la t\u00e2che
|
addjob.tasks.todolist.item.placeholder=Saisir la t\u00e2che
|
||||||
addjob.tasks.todolist.add=Ajouter une t\u00e2che
|
addjob.tasks.todolist.add=Ajouter une t\u00e2che
|
||||||
@@ -610,6 +612,14 @@ jobsummary.task.photo.taken=Photos prises ({0})
|
|||||||
jobsummary.task.button.text=Texte du bouton
|
jobsummary.task.button.text=Texte du bouton
|
||||||
jobsummary.button.schliessen=Fermer
|
jobsummary.button.schliessen=Fermer
|
||||||
jobsummary.route.planned=Itin\u00e9raire pr\u00e9vu
|
jobsummary.route.planned=Itin\u00e9raire pr\u00e9vu
|
||||||
|
jobsummary.button.manualcomplete=Terminer manuellement
|
||||||
|
jobsummary.dialog.manualcomplete.title=Terminer la commande manuellement
|
||||||
|
jobsummary.dialog.manualcomplete.text=La commande {0} va maintenant \u00eatre termin\u00e9e manuellement. Elle ne pourra plus \u00eatre trait\u00e9e via l\u2019application par la suite.
|
||||||
|
jobsummary.dialog.manualcomplete.reason=Motif
|
||||||
|
jobsummary.dialog.manualcomplete.reason.required=Veuillez saisir un motif
|
||||||
|
jobsummary.dialog.manualcomplete.cancel=Annuler
|
||||||
|
jobsummary.dialog.manualcomplete.confirm=Accepter
|
||||||
|
jobsummary.history.manualcomplete.reason=Termin\u00e9 manuellement
|
||||||
|
|
||||||
# Jobs
|
# Jobs
|
||||||
jobs.title=Missions
|
jobs.title=Missions
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ dialog.confirm=Patvirtinti
|
|||||||
# Navigation and Main Layout
|
# Navigation and Main Layout
|
||||||
nav.jobs=Užsakymai
|
nav.jobs=Užsakymai
|
||||||
nav.job.create=Užsakymo kūrimas
|
nav.job.create=Užsakymo kūrimas
|
||||||
nav.customers=Klientai
|
nav.customers=Adres\u0173 knyga
|
||||||
nav.appusers=Programėlės naudotojai
|
nav.appusers=Programėlės naudotojai
|
||||||
nav.statistics=Statistika
|
nav.statistics=Statistika
|
||||||
nav.invoices=Sąskaitos faktūros
|
nav.invoices=Sąskaitos faktūros
|
||||||
@@ -31,7 +31,7 @@ profile.lastname=Pavardė
|
|||||||
profile.phone=Telefono numeris
|
profile.phone=Telefono numeris
|
||||||
profile.fax=Telefonas (faksas)
|
profile.fax=Telefonas (faksas)
|
||||||
profile.mobile=Telefonas (mob.)
|
profile.mobile=Telefonas (mob.)
|
||||||
profile.email=El. pašto adresas (prisijungimas)*
|
profile.email=El. pašto adresas*
|
||||||
profile.street=Gatvė
|
profile.street=Gatvė
|
||||||
profile.housenr=Namo nr.
|
profile.housenr=Namo nr.
|
||||||
profile.addressadd=Adreso priedas
|
profile.addressadd=Adreso priedas
|
||||||
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Vietovė (atsiėmimas)
|
|||||||
addjob.address.city.placeholder.delivery=Vietovė (pristatymas)
|
addjob.address.city.placeholder.delivery=Vietovė (pristatymas)
|
||||||
addjob.address.delivery.street.placeholder=Gatvė (pristatymas)
|
addjob.address.delivery.street.placeholder=Gatvė (pristatymas)
|
||||||
addjob.address.delivery.addition.placeholder=Adreso priedas (pristatymas)
|
addjob.address.delivery.addition.placeholder=Adreso priedas (pristatymas)
|
||||||
addjob.address.save=Išsaugoti adresą
|
addjob.address.save=Pridėti adresą į adresų knygą
|
||||||
|
addjob.address.update=Atnaujinti adresą adresų knygoje
|
||||||
addjob.section.pickup=Atsiėmimas
|
addjob.section.pickup=Atsiėmimas
|
||||||
addjob.section.delivery=Pristatymas
|
addjob.section.delivery=Pristatymas
|
||||||
addjob.stations.apply=Pritaikyti stotis
|
addjob.stations.apply=Pritaikyti stotis
|
||||||
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. nuotraukų
|
|||||||
addjob.tasks.photo.max=Maks. nuotraukų
|
addjob.tasks.photo.max=Maks. nuotraukų
|
||||||
addjob.tasks.barcode.min=Min. brūkšninių kodų
|
addjob.tasks.barcode.min=Min. brūkšninių kodų
|
||||||
addjob.tasks.barcode.max=Maks. brūkšninių kodų
|
addjob.tasks.barcode.max=Maks. brūkšninių kodų
|
||||||
addjob.tasks.signature.noconfig=Konfigūracija nereikalinga
|
addjob.tasks.signature.notelabel=Pastaba (neprivaloma)
|
||||||
|
addjob.tasks.signature.notelabel.placeholder=Įveskite patarimo tekstą pastabai
|
||||||
addjob.tasks.todolist.title=Užduočių sąrašas
|
addjob.tasks.todolist.title=Užduočių sąrašas
|
||||||
addjob.tasks.todolist.item.placeholder=Įveskite užduotį
|
addjob.tasks.todolist.item.placeholder=Įveskite užduotį
|
||||||
addjob.tasks.todolist.add=Pridėti užduotį
|
addjob.tasks.todolist.add=Pridėti užduotį
|
||||||
@@ -610,6 +612,14 @@ jobsummary.task.photo.taken=Padarytos nuotraukos ({0})
|
|||||||
jobsummary.task.button.text=Mygtuko tekstas
|
jobsummary.task.button.text=Mygtuko tekstas
|
||||||
jobsummary.button.schliessen=Uždaryti
|
jobsummary.button.schliessen=Uždaryti
|
||||||
jobsummary.route.planned=Planuotas maršrutas
|
jobsummary.route.planned=Planuotas maršrutas
|
||||||
|
jobsummary.button.manualcomplete=Užbaigti rankiniu būdu
|
||||||
|
jobsummary.dialog.manualcomplete.title=Užbaigti užsakymą rankiniu būdu
|
||||||
|
jobsummary.dialog.manualcomplete.text=Užsakymas {0} dabar bus užbaigtas rankiniu būdu. Po to jo nebebus galima apdoroti per programėlę.
|
||||||
|
jobsummary.dialog.manualcomplete.reason=Priežastis
|
||||||
|
jobsummary.dialog.manualcomplete.reason.required=Prašome įvesti priežastį
|
||||||
|
jobsummary.dialog.manualcomplete.cancel=Atšaukti
|
||||||
|
jobsummary.dialog.manualcomplete.confirm=Priimti
|
||||||
|
jobsummary.history.manualcomplete.reason=Užbaigta rankiniu būdu
|
||||||
|
|
||||||
# Jobs
|
# Jobs
|
||||||
jobs.title=Užsakymai
|
jobs.title=Užsakymai
|
||||||
@@ -652,6 +662,8 @@ createinvoice.section.job=Užsakymo informacija
|
|||||||
createinvoice.section.route=Maršruto informacija
|
createinvoice.section.route=Maršruto informacija
|
||||||
createinvoice.section.services=Paslaugos
|
createinvoice.section.services=Paslaugos
|
||||||
createinvoice.section.summary=Santrauka
|
createinvoice.section.summary=Santrauka
|
||||||
|
createinvoice.section.vat=PVM
|
||||||
|
createinvoice.field.vatrate=PVM tarifas
|
||||||
createinvoice.field.jobnumber=Užsakymo numeris
|
createinvoice.field.jobnumber=Užsakymo numeris
|
||||||
createinvoice.field.customer=Klientas
|
createinvoice.field.customer=Klientas
|
||||||
createinvoice.field.status=Būsena
|
createinvoice.field.status=Būsena
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ dialog.confirm=Apstiprināt
|
|||||||
# Navigation and Main Layout
|
# Navigation and Main Layout
|
||||||
nav.jobs=Uzdevumi
|
nav.jobs=Uzdevumi
|
||||||
nav.job.create=Izveidot uzdevumu
|
nav.job.create=Izveidot uzdevumu
|
||||||
nav.customers=Klienti
|
nav.customers=Adrešu gr\u0101mata
|
||||||
nav.appusers=Lietotnes lietotāji
|
nav.appusers=Lietotnes lietotāji
|
||||||
nav.statistics=Statistika
|
nav.statistics=Statistika
|
||||||
nav.invoices=Rēķini
|
nav.invoices=Rēķini
|
||||||
@@ -31,7 +31,7 @@ profile.lastname=Uzvārds
|
|||||||
profile.phone=Tālruņa numurs
|
profile.phone=Tālruņa numurs
|
||||||
profile.fax=Tālrunis (fakss)
|
profile.fax=Tālrunis (fakss)
|
||||||
profile.mobile=Tālrunis (mobilais)
|
profile.mobile=Tālrunis (mobilais)
|
||||||
profile.email=E-pasta adrese (pieteikšanās)*
|
profile.email=E-pasta adrese
|
||||||
profile.street=Iela
|
profile.street=Iela
|
||||||
profile.housenr=Mājas nr.
|
profile.housenr=Mājas nr.
|
||||||
profile.addressadd=Adreses papildinājums
|
profile.addressadd=Adreses papildinājums
|
||||||
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Vieta (saņemšana)
|
|||||||
addjob.address.city.placeholder.delivery=Vieta (piegāde)
|
addjob.address.city.placeholder.delivery=Vieta (piegāde)
|
||||||
addjob.address.delivery.street.placeholder=Iela (piegāde)
|
addjob.address.delivery.street.placeholder=Iela (piegāde)
|
||||||
addjob.address.delivery.addition.placeholder=Adreses papildinājums (piegāde)
|
addjob.address.delivery.addition.placeholder=Adreses papildinājums (piegāde)
|
||||||
addjob.address.save=Saglabāt adresi
|
addjob.address.save=Pievienot adresi adrešu grāmatai
|
||||||
|
addjob.address.update=Atjaunin\u0101t adresi adrešu gr\u0101mat\u0101
|
||||||
addjob.section.pickup=Saņemšana
|
addjob.section.pickup=Saņemšana
|
||||||
addjob.section.delivery=Piegāde
|
addjob.section.delivery=Piegāde
|
||||||
addjob.stations.apply=Pārņemt stacijas
|
addjob.stations.apply=Pārņemt stacijas
|
||||||
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. fotogrāfijas
|
|||||||
addjob.tasks.photo.max=Maks. fotogrāfijas
|
addjob.tasks.photo.max=Maks. fotogrāfijas
|
||||||
addjob.tasks.barcode.min=Min. svītrkodi
|
addjob.tasks.barcode.min=Min. svītrkodi
|
||||||
addjob.tasks.barcode.max=Maks. svītrkodi
|
addjob.tasks.barcode.max=Maks. svītrkodi
|
||||||
addjob.tasks.signature.noconfig=Konfigurācija nav nepieciešama
|
addjob.tasks.signature.notelabel=Piezīme (neobligāta)
|
||||||
|
addjob.tasks.signature.notelabel.placeholder=Ievadiet padoma tekstu piezīmei
|
||||||
addjob.tasks.todolist.title=Uzdevumu saraksts
|
addjob.tasks.todolist.title=Uzdevumu saraksts
|
||||||
addjob.tasks.todolist.item.placeholder=Ievadiet uzdevumu
|
addjob.tasks.todolist.item.placeholder=Ievadiet uzdevumu
|
||||||
addjob.tasks.todolist.add=Pievienot uzdevumu
|
addjob.tasks.todolist.add=Pievienot uzdevumu
|
||||||
@@ -610,6 +612,14 @@ jobsummary.task.photo.taken=Uzņemtās fotogrāfijas ({0})
|
|||||||
jobsummary.task.button.text=Pogas teksts
|
jobsummary.task.button.text=Pogas teksts
|
||||||
jobsummary.button.schliessen=Aizvērt
|
jobsummary.button.schliessen=Aizvērt
|
||||||
jobsummary.route.planned=Plānotais maršruts
|
jobsummary.route.planned=Plānotais maršruts
|
||||||
|
jobsummary.button.manualcomplete=Pabeigt manuāli
|
||||||
|
jobsummary.dialog.manualcomplete.title=Pabeigt pasūtījumu manuāli
|
||||||
|
jobsummary.dialog.manualcomplete.text=Pasūtījums {0} tagad tiks pabeigts manuāli. Pēc tam to vairs nevarēs apstrādāt, izmantojot lietotni.
|
||||||
|
jobsummary.dialog.manualcomplete.reason=Pamatojums
|
||||||
|
jobsummary.dialog.manualcomplete.reason.required=Lūdzu, ievadiet pamatojumu
|
||||||
|
jobsummary.dialog.manualcomplete.cancel=Atcelt
|
||||||
|
jobsummary.dialog.manualcomplete.confirm=Apstiprināt
|
||||||
|
jobsummary.history.manualcomplete.reason=Pabeigts manuāli
|
||||||
|
|
||||||
# Jobs
|
# Jobs
|
||||||
jobs.title=Uzdevumi
|
jobs.title=Uzdevumi
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ dialog.confirm=Potwierd\u017a
|
|||||||
# Navigation and Main Layout
|
# Navigation and Main Layout
|
||||||
nav.jobs=Zlecenia
|
nav.jobs=Zlecenia
|
||||||
nav.job.create=Tworzenie zlecenia
|
nav.job.create=Tworzenie zlecenia
|
||||||
nav.customers=Klienci
|
nav.customers=Ksi\u0105\u017cka adresowa
|
||||||
nav.appusers=U\u017cytkownicy aplikacji
|
nav.appusers=U\u017cytkownicy aplikacji
|
||||||
nav.statistics=Statystyki
|
nav.statistics=Statystyki
|
||||||
nav.invoices=Faktury
|
nav.invoices=Faktury
|
||||||
@@ -31,7 +31,7 @@ profile.lastname=Nazwisko
|
|||||||
profile.phone=Numer telefonu
|
profile.phone=Numer telefonu
|
||||||
profile.fax=Telefon (faks)
|
profile.fax=Telefon (faks)
|
||||||
profile.mobile=Telefon (kom\u00f3rkowy)
|
profile.mobile=Telefon (kom\u00f3rkowy)
|
||||||
profile.email=Adres e-mail (login)*
|
profile.email=Adres e-mail
|
||||||
profile.street=Ulica
|
profile.street=Ulica
|
||||||
profile.housenr=Nr domu
|
profile.housenr=Nr domu
|
||||||
profile.addressadd=Dodatek do adresu
|
profile.addressadd=Dodatek do adresu
|
||||||
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Miejscowo\u015b\u0107 (odbi\u00f3r)
|
|||||||
addjob.address.city.placeholder.delivery=Miejscowo\u015b\u0107 (dostawa)
|
addjob.address.city.placeholder.delivery=Miejscowo\u015b\u0107 (dostawa)
|
||||||
addjob.address.delivery.street.placeholder=Ulica (dostawa)
|
addjob.address.delivery.street.placeholder=Ulica (dostawa)
|
||||||
addjob.address.delivery.addition.placeholder=Dodatek do adresu (dostawa)
|
addjob.address.delivery.addition.placeholder=Dodatek do adresu (dostawa)
|
||||||
addjob.address.save=Zapisz adres
|
addjob.address.save=Dodaj adres do ksi\u0105\u017cki adresowej
|
||||||
|
addjob.address.update=Zaktualizuj adres w ksi\u0105\u017cce adresowej
|
||||||
addjob.section.pickup=Odbi\u00f3r
|
addjob.section.pickup=Odbi\u00f3r
|
||||||
addjob.section.delivery=Dostawa
|
addjob.section.delivery=Dostawa
|
||||||
addjob.stations.apply=Zastosuj stacje
|
addjob.stations.apply=Zastosuj stacje
|
||||||
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. zdj\u0119\u0107
|
|||||||
addjob.tasks.photo.max=Maks. zdj\u0119\u0107
|
addjob.tasks.photo.max=Maks. zdj\u0119\u0107
|
||||||
addjob.tasks.barcode.min=Min. kod\u00f3w kreskowych
|
addjob.tasks.barcode.min=Min. kod\u00f3w kreskowych
|
||||||
addjob.tasks.barcode.max=Maks. kod\u00f3w kreskowych
|
addjob.tasks.barcode.max=Maks. kod\u00f3w kreskowych
|
||||||
addjob.tasks.signature.noconfig=Konfiguracja nie jest wymagana
|
addjob.tasks.signature.notelabel=Notatka (opcjonalnie)
|
||||||
|
addjob.tasks.signature.notelabel.placeholder=Wprowadź tekst podpowiedzi dla notatki
|
||||||
addjob.tasks.todolist.title=Lista zada\u0144
|
addjob.tasks.todolist.title=Lista zada\u0144
|
||||||
addjob.tasks.todolist.item.placeholder=Wprowad\u017a zadanie
|
addjob.tasks.todolist.item.placeholder=Wprowad\u017a zadanie
|
||||||
addjob.tasks.todolist.add=Dodaj zadanie
|
addjob.tasks.todolist.add=Dodaj zadanie
|
||||||
@@ -610,6 +612,14 @@ jobsummary.task.photo.taken=Wykonane zdj\u0119cia ({0})
|
|||||||
jobsummary.task.button.text=Tekst przycisku
|
jobsummary.task.button.text=Tekst przycisku
|
||||||
jobsummary.button.schliessen=Zamknij
|
jobsummary.button.schliessen=Zamknij
|
||||||
jobsummary.route.planned=Planowana trasa
|
jobsummary.route.planned=Planowana trasa
|
||||||
|
jobsummary.button.manualcomplete=Zako\u0144cz r\u0119cznie
|
||||||
|
jobsummary.dialog.manualcomplete.title=Zako\u0144cz zlecenie r\u0119cznie
|
||||||
|
jobsummary.dialog.manualcomplete.text=Zlecenie {0} zostanie teraz zako\u0144czone r\u0119cznie. Po tym nie b\u0119dzie mo\u017cna go dalej obs\u0142ugiwa\u0107 przez aplikacj\u0119.
|
||||||
|
jobsummary.dialog.manualcomplete.reason=Uzasadnienie
|
||||||
|
jobsummary.dialog.manualcomplete.reason.required=Prosz\u0119 poda\u0107 uzasadnienie
|
||||||
|
jobsummary.dialog.manualcomplete.cancel=Anuluj
|
||||||
|
jobsummary.dialog.manualcomplete.confirm=Akceptuj
|
||||||
|
jobsummary.history.manualcomplete.reason=Zako\u0144czono r\u0119cznie
|
||||||
|
|
||||||
# Jobs
|
# Jobs
|
||||||
jobs.title=Zlecenia
|
jobs.title=Zlecenia
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ dialog.confirm=Подтвердить
|
|||||||
# Navigation and Main Layout
|
# Navigation and Main Layout
|
||||||
nav.jobs=Заказы
|
nav.jobs=Заказы
|
||||||
nav.job.create=Создание заказа
|
nav.job.create=Создание заказа
|
||||||
nav.customers=Клиенты
|
nav.customers=Адресная книга
|
||||||
nav.appusers=Пользователи приложения
|
nav.appusers=Пользователи приложения
|
||||||
nav.statistics=Статистика
|
nav.statistics=Статистика
|
||||||
nav.invoices=Счета
|
nav.invoices=Счета
|
||||||
@@ -31,7 +31,7 @@ profile.lastname=Фамилия
|
|||||||
profile.phone=Номер телефона
|
profile.phone=Номер телефона
|
||||||
profile.fax=Телефон (факс)
|
profile.fax=Телефон (факс)
|
||||||
profile.mobile=Телефон (мобильный)
|
profile.mobile=Телефон (мобильный)
|
||||||
profile.email=Адрес электронной почты (логин)*
|
profile.email=Адрес электронной почты
|
||||||
profile.street=Улица
|
profile.street=Улица
|
||||||
profile.housenr=Дом
|
profile.housenr=Дом
|
||||||
profile.addressadd=Дополнение к адресу
|
profile.addressadd=Дополнение к адресу
|
||||||
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=Город (забор)
|
|||||||
addjob.address.city.placeholder.delivery=Город (доставка)
|
addjob.address.city.placeholder.delivery=Город (доставка)
|
||||||
addjob.address.delivery.street.placeholder=Улица (доставка)
|
addjob.address.delivery.street.placeholder=Улица (доставка)
|
||||||
addjob.address.delivery.addition.placeholder=Дополнение к адресу (доставка)
|
addjob.address.delivery.addition.placeholder=Дополнение к адресу (доставка)
|
||||||
addjob.address.save=Сохранить адрес
|
addjob.address.save=Добавить адрес в адресную книгу
|
||||||
|
addjob.address.update=Обновить адрес в адресной книге
|
||||||
addjob.section.pickup=Забор
|
addjob.section.pickup=Забор
|
||||||
addjob.section.delivery=Доставка
|
addjob.section.delivery=Доставка
|
||||||
addjob.stations.apply=Применить станции
|
addjob.stations.apply=Применить станции
|
||||||
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Мин. фото
|
|||||||
addjob.tasks.photo.max=Макс. фото
|
addjob.tasks.photo.max=Макс. фото
|
||||||
addjob.tasks.barcode.min=Мин. штрих-кодов
|
addjob.tasks.barcode.min=Мин. штрих-кодов
|
||||||
addjob.tasks.barcode.max=Макс. штрих-кодов
|
addjob.tasks.barcode.max=Макс. штрих-кодов
|
||||||
addjob.tasks.signature.noconfig=Настройка не требуется
|
addjob.tasks.signature.notelabel=Примечание (необязательно)
|
||||||
|
addjob.tasks.signature.notelabel.placeholder=Введите текст подсказки для примечания
|
||||||
addjob.tasks.todolist.title=Список дел
|
addjob.tasks.todolist.title=Список дел
|
||||||
addjob.tasks.todolist.item.placeholder=Введите задачу
|
addjob.tasks.todolist.item.placeholder=Введите задачу
|
||||||
addjob.tasks.todolist.add=Добавить задачу
|
addjob.tasks.todolist.add=Добавить задачу
|
||||||
@@ -610,6 +612,14 @@ jobsummary.task.photo.taken=Сделанные фотографии ({0})
|
|||||||
jobsummary.task.button.text=Текст кнопки
|
jobsummary.task.button.text=Текст кнопки
|
||||||
jobsummary.button.schliessen=Закрыть
|
jobsummary.button.schliessen=Закрыть
|
||||||
jobsummary.route.planned=Запланированный маршрут
|
jobsummary.route.planned=Запланированный маршрут
|
||||||
|
jobsummary.button.manualcomplete=Завершить вручную
|
||||||
|
jobsummary.dialog.manualcomplete.title=Завершить заказ вручную
|
||||||
|
jobsummary.dialog.manualcomplete.text=Заказ {0} будет завершён вручную. После этого его больше нельзя будет обрабатывать через приложение.
|
||||||
|
jobsummary.dialog.manualcomplete.reason=Обоснование
|
||||||
|
jobsummary.dialog.manualcomplete.reason.required=Пожалуйста, укажите обоснование
|
||||||
|
jobsummary.dialog.manualcomplete.cancel=Отмена
|
||||||
|
jobsummary.dialog.manualcomplete.confirm=Принять
|
||||||
|
jobsummary.history.manualcomplete.reason=Завершено вручную
|
||||||
|
|
||||||
# Jobs
|
# Jobs
|
||||||
jobs.title=Заказы
|
jobs.title=Заказы
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ dialog.confirm=Onayla
|
|||||||
# Navigation and Main Layout
|
# Navigation and Main Layout
|
||||||
nav.jobs=\u0130\u015fler
|
nav.jobs=\u0130\u015fler
|
||||||
nav.job.create=\u0130\u015f Olu\u015ftur
|
nav.job.create=\u0130\u015f Olu\u015ftur
|
||||||
nav.customers=M\u00fc\u015fteriler
|
nav.customers=Adres Defteri
|
||||||
nav.appusers=Uygulama Kullan\u0131c\u0131lar\u0131
|
nav.appusers=Uygulama Kullan\u0131c\u0131lar\u0131
|
||||||
nav.statistics=\u0130statistikler
|
nav.statistics=\u0130statistikler
|
||||||
nav.invoices=Faturalar
|
nav.invoices=Faturalar
|
||||||
@@ -31,7 +31,7 @@ profile.lastname=Soyad
|
|||||||
profile.phone=Telefon Numaras\u0131
|
profile.phone=Telefon Numaras\u0131
|
||||||
profile.fax=Telefon (Faks)
|
profile.fax=Telefon (Faks)
|
||||||
profile.mobile=Telefon (Mobil)
|
profile.mobile=Telefon (Mobil)
|
||||||
profile.email=E-Posta Adresi (Giri\u015f)*
|
profile.email=E-Posta Adresi*
|
||||||
profile.street=Sokak
|
profile.street=Sokak
|
||||||
profile.housenr=Kap\u0131 No
|
profile.housenr=Kap\u0131 No
|
||||||
profile.addressadd=Adres Eki
|
profile.addressadd=Adres Eki
|
||||||
@@ -447,7 +447,8 @@ addjob.address.city.placeholder.pickup=\u015eehir (Al\u0131m)
|
|||||||
addjob.address.city.placeholder.delivery=\u015eehir (Teslimat)
|
addjob.address.city.placeholder.delivery=\u015eehir (Teslimat)
|
||||||
addjob.address.delivery.street.placeholder=Sokak (Teslimat)
|
addjob.address.delivery.street.placeholder=Sokak (Teslimat)
|
||||||
addjob.address.delivery.addition.placeholder=Adres eki (Teslimat)
|
addjob.address.delivery.addition.placeholder=Adres eki (Teslimat)
|
||||||
addjob.address.save=Adresi Kaydet
|
addjob.address.save=Adresi adres defterine ekle
|
||||||
|
addjob.address.update=Adres defterindeki adresi g\u00fcncelle
|
||||||
addjob.section.pickup=Al\u0131m
|
addjob.section.pickup=Al\u0131m
|
||||||
addjob.section.delivery=Teslimat
|
addjob.section.delivery=Teslimat
|
||||||
addjob.stations.apply=\u0130stasyonlar\u0131 \u00fcbernehmennehmen
|
addjob.stations.apply=\u0130stasyonlar\u0131 \u00fcbernehmennehmen
|
||||||
@@ -513,7 +514,8 @@ addjob.tasks.photo.min=Min. Foto\u011fraf
|
|||||||
addjob.tasks.photo.max=Maks. Foto\u011fraf
|
addjob.tasks.photo.max=Maks. Foto\u011fraf
|
||||||
addjob.tasks.barcode.min=Min. Barkod
|
addjob.tasks.barcode.min=Min. Barkod
|
||||||
addjob.tasks.barcode.max=Maks. Barkod
|
addjob.tasks.barcode.max=Maks. Barkod
|
||||||
addjob.tasks.signature.noconfig=Yap\u0131land\u0131rma gerekli de\u011fil
|
addjob.tasks.signature.notelabel=Not (iste\u011fe ba\u011fl\u0131)
|
||||||
|
addjob.tasks.signature.notelabel.placeholder=Not i\u00e7in ipucu metnini girin
|
||||||
addjob.tasks.todolist.title=Yap\u0131lacaklar Listesi
|
addjob.tasks.todolist.title=Yap\u0131lacaklar Listesi
|
||||||
addjob.tasks.todolist.item.placeholder=Yap\u0131lacak \u00f6\u011feyi girin
|
addjob.tasks.todolist.item.placeholder=Yap\u0131lacak \u00f6\u011feyi girin
|
||||||
addjob.tasks.todolist.add=Yap\u0131lacak \u00d6\u011fe Ekle
|
addjob.tasks.todolist.add=Yap\u0131lacak \u00d6\u011fe Ekle
|
||||||
@@ -610,6 +612,14 @@ jobsummary.task.photo.taken=\u00c7ekilen Foto\u011fraflar ({0})
|
|||||||
jobsummary.task.button.text=Buton Metni
|
jobsummary.task.button.text=Buton Metni
|
||||||
jobsummary.button.schliessen=Kapat
|
jobsummary.button.schliessen=Kapat
|
||||||
jobsummary.route.planned=Planlanan Rota
|
jobsummary.route.planned=Planlanan Rota
|
||||||
|
jobsummary.button.manualcomplete=Manuel olarak tamamla
|
||||||
|
jobsummary.dialog.manualcomplete.title=Siparişi manuel olarak tamamla
|
||||||
|
jobsummary.dialog.manualcomplete.text=Sipariş {0} şimdi manuel olarak tamamlanacak. Bundan sonra uygulama üzerinden işlenemez.
|
||||||
|
jobsummary.dialog.manualcomplete.reason=Gerekçe
|
||||||
|
jobsummary.dialog.manualcomplete.reason.required=Lütfen bir gerekçe girin
|
||||||
|
jobsummary.dialog.manualcomplete.cancel=İptal
|
||||||
|
jobsummary.dialog.manualcomplete.confirm=Kabul et
|
||||||
|
jobsummary.history.manualcomplete.reason=Manuel olarak tamamlandı
|
||||||
|
|
||||||
# Jobs
|
# Jobs
|
||||||
jobs.title=\u0130\u015fler
|
jobs.title=\u0130\u015fler
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package de.assecutor.votianlt.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.AppUser;
|
||||||
|
import de.assecutor.votianlt.model.Job;
|
||||||
|
import de.assecutor.votianlt.model.User;
|
||||||
|
import de.assecutor.votianlt.model.task.ConfirmationTask;
|
||||||
|
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||||
|
import de.assecutor.votianlt.repository.JobRepository;
|
||||||
|
import de.assecutor.votianlt.repository.TaskRepository;
|
||||||
|
import de.assecutor.votianlt.repository.UserRepository;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.mail.SimpleMailMessage;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class EmailServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private UserRepository userRepository;
|
||||||
|
@Mock
|
||||||
|
private JobRepository jobRepository;
|
||||||
|
@Mock
|
||||||
|
private TaskRepository taskRepository;
|
||||||
|
@Mock
|
||||||
|
private AppUserRepository appUserRepository;
|
||||||
|
@Mock
|
||||||
|
private JavaMailSender mailSender;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<SimpleMailMessage> mailCaptor;
|
||||||
|
|
||||||
|
private EmailService emailService;
|
||||||
|
private TaskAssignmentService taskAssignmentService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
taskAssignmentService = new TaskAssignmentService(taskRepository, jobRepository);
|
||||||
|
emailService = new EmailService(userRepository, jobRepository, taskAssignmentService, appUserRepository,
|
||||||
|
mailSender);
|
||||||
|
ReflectionTestUtils.setField(emailService, "smtpUsername", "noreply@example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendTaskCompletionNotificationUsesAssignedAppUserName() {
|
||||||
|
User webUser = createWebUser();
|
||||||
|
AppUser assignedAppUser = createAssignedAppUser();
|
||||||
|
Job job = createJob(webUser, assignedAppUser);
|
||||||
|
|
||||||
|
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
|
||||||
|
when(userRepository.findById(webUser.getId())).thenReturn(Optional.of(webUser));
|
||||||
|
when(appUserRepository.findById(assignedAppUser.getId())).thenReturn(Optional.of(assignedAppUser));
|
||||||
|
|
||||||
|
emailService.sendTaskCompletionNotification(job.getId(), "PHOTO", "task-1", "ignored");
|
||||||
|
|
||||||
|
verify(mailSender).send(mailCaptor.capture());
|
||||||
|
String body = mailCaptor.getValue().getText();
|
||||||
|
assertThat(body).contains("eine Aufgabe wurde von Max Mustermann abgeschlossen:");
|
||||||
|
assertThat(body).contains("Abgeschlossen von: Max Mustermann");
|
||||||
|
assertThat(body).contains("Hallo Dr. Anna Unternehmer,");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendJobCompletionNotificationUsesAssignedAppUserName() {
|
||||||
|
User webUser = createWebUser();
|
||||||
|
AppUser assignedAppUser = createAssignedAppUser();
|
||||||
|
Job job = createJob(webUser, assignedAppUser);
|
||||||
|
|
||||||
|
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
|
||||||
|
when(userRepository.findById(webUser.getId())).thenReturn(Optional.of(webUser));
|
||||||
|
when(appUserRepository.findById(assignedAppUser.getId())).thenReturn(Optional.of(assignedAppUser));
|
||||||
|
when(taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId())).thenReturn(List.of(
|
||||||
|
createConfirmationTask(0, "OK"),
|
||||||
|
createConfirmationTask(1, "Weiter")));
|
||||||
|
|
||||||
|
emailService.sendJobCompletionNotification(job.getId(), "ignored");
|
||||||
|
|
||||||
|
verify(mailSender).send(mailCaptor.capture());
|
||||||
|
String body = mailCaptor.getValue().getText();
|
||||||
|
assertThat(body).contains("Anzahl erledigter Aufgaben: 2");
|
||||||
|
assertThat(body).contains("Abgeschlossen von: Max Mustermann");
|
||||||
|
assertThat(body).contains("Hallo Dr. Anna Unternehmer,");
|
||||||
|
}
|
||||||
|
|
||||||
|
private User createWebUser() {
|
||||||
|
User user = new User();
|
||||||
|
user.setId(new ObjectId());
|
||||||
|
user.setTitle("Dr.");
|
||||||
|
user.setFirstname("Anna");
|
||||||
|
user.setName("Unternehmer");
|
||||||
|
user.setEmail("anna@example.com");
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppUser createAssignedAppUser() {
|
||||||
|
AppUser appUser = new AppUser();
|
||||||
|
appUser.setId(new ObjectId());
|
||||||
|
appUser.setVorname("Max");
|
||||||
|
appUser.setNachname("Mustermann");
|
||||||
|
appUser.setBezeichnung("Fahrer Max");
|
||||||
|
return appUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Job createJob(User webUser, AppUser assignedAppUser) {
|
||||||
|
Job job = new Job();
|
||||||
|
job.setId(new ObjectId());
|
||||||
|
job.setCreatedBy(webUser.getId().toHexString());
|
||||||
|
job.setAppUser(assignedAppUser.getId().toHexString());
|
||||||
|
job.setJobNumber("JOB-2026-001");
|
||||||
|
job.setDeliveryCompany("Beispiel GmbH");
|
||||||
|
job.setPickupCity("Berlin");
|
||||||
|
job.setDeliveryCity("Hamburg");
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfirmationTask createConfirmationTask(int taskOrder, String buttonText) {
|
||||||
|
ConfirmationTask task = new ConfirmationTask(buttonText);
|
||||||
|
task.setTaskOrder(taskOrder);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user