This commit is contained in:
MarcWieland
2026-07-26 23:58:12 +02:00
commit 411ede36ef
596 changed files with 39941 additions and 0 deletions
+503
View File
@@ -0,0 +1,503 @@
import 'package:flutter/material.dart';
import '../models/customer.dart';
import '../models/order.dart';
import '../theme/app_theme.dart';
class CustomerDetailScreen extends StatelessWidget {
final Customer customer;
const CustomerDetailScreen({
super.key,
required this.customer,
});
String _formatDate(DateTime date) {
final day = date.day.toString().padLeft(2, '0');
final month = date.month.toString().padLeft(2, '0');
return '$day.$month.${date.year}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
title: const Text('Kundendetails'),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.more_horiz),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Optionen geöffnet')),
);
},
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top Customer Profile Card
_buildProfileHeader(context),
const SizedBox(height: 16),
// Quick Action Buttons
_buildQuickActions(context),
const SizedBox(height: 20),
// Section 1: Kundeninformationen
_buildCustomerInfoSection(),
const SizedBox(height: 20),
// Section 2: Verbaute Teile (letzter Auftrag)
_buildInstalledPartsSection(),
const SizedBox(height: 20),
// Section 3: Aufträge
_buildWorkOrdersSection(),
const SizedBox(height: 24),
],
),
),
);
}
Widget _buildProfileHeader(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
radius: 28,
backgroundColor: AppTheme.primaryBlue,
child: const Icon(
Icons.person,
color: Colors.white,
size: 32,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
customer.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
customer.street,
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
Text(
'${customer.zipCode} ${customer.city}',
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
],
),
),
Container(
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppTheme.cardBorder),
),
child: IconButton(
icon: const Icon(Icons.phone_outlined, color: AppTheme.textPrimary),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Anruf gestartet: ${customer.phone}')),
);
},
),
),
],
),
),
);
}
Widget _buildQuickActions(BuildContext context) {
return Row(
children: [
_buildActionButton(
context,
icon: Icons.navigation_outlined,
label: 'Route',
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Navigation zu ${customer.fullAddress} gestartet')),
);
},
),
const SizedBox(width: 10),
_buildActionButton(
context,
icon: Icons.phone_outlined,
label: 'Anrufen',
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Anrufen: ${customer.phone}')),
);
},
),
const SizedBox(width: 10),
_buildActionButton(
context,
icon: Icons.email_outlined,
label: 'E-Mail',
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('E-Mail an: ${customer.email}')),
);
},
),
const SizedBox(width: 10),
_buildActionButton(
context,
icon: Icons.edit_outlined,
label: 'Bearbeiten',
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Kunde bearbeiten')),
);
},
),
],
);
}
Widget _buildActionButton(
BuildContext context, {
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return Expanded(
child: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppTheme.cardBorder),
),
child: Column(
children: [
Icon(icon, color: AppTheme.primaryBlue, size: 22),
const SizedBox(height: 6),
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
],
),
),
),
),
);
}
Widget _buildCustomerInfoSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Kundeninformationen',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 10),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
children: [
_buildInfoRow('Kundennummer', customer.customerNumber),
const Divider(height: 1, color: AppTheme.cardBorder),
_buildInfoRow('Telefon', customer.phone),
const Divider(height: 1, color: AppTheme.cardBorder),
_buildInfoRow('E-Mail', customer.email),
const Divider(height: 1, color: AppTheme.cardBorder),
_buildInfoRow('Erstkontakt', _formatDate(customer.firstContactDate)),
if (customer.notes != null) ...[
const Divider(height: 1, color: AppTheme.cardBorder),
_buildInfoRow('Notizen', customer.notes!),
],
],
),
),
),
],
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
label,
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
),
Expanded(
child: Text(
value,
textAlign: TextAlign.end,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
),
],
),
);
}
Widget _buildInstalledPartsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Verbaute Teile (letzter Auftrag)',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 10),
Card(
child: customer.installedParts.isEmpty
? const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
'Keine verbauten Teile vorhanden.',
style: TextStyle(color: AppTheme.textSecondary),
),
),
)
: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: customer.installedParts.length,
separatorBuilder: (context, index) =>
const Divider(height: 1, color: AppTheme.cardBorder),
itemBuilder: (context, index) {
final part = customer.installedParts[index];
return Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.cardBorder),
),
child: Icon(
_getPartIcon(part.category),
color: AppTheme.textSecondary,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
part.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 2),
Text(
part.category,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 2),
Text(
'Seriennr.: ${part.serialNumber}',
style: const TextStyle(
fontSize: 11,
color: AppTheme.textMuted,
),
),
],
),
),
Text(
'${part.quantity} Stk.',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
],
),
);
},
),
),
],
);
}
IconData _getPartIcon(String category) {
if (category.contains('Heiz') || category.contains('Gas')) {
return Icons.hvac_outlined;
} else if (category.contains('thermostat') || category.contains('Raum')) {
return Icons.thermostat_outlined;
} else if (category.contains('Filter') || category.contains('Wasser')) {
return Icons.filter_alt_outlined;
} else if (category.contains('Pumpe')) {
return Icons.speed_outlined;
}
return Icons.build_outlined;
}
Widget _buildWorkOrdersSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Aufträge',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 10),
Card(
child: customer.orders.isEmpty
? const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
'Keine Aufträge vorhanden.',
style: TextStyle(color: AppTheme.textSecondary),
),
),
)
: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: customer.orders.length,
separatorBuilder: (context, index) =>
const Divider(height: 1, color: AppTheme.cardBorder),
itemBuilder: (context, index) {
final order = customer.orders[index];
final isCompleted = order.status == OrderStatus.abgeschlossen;
return Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_formatDate(order.date),
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 4),
Text(
order.title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: isCompleted
? const Color(0xFFDCFCE7)
: const Color(0xFFFEF3C7),
borderRadius: BorderRadius.circular(6),
),
child: Text(
order.status.label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: isCompleted
? const Color(0xFF166534)
: const Color(0xFF92400E),
),
),
),
const SizedBox(width: 8),
const Icon(
Icons.chevron_right,
color: AppTheme.textSecondary,
size: 20,
),
],
),
);
},
),
),
],
);
}
}
+341
View File
@@ -0,0 +1,341 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import '../models/customer.dart';
import '../theme/app_theme.dart';
import '../widgets/customer_preview_sheet.dart';
import '../services/database_service.dart';
class CustomerMapScreen extends StatefulWidget {
const CustomerMapScreen({super.key});
@override
State<CustomerMapScreen> createState() => _CustomerMapScreenState();
}
class _CustomerMapScreenState extends State<CustomerMapScreen> {
final MapController _mapController = MapController();
Customer? _selectedCustomer;
String _searchQuery = '';
final TextEditingController _searchController = TextEditingController();
List<Customer> _allCustomers = [];
// Initial center position (Musterstadt / Frankfurt coordinates)
static final LatLng _initialCenter = LatLng(50.1109, 8.6821);
@override
void initState() {
super.initState();
_loadCustomers();
}
Future<void> _loadCustomers() async {
final list = await DatabaseService.getCustomers();
if (!mounted) return;
setState(() {
_allCustomers = list;
if (_allCustomers.isNotEmpty) {
_selectedCustomer = _allCustomers.firstWhere(
(c) => c.customerNumber == 'K-10025',
orElse: () => _allCustomers.first,
);
}
});
}
@override
void dispose() {
_searchController.dispose();
_mapController.dispose();
super.dispose();
}
List<Customer> get _filteredCustomers {
if (_searchQuery.trim().isEmpty) {
return _allCustomers;
}
final q = _searchQuery.toLowerCase();
return _allCustomers.where((c) {
return c.name.toLowerCase().contains(q) ||
c.customerNumber.toLowerCase().contains(q) ||
c.city.toLowerCase().contains(q) ||
c.street.toLowerCase().contains(q);
}).toList();
}
void _selectCustomer(Customer customer) {
setState(() {
_selectedCustomer = customer;
});
_mapController.move(
LatLng(customer.latitude, customer.longitude),
14.5,
);
}
void _recenterMap() {
_mapController.move(
_initialCenter,
13.0,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: Column(
children: [
// Dark Navy Top Header Bar (matches Mockup NaviScreen.png)
Container(
color: AppTheme.primaryDark,
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.menu, color: Colors.white),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Menü geöffnet')),
);
},
),
const Expanded(
child: Text(
'Kunden',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
IconButton(
icon: const Icon(Icons.search, color: Colors.white),
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.tune, color: Colors.white),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Filter geöffnet')),
);
},
),
],
),
),
),
),
// Map Area + Floating Overlays
Expanded(
child: Stack(
children: [
// Clean Minimalistic CartoDB Tile Layer Map View
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: _selectedCustomer != null
? LatLng(_selectedCustomer!.latitude, _selectedCustomer!.longitude)
: _initialCenter,
initialZoom: 13.5,
// Enable full multi-touch gestures (Pinch Zoom, Pan, Double Tap)
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all,
),
onTap: (tapPosition, point) {
setState(() {
_selectedCustomer = null;
});
},
),
children: [
TileLayer(
// Clean minimal map tiles matching the mockup design
urlTemplate: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
subdomains: const ['a', 'b', 'c', 'd'],
retinaMode: RetinaMode.isHighDensity(context),
userAgentPackageName: 'de.handwerksfreund.app',
),
MarkerLayer(
markers: _filteredCustomers.map((customer) {
final isSelected = _selectedCustomer?.id == customer.id;
return Marker(
width: isSelected ? 50 : 40,
height: isSelected ? 50 : 40,
point: LatLng(customer.latitude, customer.longitude),
child: GestureDetector(
onTap: () => _selectCustomer(customer),
child: isSelected
? Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppTheme.primaryBlue.withAlpha(102),
blurRadius: 12,
spreadRadius: 4,
),
],
),
padding: const EdgeInsets.all(4),
child: Container(
decoration: const BoxDecoration(
color: AppTheme.primaryBlue,
shape: BoxShape.circle,
),
child: const Icon(
Icons.location_on,
color: Colors.white,
size: 26,
),
),
)
: const Icon(
Icons.location_on,
color: AppTheme.primaryBlue,
size: 38,
),
),
);
}).toList(),
),
],
),
// Dim Overlay when customer sheet is selected
if (_selectedCustomer != null)
Positioned.fill(
child: GestureDetector(
onTap: () {
setState(() {
_selectedCustomer = null;
});
},
child: Container(
color: Colors.black.withAlpha(51),
),
),
),
// Search Input Card overlay
Positioned(
top: 12,
left: 16,
right: 16,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: TextField(
controller: _searchController,
onChanged: (val) {
setState(() {
_searchQuery = val;
});
},
decoration: InputDecoration(
hintText: 'Kunden suchen...',
hintStyle: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
prefixIcon: const Icon(
Icons.search,
color: AppTheme.textSecondary,
),
suffixIcon: IconButton(
icon: const Icon(
Icons.tune,
color: AppTheme.textSecondary,
size: 20,
),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Filteroptionen geöffnet')),
);
},
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
),
),
),
// Map Action Buttons (Center location & Add customer FAB)
Positioned(
right: 16,
bottom: _selectedCustomer != null ? 230 : 24,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton.small(
heroTag: 'recenter_fab',
onPressed: _recenterMap,
backgroundColor: Colors.white,
foregroundColor: AppTheme.primaryDark,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.my_location, size: 20),
),
const SizedBox(height: 12),
FloatingActionButton(
heroTag: 'add_customer_fab',
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Neuen Kunden anlegen')),
);
},
backgroundColor: AppTheme.primaryDark,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: const Icon(Icons.add, size: 28),
),
],
),
),
// Bottom Sheet Customer Preview Card Overlay
if (_selectedCustomer != null)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: CustomerPreviewSheet(
customer: _selectedCustomer!,
onClose: () {
setState(() {
_selectedCustomer = null;
});
},
),
),
],
),
),
],
),
);
}
}
+64
View File
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class DummyScreen extends StatelessWidget {
final String title;
final IconData icon;
const DummyScreen({
super.key,
required this.title,
required this.icon,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
title: Text(title),
centerTitle: true,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.lightBlueBg,
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 48,
color: AppTheme.primaryBlue,
),
),
const SizedBox(height: 20),
Text(
title,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: AppTheme.primaryDark,
),
),
const SizedBox(height: 8),
const Text(
'Diese Funktion wird im nächsten Schritt ausgebaut.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
),
),
);
}
}
+66
View File
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'customer_map_screen.dart';
import 'dummy_screen.dart';
import 'more_screen.dart';
import 'orders_screen.dart';
class MainNavigationScreen extends StatefulWidget {
const MainNavigationScreen({super.key});
@override
State<MainNavigationScreen> createState() => _MainNavigationScreenState();
}
class _MainNavigationScreenState extends State<MainNavigationScreen> {
// Start on tab 1 (Kunden / Map Screen) as requested in the mockup
int _currentIndex = 1;
final List<Widget> _screens = const [
DummyScreen(title: 'Übersicht', icon: Icons.grid_view),
CustomerMapScreen(),
OrdersScreen(),
DummyScreen(title: 'Kalender', icon: Icons.calendar_month_outlined),
MoreScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _screens,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.grid_view),
label: 'Übersicht',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: 'Kunden',
),
BottomNavigationBarItem(
icon: Icon(Icons.receipt_long_outlined),
label: 'Aufträge',
),
BottomNavigationBarItem(
icon: Icon(Icons.calendar_month_outlined),
label: 'Kalender',
),
BottomNavigationBarItem(
icon: Icon(Icons.more_horiz),
label: 'Mehr',
),
],
),
);
}
}
+506
View File
@@ -0,0 +1,506 @@
import 'package:flutter/material.dart';
import '../services/database_service.dart';
import '../theme/app_theme.dart';
import '../widgets/system_status_dialog.dart';
class MoreScreen extends StatefulWidget {
const MoreScreen({super.key});
@override
State<MoreScreen> createState() => _MoreScreenState();
}
class _MoreScreenState extends State<MoreScreen> {
bool _notificationsEnabled = true;
SystemStatusResult? _statusResult;
bool _isCheckingStatus = true;
@override
void initState() {
super.initState();
_checkStatus();
}
Future<void> _checkStatus() async {
final status = await DatabaseService.testConnection();
if (!mounted) return;
setState(() {
_statusResult = status;
_isCheckingStatus = false;
});
}
void _openSystemStatusDetails() async {
setState(() {
_isCheckingStatus = true;
});
final status = await DatabaseService.testConnection();
if (!mounted) return;
setState(() {
_statusResult = status;
_isCheckingStatus = false;
});
showDialog(
context: context,
builder: (context) => SystemStatusDialog(status: status),
);
}
@override
Widget build(BuildContext context) {
final isConnected = _statusResult?.isConnected ?? DatabaseService.isInitialized;
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: Column(
children: [
// Dark Navy Top Header Bar (matches Mockup Mehr_Page.png)
Container(
color: AppTheme.primaryDark,
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.menu, color: Colors.white),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Menü geöffnet')),
);
},
),
const Expanded(
child: Text(
'Mehr',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
IconButton(
icon: const CircleAvatar(
radius: 14,
backgroundColor: Colors.white,
child: Icon(Icons.person, color: AppTheme.primaryDark, size: 18),
),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Profil geöffnet')),
);
},
),
],
),
),
),
),
// Scrollable Settings Content Body
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Profile Header Card
_buildProfileCard(context),
const SizedBox(height: 16),
// Systemstatus Card (Interactive Supabase Connection Status)
_buildSystemStatusCard(context, isConnected),
const SizedBox(height: 20),
// Gruppe: Allgemein
_buildSectionTitle('Gruppe: Allgemein'),
const SizedBox(height: 8),
Card(
child: Column(
children: [
_buildSwitchTile(
icon: Icons.notifications_outlined,
title: 'Benachrichtigungen',
value: _notificationsEnabled,
onChanged: (val) {
setState(() {
_notificationsEnabled = val;
});
},
),
const Divider(height: 1, color: AppTheme.cardBorder),
_buildValueTile(
icon: Icons.language_outlined,
title: 'App-Sprache',
value: 'Deutsch',
onTap: () {},
),
const Divider(height: 1, color: AppTheme.cardBorder),
_buildTile(
icon: Icons.shield_outlined,
title: 'Sicherheit & Datenschutz',
onTap: () {},
),
],
),
),
const SizedBox(height: 20),
// Gruppe: Verwaltung
_buildSectionTitle('Gruppe: Verwaltung'),
const SizedBox(height: 8),
Card(
child: Column(
children: [
_buildTile(
icon: Icons.event_available_outlined,
title: 'Urlaub & Abwesenheit',
badgeText: 'HR',
onTap: () {},
),
const Divider(height: 1, color: AppTheme.cardBorder),
_buildTile(
icon: Icons.build_outlined,
title: 'Meine Werkzeug-Ausstattung',
onTap: () {},
),
],
),
),
const SizedBox(height: 20),
// Gruppe: App-Info
_buildSectionTitle('Gruppe: App-Info'),
const SizedBox(height: 8),
Card(
child: Column(
children: [
_buildTile(
icon: Icons.info_outline,
title: 'Impressum',
onTap: () {},
),
const Divider(height: 1, color: AppTheme.cardBorder),
_buildTile(
icon: Icons.article_outlined,
title: 'Datenschutz',
onTap: () {},
),
const Divider(height: 1, color: AppTheme.cardBorder),
_buildTile(
icon: Icons.star_outline,
title: 'Feedback zur App',
onTap: () {},
),
],
),
),
const SizedBox(height: 24),
// Logout Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Abgemeldet')),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFDC2626),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Abmelden',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 16),
],
),
),
),
],
),
);
}
Widget _buildProfileCard(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
CircleAvatar(
radius: 26,
backgroundColor: AppTheme.primaryBlue,
child: const Icon(
Icons.person,
color: Colors.white,
size: 30,
),
),
const SizedBox(width: 14),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Max Müller',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 2),
Text(
'Monteur / ID: M-01023',
style: TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
],
),
],
),
const SizedBox(height: 14),
OutlinedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Profil bearbeiten')),
);
},
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textPrimary,
side: const BorderSide(color: AppTheme.cardBorder),
minimumSize: const Size(double.infinity, 40),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
'Profil bearbeiten',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
);
}
Widget _buildSystemStatusCard(BuildContext context, bool isConnected) {
final statusText = _isCheckingStatus
? 'Verbindung wird geprüft...'
: (isConnected ? 'Verbunden: Supabase' : 'Offline / Mock-Modus');
final statusColor = _isCheckingStatus
? Colors.orange[700]
: (isConnected ? Colors.green[800] : Colors.red[800]);
final dotColor = _isCheckingStatus
? Colors.orange
: (isConnected ? Colors.green[600] : Colors.red);
return Card(
child: InkWell(
onTap: _openSystemStatusDetails,
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Systemstatus',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.lightBlueBg,
shape: BoxShape.circle,
),
child: _isCheckingStatus
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.primaryBlue),
)
: const Icon(
Icons.bolt,
color: AppTheme.primaryBlue,
size: 20,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: dotColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
statusText,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: statusColor,
),
),
],
),
const SizedBox(height: 4),
Text(
_statusResult != null && _statusResult!.latencyMs > 0
? 'Latenz: ${_statusResult!.latencyMs} ms • supabase.marc-wieland.de'
: 'Tippen für erweiterte Verbindungsdetails',
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
),
);
}
Widget _buildSectionTitle(String title) {
return Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
);
}
Widget _buildTile({
required IconData icon,
required String title,
String? badgeText,
required VoidCallback onTap,
}) {
return ListTile(
leading: Icon(icon, color: AppTheme.primaryBlue, size: 22),
title: Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
trailing: badgeText != null
? Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: AppTheme.lightBlueBg,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppTheme.primaryBlue.withAlpha(50)),
),
child: Text(
'$badgeText',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: AppTheme.primaryBlue,
),
),
)
: null,
onTap: onTap,
);
}
Widget _buildValueTile({
required IconData icon,
required String title,
required String value,
required VoidCallback onTap,
}) {
return ListTile(
leading: Icon(icon, color: AppTheme.primaryBlue, size: 22),
title: Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
trailing: Text(
value,
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
onTap: onTap,
);
}
Widget _buildSwitchTile({
required IconData icon,
required String title,
required bool value,
required ValueChanged<bool> onChanged,
}) {
return ListTile(
leading: Icon(icon, color: AppTheme.primaryBlue, size: 22),
title: Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
trailing: Switch.adaptive(
value: value,
activeThumbColor: AppTheme.primaryBlue,
onChanged: onChanged,
),
);
}
}
+157
View File
@@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import '../data/mock_data.dart';
import '../theme/app_theme.dart';
import '../widgets/orders_day_view.dart';
import '../widgets/orders_week_view.dart';
class OrdersScreen extends StatefulWidget {
const OrdersScreen({super.key});
@override
State<OrdersScreen> createState() => _OrdersScreenState();
}
class _OrdersScreenState extends State<OrdersScreen> {
bool _isWeekView = false;
DateTime _selectedDate = DateTime(2024, 5, 16);
String _formatDate(DateTime date) {
final day = date.day.toString().padLeft(2, '0');
final month = date.month.toString().padLeft(2, '0');
return 'Heute, $day.$month.${date.year}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: Column(
children: [
// Dark Navy Top Header Bar (matches Mockup Auftragsuebersicht.png)
Container(
color: AppTheme.primaryDark,
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.menu, color: Colors.white),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Menü geöffnet')),
);
},
),
const Expanded(
child: Text(
'Aufträge',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
IconButton(
icon: const CircleAvatar(
radius: 14,
backgroundColor: Colors.white,
child: Icon(Icons.person, color: AppTheme.primaryDark, size: 18),
),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Profil geöffnet')),
);
},
),
],
),
),
),
),
// Date Navigator & View Switch Controls Bar
Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
// Date Selector Chip `< Heute, 16.05.2024 >`
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.cardBorder),
),
child: Row(
children: [
InkWell(
onTap: () {
setState(() {
_selectedDate = _selectedDate.subtract(const Duration(days: 1));
});
},
child: const Icon(Icons.chevron_left, size: 20, color: AppTheme.textSecondary),
),
const SizedBox(width: 6),
Text(
_formatDate(_selectedDate),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(width: 6),
InkWell(
onTap: () {
setState(() {
_selectedDate = _selectedDate.add(const Duration(days: 1));
});
},
child: const Icon(Icons.chevron_right, size: 20, color: AppTheme.textSecondary),
),
],
),
),
const Spacer(),
// "Woche" Toggle Switch
const Text(
'Woche',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(width: 6),
Switch.adaptive(
value: _isWeekView,
activeThumbColor: AppTheme.primaryBlue,
onChanged: (val) {
setState(() {
_isWeekView = val;
});
},
),
],
),
),
const Divider(height: 1, color: AppTheme.cardBorder),
// Dynamic Body (Day Timeline View or Week Matrix View)
Expanded(
child: _isWeekView
? OrdersWeekView(summaries: MockData.weekSummaries)
: OrdersDayView(items: MockData.dayScheduleItems),
),
],
),
);
}
}