import 'package:appwrite/appwrite.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../appwrite_config.dart'; import '../../config/appwrite_rechnungen_setup.dart'; import '../../config/branding.dart'; import '../../utils/appwrite_error_message.dart'; import '../../legal/legal_content.dart'; import '../../models/auftrag.dart'; import '../../models/auftrag_list_sort.dart'; import '../../models/auftrag_status.dart'; import '../../models/dokument_typ.dart'; import '../../models/zahlungs_status.dart'; import '../../services/auftrag_repository.dart'; import '../../services/data_export_service.dart'; import '../../services/pdf_history_service.dart'; import '../../services/workflow_metrics_service.dart'; import '../../theme/app_theme.dart'; import '../auftrag/auftrag_bearbeiten_screen.dart'; import '../legal/legal_document_screen.dart'; /// Haupt-Navigation wie im Mockup: Start, Aufträge, PDFs, Profil. class MainShellScreen extends StatefulWidget { const MainShellScreen({super.key, this.onLoggedOut}); final VoidCallback? onLoggedOut; @override State createState() => _MainShellScreenState(); } class _MainShellScreenState extends State { final _repo = AuftragRepository(); final _account = Account(appwriteClient); final _searchCtrl = TextEditingController(); int _tab = 0; String? _name; String? _email; List _list = []; bool _loading = true; String? _error; AuftragListenSort _sort = AuftragListenSort.datumNeu; List _pdfHistory = []; String _appVersion = ''; WorkflowMetricsSnapshot? _metrics; @override void initState() { super.initState(); _searchCtrl.addListener(() => setState(() {})); _refresh(); _loadPdfHistory(); _loadMetrics(); PackageInfo.fromPlatform().then((p) { if (mounted) { setState(() => _appVersion = '${p.version} (${p.buildNumber})'); } }); } Future _loadPdfHistory() async { final h = await PdfHistoryService.load(); if (mounted) setState(() => _pdfHistory = h); } Future _loadMetrics() async { final m = await WorkflowMetricsService.load(); if (mounted) setState(() => _metrics = m); } @override void dispose() { _searchCtrl.dispose(); super.dispose(); } List get _filteredList { final q = _searchCtrl.text.trim().toLowerCase(); if (q.isEmpty) return _list; bool hit(String s) => s.toLowerCase().contains(q); return _list.where((a) { return hit(a.titel) || hit(a.kundenName) || hit(a.beschreibung) || hit(a.kundenAdresse) || hit(a.rechnungsnummer) || hit(a.dokumentTyp.labelDe) || hit(a.zahlungsStatus.labelDe); }).toList(); } Future _refresh() async { setState(() { _loading = true; _error = null; }); try { final u = await _account.get(); final list = await _repo.listAuftraege(); if (!mounted) return; setState(() { _name = u.name; _email = u.email; _list = list; _loading = false; }); await _loadPdfHistory(); await _loadMetrics(); } catch (e) { if (!mounted) return; setState(() { _error = nachrichtFuerAppwriteDatenFehler(e); _loading = false; }); } } Future _openEditor({String? auftragId}) async { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => auftragId != null ? AuftragBearbeitenScreen(auftragId: auftragId) : const AuftragBearbeitenScreen(), ), ); if (mounted) await _refresh(); } List get _sortedAll => sortAuftraege(_list, _sort); List get _filteredSorted => sortAuftraege(_filteredList, _sort); Widget _retryLoadSliver() { return SliverFillRemaining( hasScrollBody: false, child: Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Daten konnten nicht geladen werden.', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey.shade400), ), const SizedBox(height: 8), SelectableText( _error ?? '', textAlign: TextAlign.center, style: const TextStyle( color: Color(0xFF888888), fontSize: 12, height: 1.35, ), ), const SizedBox(height: 20), FilledButton.icon( onPressed: _loading ? null : _refresh, icon: const Icon(Icons.refresh), label: const Text('Erneut versuchen'), ), ], ), ), ), ); } bool _istForderungsDokument(DokumentTyp t) => t == DokumentTyp.rechnung || t == DokumentTyp.abschlag || t == DokumentTyp.schlussrechnung || t == DokumentTyp.mahnung; int get _offenePostenCount => _list.where((a) { return _istForderungsDokument(a.dokumentTyp) && a.zahlungsStatus == ZahlungsStatus.offen; }).length; int get _ueberfaelligCount { final heute = DateTime.now(); final tagHeute = DateTime(heute.year, heute.month, heute.day); return _list.where((a) { if (a.zahlungsStatus == ZahlungsStatus.bezahlt) return false; final f = a.faelligAm; if (f == null) return false; final tf = DateTime(f.year, f.month, f.day); return !tf.isAfter(tagHeute); }).length; } Future _exportData() async { try { final list = await _repo.listAuftraege(); final file = await DataExportService.exportAuftraegeToTempFile(list); if (!mounted) return; await SharePlus.instance.share( ShareParams( files: [XFile(file.path)], subject: '$kAppDisplayName Datenexport', ), ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Export vorbereitet.')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Export fehlgeschlagen: $e')), ); } } } Future _exportCsv() async { try { final list = await _repo.listAuftraege(); final file = await DataExportService.exportAuftraegeCsvToTempFile(list); if (!mounted) return; await SharePlus.instance.share( ShareParams( files: [XFile(file.path)], subject: '$kAppDisplayName CSV-Export', ), ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('CSV-Export vorbereitet (Excel DE).')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('CSV-Export fehlgeschlagen: $e')), ); } } } Future _openSupportMail() async { final uri = Uri.parse( 'mailto:$kSupportEmail?subject=${Uri.encodeComponent('$kAppDisplayName Support')}', ); if (await canLaunchUrl(uri)) { await launchUrl(uri); } else if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Mail-App nicht erreichbar ($kSupportEmail)')), ); } } String _greeting() { final h = DateTime.now().hour; if (h < 11) return 'Guten Morgen'; if (h < 18) return 'Guten Tag'; return 'Guten Abend'; } ({String label, Color color}) _statusBadge(Auftrag a) { return ( label: a.status.labelDe, color: a.status.badgeColor, ); } String _zeitZeile(Auftrag a) { final c = a.createdAt ?? DateTime.now(); final n = DateTime.now(); final heute = DateTime(n.year, n.month, n.day); final tag = DateTime(c.year, c.month, c.day); final morgen = heute.add(const Duration(days: 1)); final zeit = DateFormat('HH:mm').format(c); if (tag == heute) return 'Heute, $zeit Uhr'; if (tag == morgen) return 'Morgen, $zeit Uhr'; return DateFormat('dd.MM.yyyy, HH:mm').format(c); } int get _heuteCount { final n = DateTime.now(); return _list.where((a) { final c = a.createdAt; return c != null && c.year == n.year && c.month == n.month && c.day == n.day; }).length; } int get _offenCount => _list.where((a) => a.status == AuftragStatus.offen).length; int get _erledigtCount => _list.where((a) => a.status == AuftragStatus.fertig).length; @override Widget build(BuildContext context) { final name = _name?.trim().isNotEmpty == true ? _name!.trim() : 'Handwerker'; return Scaffold( body: IndexedStack( index: _tab, children: [ _buildStartTab(context, name), _buildAuftraegeTab(context), _buildPdfsTab(context), _buildProfilTab(context, name), ], ), bottomNavigationBar: NavigationBar( height: 72, selectedIndex: _tab, onDestinationSelected: (i) { setState(() => _tab = i); if (i == 2) _loadPdfHistory(); }, backgroundColor: AppTheme.card, indicatorColor: AppTheme.accentCyan.withValues(alpha: 0.22), labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, destinations: [ NavigationDestination( icon: const Icon(Icons.home_outlined), selectedIcon: const Icon(Icons.home, color: AppTheme.accentCyan), label: 'Start', ), NavigationDestination( icon: const Icon(Icons.assignment_outlined), selectedIcon: const Icon(Icons.assignment, color: AppTheme.accentCyan), label: 'Aufträge', ), NavigationDestination( icon: const Icon(Icons.picture_as_pdf_outlined), selectedIcon: const Icon(Icons.picture_as_pdf, color: AppTheme.accentCyan), label: 'PDFs', ), NavigationDestination( icon: const Icon(Icons.person_outline), selectedIcon: const Icon(Icons.person, color: AppTheme.accentCyan), label: 'Profil', ), ], ), floatingActionButton: (_tab == 0 || _tab == 1) ? FloatingActionButton( onPressed: _loading ? null : () => _openEditor(), child: const Icon(Icons.add), ) : null, floatingActionButtonLocation: FloatingActionButtonLocation.startFloat, ); } Widget _purpleAppBar({ required String title, List? actions, }) { return SliverAppBar( pinned: true, backgroundColor: AppTheme.headerPurple, foregroundColor: Colors.white, title: Text( title, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 20), ), actions: actions, ); } Widget _buildStartTab(BuildContext context, String displayName) { return RefreshIndicator( color: AppTheme.accentCyan, onRefresh: _refresh, child: CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ _purpleAppBar( title: 'HandwerkPro', actions: [ IconButton( icon: const Icon(Icons.notifications_none), onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Benachrichtigungen folgen.')), ); }, ), IconButton( icon: const Icon(Icons.account_circle_outlined), onPressed: () => setState(() => _tab = 3), ), ], ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 8), child: Text( '${_greeting()}, $displayName 👷', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Expanded( child: _StatCard( label: 'Offen', value: _loading ? '…' : '$_offenCount', ), ), const SizedBox(width: 10), Expanded( child: _StatCard( label: 'Heute', value: _loading ? '…' : '$_heuteCount', ), ), const SizedBox(width: 10), Expanded( child: _StatCard( label: 'Erledigt', value: _loading ? '…' : '$_erledigtCount', ), ), ], ), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 10, 16, 0), child: Row( children: [ Expanded( child: _StatCard( label: 'Offene Posten', value: _loading ? '…' : '$_offenePostenCount', ), ), const SizedBox(width: 10), Expanded( child: _StatCard( label: 'Überfällig', value: _loading ? '…' : '$_ueberfaelligCount', ), ), const SizedBox(width: 10), Expanded( child: _StatCard( label: 'PDFs (Woche)', value: _metrics == null ? '…' : '${_metrics!.pdfThisCalendarWeek}', ), ), ], ), ), ), if (_metrics != null && (_metrics!.pdfAllTime > 0 || _metrics!.lastPdfAt != null)) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 0), child: Text( _metrics!.lastPdfAt != null ? 'Zuletzt PDF: ${DateFormat('dd.MM.yyyy HH:mm').format(_metrics!.lastPdfAt!)} · ' 'gesamt ${_metrics!.pdfAllTime}' : 'PDFs gesamt: ${_metrics!.pdfAllTime}', style: TextStyle(color: Colors.grey.shade600, fontSize: 12), ), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(20, 24, 8, 8), child: Row( children: [ Text( 'Aufträge', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: Colors.white, ), ), const SizedBox(width: 4), PopupMenuButton( initialValue: _sort, tooltip: 'Sortierung', color: AppTheme.card, onSelected: (v) => setState(() => _sort = v), itemBuilder: (context) => AuftragListenSort.values .map( (s) => PopupMenuItem( value: s, child: Text(s.label), ), ) .toList(), child: Icon( Icons.sort_rounded, color: Colors.grey.shade500, size: 22, ), ), const Spacer(), TextButton( onPressed: () => setState(() => _tab = 1), child: const Text( 'Alle anzeigen →', style: TextStyle(color: AppTheme.accentCyan), ), ), ], ), ), ), if (_loading) const SliverFillRemaining( hasScrollBody: false, child: Center(child: CircularProgressIndicator()), ) else if (_error != null) _retryLoadSliver() else if (_list.isEmpty) SliverFillRemaining( hasScrollBody: false, child: Center( child: Text( 'Noch keine Aufträge.\nTippe auf + für einen neuen Auftrag.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: const Color(0xFF9E9E9E), ), ), ), ) else SliverPadding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), sliver: SliverList.separated( itemCount: _sortedAll.length > 5 ? 5 : _sortedAll.length, separatorBuilder: (context, index) => const SizedBox(height: 10), itemBuilder: (context, i) { final a = _sortedAll[i]; final st = _statusBadge(a); return _AuftragKarte( titel: a.titel.isEmpty ? '(Ohne Titel)' : a.titel, untertitel: _zeitZeile(a), kunde: a.kundenName.isEmpty ? 'Kunde' : a.kundenName, metaZeile: '${a.dokumentTyp.labelDe} · ${a.zahlungsStatus.labelDe}', statusLabel: st.label, statusColor: st.color, onTap: () => _openEditor(auftragId: a.id), ); }, ), ), ], ), ); } Widget _buildAuftraegeTab(BuildContext context) { final filtered = _filteredSorted; return RefreshIndicator( color: AppTheme.accentCyan, onRefresh: _refresh, child: CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ _purpleAppBar(title: 'Aufträge'), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: TextField( controller: _searchCtrl, style: const TextStyle(color: Colors.white), decoration: InputDecoration( hintText: 'Suche Titel, Kunde, Adresse, Nr. …', hintStyle: TextStyle(color: Colors.grey.shade600), prefixIcon: Icon(Icons.search, color: Colors.grey.shade500), suffixIcon: _searchCtrl.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), color: Colors.grey.shade500, onPressed: () { _searchCtrl.clear(); setState(() {}); }, ) : null, filled: true, fillColor: AppTheme.card, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF404040)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide( color: AppTheme.accentCyan, width: 1.5, ), ), ), ), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), child: Row( children: [ Text( 'Sortierung', style: TextStyle( color: Colors.grey.shade500, fontSize: 13, ), ), const SizedBox(width: 12), Expanded( child: DropdownButtonFormField( key: ValueKey(_sort), initialValue: _sort, dropdownColor: AppTheme.card, style: const TextStyle(color: Colors.white), decoration: InputDecoration( filled: true, fillColor: AppTheme.card, contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFF404040)), ), ), items: AuftragListenSort.values .map( (s) => DropdownMenuItem( value: s, child: Text(s.label), ), ) .toList(), onChanged: (v) { if (v != null) setState(() => _sort = v); }, ), ), ], ), ), ), if (_loading) const SliverFillRemaining( hasScrollBody: false, child: Center(child: CircularProgressIndicator()), ) else if (_error != null) _retryLoadSliver() else if (_list.isEmpty) SliverFillRemaining( hasScrollBody: false, child: Center( child: Text( 'Keine Aufträge', style: TextStyle(color: Colors.grey.shade600), ), ), ) else if (filtered.isEmpty) SliverFillRemaining( hasScrollBody: false, child: Center( child: Text( 'Keine Treffer für die Suche.', style: TextStyle(color: Colors.grey.shade600), ), ), ) else SliverPadding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 100), sliver: SliverList.separated( itemCount: filtered.length, separatorBuilder: (context, index) => const SizedBox(height: 10), itemBuilder: (context, i) { final a = filtered[i]; final st = _statusBadge(a); return _AuftragKarte( titel: a.titel.isEmpty ? '(Ohne Titel)' : a.titel, untertitel: _zeitZeile(a), kunde: a.kundenName.isEmpty ? 'Kunde' : a.kundenName, metaZeile: '${a.dokumentTyp.labelDe} · ${a.zahlungsStatus.labelDe}', statusLabel: st.label, statusColor: st.color, onTap: () => _openEditor(auftragId: a.id), ); }, ), ), ], ), ); } Widget _buildPdfsTab(BuildContext context) { return CustomScrollView( slivers: [ _purpleAppBar(title: 'PDFs'), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), child: Text( 'Zuletzt erzeugt oder geteilt – nur Metadaten auf diesem Gerät, ' 'keine PDF-Ablage in der Cloud.', style: TextStyle(color: Colors.grey.shade600, fontSize: 13), ), ), ), if (_pdfHistory.isEmpty) SliverFillRemaining( hasScrollBody: false, child: Padding( padding: const EdgeInsets.all(28), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.picture_as_pdf_outlined, size: 56, color: Colors.grey.shade700), const SizedBox(height: 16), Text( 'Noch kein PDF über die App geteilt oder per E-Mail gesendet.\n\n' 'Im Auftrag: „PDF erstellen & senden“ oder „PDF per E-Mail senden“.', textAlign: TextAlign.center, style: TextStyle( color: Colors.grey.shade500, height: 1.4, ), ), ], ), ), ) else ...[ SliverList( delegate: SliverChildBuilderDelegate( (context, i) { final e = _pdfHistory[i]; return ListTile( leading: Icon(Icons.picture_as_pdf, color: AppTheme.accentCyan), title: Text( e.title, style: const TextStyle(color: Colors.white), ), subtitle: Text( '${e.rechnungsnummer} · ${DateFormat('dd.MM.yyyy HH:mm').format(e.at)}', style: TextStyle(color: Colors.grey.shade500), ), ); }, childCount: _pdfHistory.length, ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), child: TextButton( onPressed: () async { await PdfHistoryService.clear(); await _loadPdfHistory(); }, child: const Text('Verlauf leeren'), ), ), ), ], ], ); } Widget _buildProfilTab(BuildContext context, String displayName) { void openLegal(String title, String body, {bool disclaimer = false}) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => LegalDocumentScreen( title: title, body: body, showDisclaimer: disclaimer, ), ), ); } return CustomScrollView( slivers: [ _purpleAppBar(title: 'Profil'), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ CircleAvatar( radius: 40, backgroundColor: AppTheme.accentCyan.withValues(alpha: 0.2), child: Text( displayName.isNotEmpty ? displayName[0].toUpperCase() : '?', style: const TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: AppTheme.accentCyan, ), ), ), const SizedBox(height: 16), Text( displayName, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), if (_email != null && _email!.isNotEmpty) ...[ const SizedBox(height: 6), Text( _email!, style: TextStyle(color: Colors.grey.shade500), ), ], const SizedBox(height: 8), Text( _appVersion.isEmpty ? 'Version …' : 'Version $_appVersion', style: TextStyle(color: Colors.grey.shade600, fontSize: 12), ), const SizedBox(height: 16), Card( child: Theme( data: Theme.of(context).copyWith( dividerColor: Colors.transparent, ), child: ExpansionTile( tilePadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 4, ), title: const Text('Appwrite-Verbindung'), subtitle: Text( 'Endpoint, Project & Database-ID – bei Fehler 404 hier prüfen', style: TextStyle( color: Colors.grey.shade600, fontSize: 12, ), ), children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: SelectableText( 'Endpoint:\n$kAppwriteEndpoint\n\n' 'Project ID:\n$kAppwriteProjectId\n\n' 'Database ID (in der Console unter Databases ' 'exakt so, oft ≠ Anzeigename):\n' '$kAppwriteDatabaseId\n\n' 'Collection ID:\n$kAppwriteCollectionId\n\n' 'Bucket ID:\n$kAppwriteBucketId\n\n' 'IDs anpassen: lib/appwrite_local.dart ' '(kAppwriteDatabaseIdOverride usw.) oder z. B.\n' 'flutter run -d macos ' '--dart-define=APPWRITE_DATABASE_ID=deine_id\n\n' '── Collection „$kAppwriteCollectionId“ anlegen ──\n\n' '${appwriteRechnungenCollectionCheckliste()}', style: TextStyle( color: Colors.grey.shade400, fontSize: 12, height: 1.4, ), ), ), ], ), ), ), const SizedBox(height: 24), Card( child: Column( children: [ ListTile( leading: const Icon(Icons.article_outlined, color: AppTheme.accentCyan), title: const Text('Impressum'), trailing: const Icon(Icons.chevron_right), onTap: () => openLegal( 'Impressum', LegalContent.impressum, disclaimer: true, ), ), const Divider(height: 1), ListTile( leading: const Icon(Icons.privacy_tip_outlined, color: AppTheme.accentCyan), title: const Text('Datenschutz'), trailing: const Icon(Icons.chevron_right), onTap: () => openLegal( 'Datenschutz', LegalContent.datenschutz, disclaimer: true, ), ), const Divider(height: 1), ListTile( leading: const Icon(Icons.gavel_outlined, color: AppTheme.accentCyan), title: const Text('AGB'), trailing: const Icon(Icons.chevron_right), onTap: () => openLegal( 'AGB', LegalContent.agb, disclaimer: true, ), ), const Divider(height: 1), ListTile( leading: const Icon(Icons.download_outlined, color: AppTheme.accentCyan), title: const Text('Aufträge exportieren (JSON)'), subtitle: Text( 'Datei-Fotos & Unterschrift nur als IDs', style: TextStyle(color: Colors.grey.shade600), ), onTap: _exportData, ), const Divider(height: 1), ListTile( leading: const Icon(Icons.table_chart_outlined, color: AppTheme.accentCyan), title: const Text('CSV-Export (Excel / Steuerbüro)'), subtitle: Text( 'Semikolon, UTF-8 – grobe DATEV-Vorbereitung', style: TextStyle(color: Colors.grey.shade600), ), onTap: _exportCsv, ), const Divider(height: 1), ListTile( leading: const Icon(Icons.mail_outline, color: AppTheme.accentCyan), title: const Text('Support'), subtitle: Text(kSupportEmail), onTap: _openSupportMail, ), ], ), ), const SizedBox(height: 12), Text( LegalContent.disclaimer, textAlign: TextAlign.center, style: TextStyle( color: Colors.grey.shade600, fontSize: 11, height: 1.35, ), ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () async { try { await _account.deleteSession(sessionId: 'current'); } catch (_) {} widget.onLoggedOut?.call(); }, icon: const Icon(Icons.logout), label: const Text('Abmelden'), style: OutlinedButton.styleFrom( foregroundColor: Colors.white, side: const BorderSide(color: Color(0xFF505050)), padding: const EdgeInsets.symmetric(vertical: 14), ), ), ), ], ), ), ), ], ); } } class _StatCard extends StatelessWidget { const _StatCard({required this.label, required this.value}); final String label; final String value; @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8), child: Column( children: [ Text( label, style: TextStyle( fontSize: 12, color: Colors.grey.shade500, ), ), const SizedBox(height: 6), Text( value, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white, ), ), ], ), ), ); } } class _AuftragKarte extends StatelessWidget { const _AuftragKarte({ required this.titel, required this.untertitel, required this.kunde, required this.metaZeile, required this.statusLabel, required this.statusColor, required this.onTap, }); final String titel; final String untertitel; final String kunde; final String metaZeile; final String statusLabel; final Color statusColor; final VoidCallback onTap; @override Widget build(BuildContext context) { return Material( color: AppTheme.card, borderRadius: BorderRadius.circular(16), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Padding( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( titel, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white, ), ), const SizedBox(height: 4), Text( untertitel, style: TextStyle( fontSize: 13, color: Colors.grey.shade500, ), ), const SizedBox(height: 6), Text( kunde, style: TextStyle( fontSize: 13, color: Colors.grey.shade400, ), ), const SizedBox(height: 4), Text( metaZeile, style: TextStyle( fontSize: 11, color: Colors.grey.shade600, ), ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(20), ), child: Text( statusLabel, style: TextStyle( color: statusColor, fontSize: 12, fontWeight: FontWeight.w600, ), ), ), ], ), ), ), ); } }