diff --git a/lib/screens/monthly_view.dart b/lib/screens/monthly_view.dart index 7b28ab0..2eafb82 100644 --- a/lib/screens/monthly_view.dart +++ b/lib/screens/monthly_view.dart @@ -52,16 +52,32 @@ class _MonthlyViewState extends State { DateTime.sunday: 0, }; - late final ScrollController _hCtrl; - late final ScrollController _vCtrl; + // Scroll-Controller + late final ScrollController _hHeaderCtrl; // Header (nur mitgeführt) + late final ScrollController _hBodyCtrl; // Body (führend) + late final ScrollController _vCtrl; // Body vertikal + bool _syncingH = false; @override void initState() { super.initState(); final now = DateTime.now(); _monthStart = DateTime(now.year, now.month, 1); - _hCtrl = ScrollController(); + + _hHeaderCtrl = ScrollController(); + _hBodyCtrl = ScrollController(); _vCtrl = ScrollController(); + + // Body scrollt -> Header folgt + _hBodyCtrl.addListener(() { + if (_syncingH) return; + _syncingH = true; + if (_hHeaderCtrl.hasClients) { + _hHeaderCtrl.jumpTo(_hBodyCtrl.offset); + } + _syncingH = false; + }); + _client = http.Client(); _bookingApi = BookingApi(client: _client); _dailyApi = DailyWorkingApi(client: _client); @@ -76,7 +92,8 @@ class _MonthlyViewState extends State { for (final c in _controllers.values) { c.dispose(); } - _hCtrl.dispose(); + _hHeaderCtrl.dispose(); + _hBodyCtrl.dispose(); _vCtrl.dispose(); _client.close(); super.dispose(); @@ -235,42 +252,34 @@ class _MonthlyViewState extends State { ), ), - // FIXIERTE KOPFZEILE - Scrollbar( - controller: _hCtrl, - notificationPredicate: (n) => n.metrics.axis == Axis.horizontal, - thumbVisibility: true, - child: SingleChildScrollView( - controller: _hCtrl, - scrollDirection: Axis.horizontal, - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: _tableMinWidth), - child: DataTableTheme( - data: DataTableThemeData( - headingRowHeight: 30, - columnSpacing: 10, - headingTextStyle: const TextStyle(fontWeight: FontWeight.w700).copyWith( - fontSize: _fontSize, - ), - ), - child: DataTable( - showCheckboxColumn: false, - columns: headerColumns, - rows: const [], + // FIXIERTE KOPFZEILE (mitgeführt, nicht interaktiv) + SingleChildScrollView( + controller: _hHeaderCtrl, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: _tableMinWidth), + child: DataTableTheme( + data: DataTableThemeData( + headingRowHeight: 30, + columnSpacing: 10, + headingTextStyle: const TextStyle(fontWeight: FontWeight.w700).copyWith( + fontSize: _fontSize, ), ), + child: const _HeaderOnlyDataTable(), ), ), ), - // BODY + // BODY (horiz. & vert. Scroll; horizontale Scrollbar hier) Expanded( child: Scrollbar( - controller: _hCtrl, + controller: _hBodyCtrl, notificationPredicate: (n) => n.metrics.axis == Axis.horizontal, thumbVisibility: true, child: SingleChildScrollView( - controller: _hCtrl, + controller: _hBodyCtrl, padding: const EdgeInsets.only(bottom: 16), scrollDirection: Axis.horizontal, child: ConstrainedBox( @@ -283,7 +292,7 @@ class _MonthlyViewState extends State { controller: _vCtrl, child: DataTableTheme( data: const DataTableThemeData( - headingRowHeight: 0, + headingRowHeight: 0, // Header im Body ausblenden dataRowMinHeight: 30, dataRowMaxHeight: 34, columnSpacing: 10, @@ -498,6 +507,15 @@ class _MonthlyViewState extends State { return cells; } + // kleines Mono-Text-Widget für Zahlen + Widget _monoSmall(String s) => Text( + s, + style: const TextStyle( + fontSize: _fontSize, + fontFeatures: [FontFeature.tabularFigures()], + ), + ); + Widget _codeDropdown(int dayIndex, WorkDay day) { final value = day.code; final values = [null, ...kAbsenceCodes]; @@ -672,14 +690,6 @@ class _MonthlyViewState extends State { ); } -Widget _monoSmall(String s) => Text( - s, - style: const TextStyle( - fontSize: _fontSize, - fontFeatures: [FontFeature.tabularFigures()], - ), -); - TextAlign _toTextAlign(Alignment a) { if (a == Alignment.centerRight) return TextAlign.right; if (a == Alignment.centerLeft) return TextAlign.left; @@ -687,6 +697,23 @@ Widget _monoSmall(String s) => Text( } } +/// Minimaler Header-Table, damit die Kopfzeile fixiert bleiben kann. +/// Nutzt die gleichen Spalten wie der Body, aber ohne Rows. +class _HeaderOnlyDataTable extends StatelessWidget { + const _HeaderOnlyDataTable(); + + @override + Widget build(BuildContext context) { + final state = context.findAncestorStateOfType<_MonthlyViewState>()!; + final cols = state._buildColumns(); + return DataTable( + showCheckboxColumn: false, + columns: cols, + rows: const [], + ); + } +} + class _MonthHeader extends StatelessWidget { final DateTime month; final VoidCallback onPrev;