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,423 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../blocs/tour/tour_bloc.dart';
import '../../widgets/loading_indicator.dart';
class DashboardPage extends StatelessWidget {
const DashboardPage({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
body: SafeArea(
child: BlocBuilder<TourBloc, TourState>(
builder: (context, state) {
if (state is TourLoading) {
return const LoadingIndicator();
}
if (state is ToursLoaded) {
return CustomScrollView(
slivers: [
// App Bar
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Guten Morgen,',
style: theme.textTheme.bodyLarge?.copyWith(
color: Colors.grey.shade600,
),
),
const SizedBox(height: 4),
Text(
'Fahrer',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 26),
borderRadius: BorderRadius.circular(16),
),
child: Icon(
Icons.person,
color: theme.colorScheme.primary,
),
),
],
),
const SizedBox(height: 8),
Text(
'Tour vom ${DateTime.now().day}.${DateTime.now().month}.${DateTime.now().year}',
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.grey.shade600,
),
),
],
),
),
),
// Quick Stats
SliverToBoxAdapter(
child: _buildQuickStats(context, state),
),
// Section Title
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Schnellzugriff',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
TextButton(
onPressed: () {
// Navigate to full tours list
},
child: const Text('Alle anzeigen'),
),
],
),
),
),
// Quick Actions Grid
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 24),
sliver: SliverGrid.count(
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
children: [
_buildQuickActionCard(
context,
'Nächste Station',
Icons.location_on,
Colors.orange,
'${state.tours.where((t) => t.state == 0).length} offen',
() {},
),
_buildQuickActionCard(
context,
'Scan',
Icons.qr_code_scanner,
Colors.green,
'Barcode scannen',
() {},
),
_buildQuickActionCard(
context,
'Bestand',
Icons.warehouse,
Colors.blue,
'Objekte anzeigen',
() {},
),
_buildQuickActionCard(
context,
'Sync',
Icons.sync,
Colors.purple,
state.isSyncing ? 'Synchronisiert...' : 'Daten aktualisieren',
() {
context.read<TourBloc>().add(const SyncData());
},
),
],
),
),
// Recent Activity
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
child: Text(
'Letzte Aktivitäten',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 24),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return _buildActivityItem(
context,
'Geldkassette gescannt',
'Station: Hauptbahnhof Nord',
'10:23 Uhr',
Icons.qr_code_scanner,
Colors.green,
);
},
childCount: 3,
),
),
),
const SliverPadding(padding: EdgeInsets.only(bottom: 100)),
],
);
}
return const Center(child: Text('Willkommen bei HHA Logistics'));
},
),
),
);
}
Widget _buildQuickStats(BuildContext context, ToursLoaded state) {
final theme = Theme.of(context);
final completionPercentage = state.completionPercentage;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
theme.colorScheme.primary,
theme.colorScheme.primary.withValues(alpha: 204),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withValues(alpha: 77),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 51),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.route,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tour-Fortschritt',
style: theme.textTheme.titleSmall?.copyWith(
color: Colors.white.withValues(alpha: 204),
),
),
const SizedBox(height: 4),
Text(
'${state.completedCount} / ${state.totalCount} Stationen',
style: theme.textTheme.titleLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 24),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: LinearProgressIndicator(
value: completionPercentage / 100,
backgroundColor: Colors.white.withValues(alpha: 51),
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
minHeight: 10,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${completionPercentage.toStringAsFixed(0)}% abgeschlossen',
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.white.withValues(alpha: 230),
),
),
if (state.isSyncing)
Row(
children: [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white.withValues(alpha: 204),
),
),
),
const SizedBox(width: 8),
Text(
'Sync...',
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.white.withValues(alpha: 230),
),
),
],
),
],
),
],
),
);
}
Widget _buildQuickActionCard(
BuildContext context,
String title,
IconData icon,
Color color,
String subtitle,
VoidCallback onTap,
) {
final theme = Theme.of(context);
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: Colors.grey.shade200),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withValues(alpha: 26),
borderRadius: BorderRadius.circular(14),
),
child: Icon(
icon,
color: color,
size: 28,
),
),
const SizedBox(height: 16),
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey.shade600,
),
),
],
),
),
),
);
}
Widget _buildActivityItem(
BuildContext context,
String title,
String subtitle,
String time,
IconData icon,
Color color,
) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.grey.shade200),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withValues(alpha: 26),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 22,
),
),
title: Text(
title,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
subtitle: Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey.shade600,
),
),
trailing: Text(
time,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey.shade500,
),
),
),
),
);
}
}

View File

@@ -0,0 +1,327 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/tour.dart';
import '../../../core/constants/app_constants.dart';
import '../../blocs/tour/tour_bloc.dart';
import '../../widgets/tour_list_item.dart';
import '../../widgets/loading_indicator.dart';
import '../../widgets/error_view.dart';
import '../tour_types/stock_start_page.dart';
import '../tour_types/veh_start_page.dart';
import '../tour_types/veh_page.dart';
import '../tour_types/fsa_page.dart';
import '../tour_types/vs_page.dart';
import '../tour_types/gi_page.dart';
import '../tour_types/veh_end_page.dart';
import '../tour_types/stock_end_page.dart';
import '../scan/scan_page.dart';
class ToursPage extends StatelessWidget {
const ToursPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocBuilder<TourBloc, TourState>(
builder: (context, state) {
if (state is TourLoading) {
return const LoadingIndicator(message: 'Touren werden geladen...');
}
if (state is TourError) {
return ErrorView(
message: state.message,
onRetry: () => context.read<TourBloc>().add(const RefreshTours()),
);
}
if (state is ToursLoaded) {
return _ToursListView(state: state);
}
return const LoadingIndicator();
},
),
);
}
}
class _ToursListView extends StatelessWidget {
final ToursLoaded state;
const _ToursListView({required this.state});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return CustomScrollView(
slivers: [
// Header mit Fortschritt
SliverToBoxAdapter(
child: _buildHeader(context),
),
// Offene Touren
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Row(
children: [
Icon(
Icons.location_on,
color: theme.colorScheme.primary,
size: 20,
),
const SizedBox(width: 8),
Text(
'Offene Stationen',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${state.tours.length}',
style: theme.textTheme.labelMedium?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final tour = state.tours[index];
return TourListItem(
tour: tour,
onTap: () => _onTourSelected(context, tour),
);
},
childCount: state.tours.length,
),
),
// Erledigte Touren (falls aktiviert)
if (state.showCompleted && state.completedTours.isNotEmpty) ...[
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Row(
children: [
const Icon(
Icons.check_circle,
color: Colors.green,
size: 20,
),
const SizedBox(width: 8),
Text(
'Erledigte Stationen',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${state.completedTours.length}',
style: theme.textTheme.labelMedium?.copyWith(
color: Colors.green,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final tour = state.completedTours[index];
return TourListItem(
tour: tour,
onTap: () => _onTourSelected(context, tour),
isCompleted: true,
);
},
childCount: state.completedTours.length,
),
),
],
const SliverPadding(padding: EdgeInsets.only(bottom: 100)),
],
);
}
Widget _buildHeader(BuildContext context) {
final theme = Theme.of(context);
final completionPercentage = state.completionPercentage;
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
theme.colorScheme.primary,
theme.colorScheme.primary.withValues(alpha: 204),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withValues(alpha: 77),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 51),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.route,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tagesübersicht',
style: theme.textTheme.titleSmall?.copyWith(
color: Colors.white.withValues(alpha: 204),
),
),
const SizedBox(height: 4),
Text(
'${state.completedCount} / ${state.totalCount} Stationen',
style: theme.textTheme.titleLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 20),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: LinearProgressIndicator(
value: completionPercentage / 100,
backgroundColor: Colors.white.withValues(alpha: 51),
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
minHeight: 8,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${completionPercentage.toStringAsFixed(0)}% abgeschlossen',
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.white.withValues(alpha: 230),
),
),
if (state.isSyncing)
Row(
children: [
SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white.withValues(alpha: 204),
),
),
),
const SizedBox(width: 6),
Text(
'Sync...',
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.white.withValues(alpha: 230),
),
),
],
),
],
),
],
),
);
}
void _onTourSelected(BuildContext context, Tour tour) {
context.read<TourBloc>().add(SelectTour(tour));
// Navigation zur tour-spezifischen Page
final page = _getPageForTourType(tour);
Navigator.push(
context,
MaterialPageRoute(builder: (_) => page),
);
}
Widget _getPageForTourType(Tour tour) {
switch (tour.type) {
case TourTypes.stockStart:
return StockStartPage(tour: tour);
case TourTypes.vehStart:
return VehStartPage(tour: tour);
case TourTypes.veh:
return VehPage(tour: tour);
case TourTypes.fsa:
return FsaPage(tour: tour);
case TourTypes.vs:
return VsPage(tour: tour);
case TourTypes.gi:
return GiPage(tour: tour);
case TourTypes.vehEnd:
return VehEndPage(tour: tour);
case TourTypes.stockEnd:
return StockEndPage(tour: tour);
case TourTypes.stock:
// Stock (HADAG) uses similar UI to stock_start
return StockStartPage(tour: tour);
default:
// Fallback to generic scan page for unknown types
return ScanPage(tour: tour);
}
}
}