first commit

This commit is contained in:
2026-03-24 15:03:35 +01:00
commit cdba16ebe8
162 changed files with 194406 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
import 'package:flutter/material.dart';
import '../../domain/entities/counter.dart';
/// Zähler-Grid wie in Lua CreateLoadingStockStartView etc.
class CounterGrid extends StatelessWidget {
final List<CounterGroup> groups;
final bool showDifferences;
const CounterGrid({
super.key,
required this.groups,
this.showDifferences = false,
});
@override
Widget build(BuildContext context) {
return Column(
children: groups.map((group) => _buildGroup(context, group)).toList(),
);
}
Widget _buildGroup(BuildContext context, CounterGroup group) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getGroupColor(group.title),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
// Titel
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
group.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
// Zähler
Container(
padding: const EdgeInsets.all(8),
child: Row(
children: group.counters.map((counter) {
return Expanded(
child: _buildCounterCell(context, counter),
);
}).toList(),
),
),
],
),
);
}
Widget _buildCounterCell(BuildContext context, ObjectCounter counter) {
final color = counter.isOver
? Colors.red.shade100
: counter.isComplete
? Colors.green.shade100
: Colors.white;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(4),
),
child: Column(
children: [
Text(
counter.label,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'${counter.currentCount}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: counter.isOver ? Colors.red : Colors.black87,
),
),
if (showDifferences && counter.targetCount > 0) ...[
const SizedBox(height: 2),
Text(
'${counter.difference > 0 ? "+" : ""}${counter.difference}',
style: TextStyle(
fontSize: 12,
color: counter.difference == 0
? Colors.green
: counter.difference > 0
? Colors.orange
: Colors.red,
),
),
],
],
),
);
}
Color _getGroupColor(String title) {
switch (title.toLowerCase()) {
case 'bestand fzg':
return const Color(0xFFA4D4F0); // Hellblau wie in Lua
case 'beladezähler':
case 'hadag':
case 'sst':
case 'cr':
return Colors.grey.shade200;
default:
return Colors.grey.shade200;
}
}
}
/// Vereinfachte Zähler-Anzeige für eine Zeile
class CounterRow extends StatelessWidget {
final String label;
final int count;
final Color? backgroundColor;
final VoidCallback? onTap;
const CounterRow({
super.key,
required this.label,
required this.count,
this.backgroundColor,
this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: backgroundColor ?? Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Expanded(
child: Text(
label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.grey.shade300),
),
child: Text(
'$count',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
class ErrorView extends StatelessWidget {
final String message;
final VoidCallback? onRetry;
final String? retryText;
final IconData? icon;
const ErrorView({
super.key,
required this.message,
this.onRetry,
this.retryText,
this.icon,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: theme.colorScheme.error.withValues(alpha: 26),
shape: BoxShape.circle,
),
child: Icon(
icon ?? Icons.error_outline,
size: 48,
color: theme.colorScheme.error,
),
),
const SizedBox(height: 24),
Text(
'Oops!',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 12),
Text(
message,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 179),
),
textAlign: TextAlign.center,
),
if (onRetry != null) ...[
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: Text(retryText ?? 'Erneut versuchen'),
),
],
],
),
),
);
}
}
class EmptyStateView extends StatelessWidget {
final String title;
final String? subtitle;
final IconData icon;
final VoidCallback? onAction;
final String? actionText;
const EmptyStateView({
super.key,
required this.title,
this.subtitle,
this.icon = Icons.inbox,
this.onAction,
this.actionText,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 48,
color: Colors.grey.shade500,
),
),
const SizedBox(height: 24),
Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
if (subtitle != null) ...[
const SizedBox(height: 8),
Text(
subtitle!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 153),
),
textAlign: TextAlign.center,
),
],
if (onAction != null && actionText != null) ...[
const SizedBox(height: 24),
ElevatedButton(
onPressed: onAction,
child: Text(actionText!),
),
],
],
),
),
);
}
}

View File

@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
class LoadingIndicator extends StatelessWidget {
final String? message;
final bool showAnimation;
const LoadingIndicator({
super.key,
this.message,
this.showAnimation = false,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (showAnimation) ...[
// Optional: Lottie Animation für Loading
SizedBox(
width: 120,
height: 120,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
theme.colorScheme.primary,
),
),
),
] else ...[
SizedBox(
width: 48,
height: 48,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
theme.colorScheme.primary,
),
),
),
],
if (message != null) ...[
const SizedBox(height: 24),
Text(
message!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 179),
),
textAlign: TextAlign.center,
),
],
],
),
);
}
}
class SkeletonLoading extends StatelessWidget {
final int itemCount;
final EdgeInsets padding;
const SkeletonLoading({
super.key,
this.itemCount = 5,
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
});
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: padding,
itemCount: itemCount,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _SkeletonCard(),
);
},
);
}
}
class _SkeletonCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(14),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
height: 16,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 8),
Container(
width: 120,
height: 12,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,173 @@
import 'package:flutter/material.dart';
import '../../domain/entities/counter.dart';
/// Zeigt zuletzt gescannte Objekte an
/// Entspricht Lua: Die Liste in ShowStockStartScreen etc.
class RecentScansList extends StatelessWidget {
final List<RecentScan>? scans;
const RecentScansList({
super.key,
this.scans,
});
@override
Widget build(BuildContext context) {
// Demo-Daten falls keine vorhanden
final demoScans = scans ?? const [
// RecentScan(
// objectCode: 'MEKA123456',
// objectName: 'MEK A',
// state: 'to_delivery',
// scanTime: '',
// ),
];
if (demoScans.isEmpty) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text(
'Noch keine Objekte gescannt',
style: TextStyle(
color: Colors.grey,
fontStyle: FontStyle.italic,
),
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Zuletzt gescannt',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
...demoScans.map((scan) => _buildScanItem(context, scan)),
],
);
}
Widget _buildScanItem(BuildContext context, RecentScan scan) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
// Objekt-Icon
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
_getIconForObject(scan.objectName),
color: Colors.blue,
),
),
const SizedBox(width: 12),
// Objekt-Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
scan.objectName,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
Text(
'#${scan.objectCode}',
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade600,
),
),
],
),
),
// Status
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getColorForState(scan.state),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_getShortStateName(scan.state),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
IconData _getIconForObject(String objectName) {
final name = objectName.toLowerCase();
if (name.contains('mek')) return Icons.money;
if (name.contains('bek')) return Icons.money_off;
if (name.contains('hp')) return Icons.print;
if (name.contains('fr')) return Icons.receipt;
if (name.contains('sb')) return Icons.shopping_bag;
if (name.contains('abs')) return Icons.delete;
return Icons.inventory_2;
}
Color _getColorForState(String state) {
switch (state) {
case 'to_delivery':
return Colors.grey.shade300;
case 'delivery':
return Colors.grey.shade400;
case 'station':
return const Color(0xFFFFDD00);
case 'in_fa':
return const Color(0xFF9CDA7A);
case 'ret_fail':
return const Color(0xFFFF9081);
default:
return Colors.grey.shade200;
}
}
String _getShortStateName(String state) {
switch (state) {
case 'to_delivery':
return 'Zum Fzg';
case 'delivery':
return 'Im Fzg';
case 'station':
return 'Station';
case 'in_fa':
return 'Im FA';
case 'ret_fail':
return 'Fehler';
default:
return state;
}
}
}

View File

@@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import '../../domain/entities/tour.dart';
import '../../core/constants/app_constants.dart';
class TourListItem extends StatelessWidget {
final Tour tour;
final VoidCallback onTap;
final bool isCompleted;
const TourListItem({
super.key,
required this.tour,
required this.onTap,
this.isCompleted = false,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final iconData = _getIconForTourType(tour.type);
final color = _getColorForTourType(tour.type);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Card(
elevation: isCompleted ? 0 : 2,
shadowColor: Colors.black.withValues(alpha: 26),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: isCompleted
? BorderSide(color: Colors.grey.shade300)
: BorderSide.none,
),
child: InkWell(
onTap: isCompleted ? null : onTap,
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Icon Container
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
color,
color.withValues(alpha: 204),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 77),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Icon(
iconData,
color: Colors.white,
size: 28,
),
),
const SizedBox(width: 16),
// Content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tour.locationName ?? 'Station ${tour.locationId}',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: isCompleted ? Colors.grey : null,
decoration: isCompleted ? TextDecoration.lineThrough : null,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
_getTypeLabel(tour.type),
style: theme.textTheme.bodySmall?.copyWith(
color: color,
fontWeight: FontWeight.w500,
),
),
if (tour.remark != null && tour.remark!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
tour.remark!,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey.shade600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
// Status Indicator
if (isCompleted)
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 26),
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
color: Colors.green,
size: 20,
),
)
else
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
shape: BoxShape.circle,
),
child: Icon(
Icons.chevron_right,
color: Colors.grey.shade600,
size: 24,
),
),
],
),
),
),
),
).animate().fadeIn(duration: 300.ms).slideX(begin: 0.1, end: 0);
}
IconData _getIconForTourType(String type) {
switch (type) {
case TourTypes.stockStart:
return Icons.warehouse;
case TourTypes.stockEnd:
return Icons.archive;
case TourTypes.start:
return Icons.business;
case TourTypes.end:
return Icons.flag;
case TourTypes.station:
return Icons.directions_bus;
case TourTypes.hls:
return Icons.train;
case TourTypes.fsa:
return Icons.confirmation_number;
case TourTypes.vs:
return Icons.store;
case TourTypes.gi:
return Icons.account_balance;
case TourTypes.veh:
case TourTypes.vehStart:
case TourTypes.vehEnd:
return Icons.local_shipping;
default:
return Icons.location_on;
}
}
Color _getColorForTourType(String type) {
switch (type) {
case TourTypes.stockStart:
case TourTypes.stockEnd:
return const Color(0xFF2196F3);
case TourTypes.start:
case TourTypes.end:
return const Color(0xFF4CAF50);
case TourTypes.station:
return const Color(0xFFFF9800);
case TourTypes.hls:
return const Color(0xFF9C27B0);
case TourTypes.fsa:
return const Color(0xFFE91E63);
case TourTypes.vs:
return const Color(0xFF00BCD4);
case TourTypes.gi:
return const Color(0xFF3F51B5);
case TourTypes.veh:
case TourTypes.vehStart:
case TourTypes.vehEnd:
return const Color(0xFF795548);
default:
return const Color(0xFF607D8B);
}
}
String _getTypeLabel(String type) {
switch (type) {
case TourTypes.stockStart:
return 'Lager - Beladung';
case TourTypes.stockEnd:
return 'Lager - Rückgabe';
case TourTypes.start:
return 'Dienststelle';
case TourTypes.end:
return 'Tour Ende';
case TourTypes.station:
return 'Haltestelle';
case TourTypes.hls:
return 'Hochbahnstation';
case TourTypes.fsa:
return 'Fahrscheinautomat';
case TourTypes.vs:
return 'Versorgungsstelle';
case TourTypes.gi:
return 'Geldinstitut';
case TourTypes.veh:
return 'Fahrzeug';
case TourTypes.vehStart:
return 'Fahrzeug - Beladung';
case TourTypes.vehEnd:
return 'Fahrzeug - Entladung';
default:
return type;
}
}
}