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
+204
View File
@@ -0,0 +1,204 @@
import 'package:flutter/material.dart';
import '../models/customer.dart';
import '../theme/app_theme.dart';
import '../screens/customer_detail_screen.dart';
class CustomerPreviewSheet extends StatelessWidget {
final Customer customer;
final VoidCallback? onClose;
const CustomerPreviewSheet({
super.key,
required this.customer,
this.onClose,
});
String _formatDate(DateTime? date) {
if (date == null) return '-';
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 Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 16,
offset: Offset(0, -4),
),
],
),
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Drag handle indicator
Center(
child: Container(
width: 36,
height: 4,
decoration: BoxDecoration(
color: AppTheme.cardBorder,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
// Customer info header row
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 24,
backgroundColor: AppTheme.primaryBlue,
child: const Icon(
Icons.person,
color: Colors.white,
size: 28,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
customer.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 2),
Text(
customer.street,
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
Text(
'${customer.zipCode} ${customer.city}',
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
],
),
),
Text(
'${customer.distanceKm.toStringAsFixed(1).replaceAll('.', ',')} km',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppTheme.primaryBlue,
),
),
],
),
const SizedBox(height: 16),
// Metric Cards Row
Row(
children: [
_buildMetricCard(
label: 'Kundennr.',
value: customer.customerNumber,
),
const SizedBox(width: 10),
_buildMetricCard(
label: 'Letzter Auftrag',
value: _formatDate(customer.lastOrderDate),
),
const SizedBox(width: 10),
_buildMetricCard(
label: 'Offene Aufträge',
value: customer.openOrdersCount.toString(),
),
],
),
const SizedBox(height: 16),
// Detail Action Button
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CustomerDetailScreen(customer: customer),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryDark,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Kundendetails anzeigen',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
SizedBox(width: 8),
Icon(Icons.chevron_right, size: 20),
],
),
),
],
),
);
}
Widget _buildMetricCard({required String label, required String value}) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppTheme.cardBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 11,
color: AppTheme.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}
+249
View File
@@ -0,0 +1,249 @@
import 'package:flutter/material.dart';
import '../models/schedule_item.dart';
import '../theme/app_theme.dart';
class OrdersDayView extends StatefulWidget {
final List<ScheduleItem> items;
const OrdersDayView({
super.key,
required this.items,
});
@override
State<OrdersDayView> createState() => _OrdersDayViewState();
}
class _OrdersDayViewState extends State<OrdersDayView> {
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
itemCount: widget.items.length,
itemBuilder: (context, index) {
final item = widget.items[index];
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Time Marker Column
SizedBox(
width: 50,
child: Text(
item.startTime,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
),
const SizedBox(width: 8),
// Content Card
Expanded(
child: _buildItemCard(context, item),
),
],
),
);
},
);
}
Widget _buildItemCard(BuildContext context, ScheduleItem item) {
switch (item.type) {
case ScheduleItemType.routine:
case ScheduleItemType.lunchBreak:
case ScheduleItemType.feierabend:
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: const Color(0xFFE2E8F0),
borderRadius: BorderRadius.circular(12),
),
child: Text(
item.title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
);
case ScheduleItemType.job:
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppTheme.cardBorder),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
clipBehavior: Clip.antiAlias,
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 5,
color: AppTheme.primaryBlue,
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
if (item.customerName != null || item.address != null)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppTheme.lightBlueBg,
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
Icons.location_on,
color: AppTheme.primaryBlue,
size: 18,
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.customerName != null)
Text(
item.customerName!,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
if (item.address != null)
Text(
item.address!,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
],
),
if (item.taskDescription != null) ...[
const SizedBox(height: 8),
Text(
item.taskDescription!,
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
],
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
setState(() {
item.isInProgress = true;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Anfahrt gestartet zu ${item.customerName ?? "Kunde"}'),
backgroundColor: AppTheme.primaryBlue,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryBlue,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Anfahrt starten',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: () {
setState(() {
item.isCompleted = true;
item.isInProgress = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Auftrag "${item.title}" als abgeschlossen markiert.'),
backgroundColor: Colors.green[700],
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: item.isCompleted
? Colors.green[100]
: const Color(0xFFE2E8F0),
foregroundColor: item.isCompleted
? Colors.green[900]
: AppTheme.textPrimary,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
item.isCompleted ? 'Abgeschlossen' : 'Auftrag abschließen',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
],
),
),
),
],
),
),
);
}
}
}
+307
View File
@@ -0,0 +1,307 @@
import 'package:flutter/material.dart';
import '../models/schedule_item.dart';
import '../theme/app_theme.dart';
class OrdersWeekView extends StatefulWidget {
final List<DaySummary> summaries;
const OrdersWeekView({
super.key,
required this.summaries,
});
@override
State<OrdersWeekView> createState() => _OrdersWeekViewState();
}
class _OrdersWeekViewState extends State<OrdersWeekView> {
late int _selectedDayIndex;
bool _isAccordionExpanded = true;
@override
void initState() {
super.initState();
// Default select Friday (Fr) or Thursday (Do) as highlighted in mockup
_selectedDayIndex = widget.summaries.indexWhere((s) => s.dayName == 'Fr');
if (_selectedDayIndex < 0) _selectedDayIndex = 3;
}
@override
Widget build(BuildContext context) {
final selectedDay = widget.summaries[_selectedDayIndex];
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
children: [
// Weekly Summary Matrix Card
Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppTheme.cardBorder),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 6,
offset: Offset(0, 2),
),
],
),
child: Row(
children: [
// Labels Column (Aufträge, Stunden)
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24),
Text(
'Aufträge',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
SizedBox(height: 28),
Text(
'Stunden',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
],
),
const SizedBox(width: 8),
// Days Columns (Mo - So)
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(widget.summaries.length, (index) {
final summary = widget.summaries[index];
final isSelected = index == _selectedDayIndex;
return GestureDetector(
onTap: () {
setState(() {
_selectedDayIndex = index;
});
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryBlue : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text(
summary.dayName,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: isSelected ? Colors.white : AppTheme.textPrimary,
),
),
const SizedBox(height: 12),
// Aufträge Count Circle / Pill
Container(
width: 26,
height: 26,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF1D4ED8)
: AppTheme.backgroundLight,
shape: BoxShape.circle,
),
child: Text(
'${summary.ordersCount}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: isSelected ? Colors.white : AppTheme.textPrimary,
),
),
),
const SizedBox(height: 16),
// Stunden Text Box
Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF1D4ED8)
: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(6),
),
child: Text(
summary.hoursTotal.toStringAsFixed(1),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: isSelected ? Colors.white : AppTheme.textPrimary,
),
),
),
],
),
),
);
}),
),
),
],
),
),
const SizedBox(height: 20),
// Collapsible Day Details Card Accordion
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppTheme.cardBorder),
),
child: Column(
children: [
// Header Bar for Selected Day
InkWell(
onTap: () {
setState(() {
_isAccordionExpanded = !_isAccordionExpanded;
});
},
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${selectedDay.fullDayName}, ${selectedDay.dateString}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
Icon(
_isAccordionExpanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
color: AppTheme.textSecondary,
),
],
),
),
),
if (_isAccordionExpanded) ...[
const Divider(height: 1, color: AppTheme.cardBorder),
if (selectedDay.items.isEmpty)
const Padding(
padding: EdgeInsets.all(20),
child: Text(
'Keine Aufträge für diesen Tag eingetragen.',
style: TextStyle(color: AppTheme.textSecondary),
),
)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
itemCount: selectedDay.items.length,
separatorBuilder: (context, index) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final item = selectedDay.items[index];
return _buildWeekJobCard(item);
},
),
],
],
),
),
],
),
);
}
Widget _buildWeekJobCard(ScheduleItem item) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppTheme.cardBorder),
),
child: Row(
children: [
// Icon Container
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.cardBorder),
),
child: Icon(
item.isCompleted ? Icons.hvac_outlined : Icons.location_on,
color: item.isCompleted ? AppTheme.textSecondary : AppTheme.primaryBlue,
size: 22,
),
),
const SizedBox(width: 12),
// Content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.customerName ?? item.title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
if (item.address != null) ...[
const SizedBox(height: 2),
Text(
item.address!,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
if (item.taskDescription != null) ...[
const SizedBox(height: 2),
Text(
item.taskDescription!,
style: const TextStyle(
fontSize: 11,
color: AppTheme.textMuted,
),
),
],
],
),
),
// Status Badge / Icon
Icon(
item.isCompleted ? Icons.check_circle_outline : Icons.person_outline,
color: item.isCompleted ? Colors.green[700] : AppTheme.textSecondary,
size: 20,
),
],
),
);
}
}
+269
View File
@@ -0,0 +1,269 @@
import 'package:flutter/material.dart';
import '../services/database_service.dart';
import '../theme/app_theme.dart';
class SystemStatusDialog extends StatelessWidget {
final SystemStatusResult status;
const SystemStatusDialog({
super.key,
required this.status,
});
String _formatTime(DateTime date) {
final h = date.hour.toString().padLeft(2, '0');
final m = date.minute.toString().padLeft(2, '0');
final s = date.second.toString().padLeft(2, '0');
return '$h:$m:$s Uhr';
}
@override
Widget build(BuildContext context) {
final isConnected = status.isConnected;
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title Header with Icon
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: isConnected
? const Color(0xFFDCFCE7)
: const Color(0xFFFEE2E2),
shape: BoxShape.circle,
),
child: Icon(
Icons.bolt,
color: isConnected
? const Color(0xFF166534)
: const Color(0xFF991B1B),
size: 24,
),
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Systemstatus Details',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
Text(
'Datenbank & API Verbindung',
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, color: AppTheme.textSecondary),
onPressed: () => Navigator.of(context).pop(),
),
],
),
const SizedBox(height: 12),
const Divider(height: 1, color: AppTheme.cardBorder),
const SizedBox(height: 12),
// Scrollable Details Body
Flexible(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Connection Badge Status Box
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isConnected
? const Color(0xFFF0FDF4)
: const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isConnected
? const Color(0xFFBBF7D0)
: const Color(0xFFFECACA),
),
),
child: Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: isConnected
? const Color(0xFF22C55E)
: const Color(0xFFEF4444),
shape: BoxShape.circle,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
isConnected
? 'Verbunden: Supabase (Self-Hosted)'
: 'Offline / Verbindung fehlgeschlagen',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: isConnected
? const Color(0xFF15803D)
: const Color(0xFFB91C1C),
),
),
),
],
),
),
const SizedBox(height: 16),
// Technical Details List
_buildDetailRow('Host Server', status.host),
_buildDetailRow('Datenbank Name', status.dbName),
_buildDetailRow(
'Latenz (Ping)',
'${status.latencyMs} ms',
valueColor: status.latencyMs < 100
? Colors.green[700]
: Colors.orange[800],
),
_buildDetailRow(
'Sicherheit / SSL',
status.isSslEnabled ? 'TLS / HTTPS (Aktiv)' : 'Insecure / HTTP',
),
_buildDetailRow(
'Letzte Synchronisation',
_formatTime(status.lastSync),
),
// Error Box (Scrollable max height)
if (status.errorMessage != null) ...[
const SizedBox(height: 14),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFFECACA)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.warning_amber_rounded, size: 16, color: Color(0xFF991B1B)),
SizedBox(width: 6),
Text(
'Fehlermeldung:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFF991B1B),
),
),
],
),
const SizedBox(height: 6),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 160),
child: SingleChildScrollView(
child: SelectableText(
status.errorMessage!,
style: const TextStyle(
fontSize: 11,
color: Color(0xFFB91C1C),
fontFamily: 'monospace',
),
),
),
),
],
),
),
],
],
),
),
),
const SizedBox(height: 16),
// Close Action Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryDark,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text('Schließen'),
),
),
],
),
),
),
);
}
Widget _buildDetailRow(String label, String value, {Color? valueColor}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
value,
textAlign: TextAlign.end,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: valueColor ?? AppTheme.textPrimary,
),
),
),
],
),
);
}
}