diff --git a/lib/screens/monthly_view.dart b/lib/screens/monthly_view.dart index ed28c98..7b28ab0 100644 --- a/lib/screens/monthly_view.dart +++ b/lib/screens/monthly_view.dart @@ -17,12 +17,13 @@ class MonthlyView extends StatefulWidget { } class _MonthlyViewState extends State { - // Spaltenbreiten (Header & Body identisch -> exakte Ausrichtung) - static const double _wDate = 120; - static const double _wHoliday = 160; - static const double _wTime = 84; - static const double _wCode = 110; - static const double _wNumber = 92; + // Kompaktere Layout-Parameter + static const double _wDate = 96; + static const double _wHoliday = 120; + static const double _wTime = 60; + static const double _wCode = 70; + static const double _wNumber = 76; + static const double _fontSize = 12; // Edit/Fokus/Controller je Feld final Set _invalidCells = {}; @@ -127,24 +128,28 @@ class _MonthlyViewState extends State { double get _tableMinWidth => _wDate + _wHoliday + (10 * _wTime) + _wCode + (4 * _wNumber) + _wDate; - // Override für Ist-Minuten nach Code + // Ist-Override für U/SU/K/T int _workedFor(WorkDay d) { final c = d.code; if (c == 'U' || c == 'SU' || c == 'K' || c == 'T') { - return d.targetMinutes; // Urlaub/Sonderurlaub/Krankenstand/Training -> Ist = Soll + return d.targetMinutes; } - return d.workedMinutes; // normal aus Intervallen + return d.workedMinutes; } Color? _rowColorFor(WorkDay d, {required Color? holidayBg, required Color? weekendBg}) { switch (d.code) { - case 'GZ': return const Color(0xFFBFBFFF); - case 'U': return const Color(0xFF7F7FFF); - case 'SU': return const Color(0xFF7F7FFF); - case 'K': return Colors.yellow; - case 'T': return Colors.red; + case 'GZ': + return const Color(0xFFBFBFFF); // 0xBFBFFF mit vollem Alpha + case 'U': + return const Color(0xFF7F7FFF); // 0x7F7FFF mit vollem Alpha + case 'SU': + return const Color(0xFF7F7FFF); + case 'K': + return Colors.yellow; + case 'T': + return Colors.red; } - // kein Code -> ggf. Feiertag/Wochenende final isHoliday = _holidays.containsKey(ymd(d.date)); final isWeekend = d.date.weekday == DateTime.saturday || d.date.weekday == DateTime.sunday; if (isHoliday) return holidayBg; @@ -155,16 +160,15 @@ class _MonthlyViewState extends State { @override Widget build(BuildContext context) { final headerColumns = _buildColumns(); - final bodyColumns = _buildColumns(); // gleiche Struktur (Body blendet Header aus) + final bodyColumns = _buildColumns(); final theme = Theme.of(context); final holidayBg = theme.colorScheme.secondaryContainer.withOpacity(0.45); final weekendBg = Colors.grey.withOpacity(0.30); - // Effektive Tage (inkl. Live-Edits) final effectiveDays = List.generate(_days.length, (i) => _effectiveDay(i, _days[i])); - // kumulative Differenz basierend auf Override- oder Intervall-Arbeitszeit + // diffs & kumuliert (mit Override) final diffs = []; for (final d in effectiveDays) { final worked = _workedFor(d); @@ -218,20 +222,20 @@ class _MonthlyViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - const Text('Fehler beim Laden:'), - SelectableText(_error ?? ''), + Text('Fehler beim Laden:', style: TextStyle(fontSize: _fontSize)), + SelectableText(_error ?? '', style: const TextStyle(fontSize: _fontSize)), ], ), actions: [ TextButton( onPressed: () => _loadMonth(_monthStart), - child: const Text('Erneut versuchen'), + child: Text('Erneut versuchen', style: const TextStyle(fontSize: _fontSize)), ), ], ), ), - // FIXIERTE KOPFZEILE (nur horizontal scrollend) + // FIXIERTE KOPFZEILE Scrollbar( controller: _hCtrl, notificationPredicate: (n) => n.metrics.axis == Axis.horizontal, @@ -242,22 +246,24 @@ class _MonthlyViewState extends State { child: ConstrainedBox( constraints: BoxConstraints(minWidth: _tableMinWidth), child: DataTableTheme( - data: const DataTableThemeData( - headingRowHeight: 42, - columnSpacing: 20, - headingTextStyle: TextStyle(fontWeight: FontWeight.w700), + data: DataTableThemeData( + headingRowHeight: 30, + columnSpacing: 10, + headingTextStyle: const TextStyle(fontWeight: FontWeight.w700).copyWith( + fontSize: _fontSize, + ), ), child: DataTable( showCheckboxColumn: false, columns: headerColumns, - rows: const [], // nur Header anzeigen + rows: const [], ), ), ), ), ), - // BODY (horiz. & vert. Scroll, aber Header im Body ausgeblendet) + // BODY Expanded( child: Scrollbar( controller: _hCtrl, @@ -265,7 +271,7 @@ class _MonthlyViewState extends State { thumbVisibility: true, child: SingleChildScrollView( controller: _hCtrl, - padding: const EdgeInsets.only(bottom: 24), + padding: const EdgeInsets.only(bottom: 16), scrollDirection: Axis.horizontal, child: ConstrainedBox( constraints: BoxConstraints(minWidth: _tableMinWidth), @@ -277,10 +283,10 @@ class _MonthlyViewState extends State { controller: _vCtrl, child: DataTableTheme( data: const DataTableThemeData( - headingRowHeight: 0, // Header hier ausblenden - dataRowMinHeight: 44, - dataRowMaxHeight: 54, - columnSpacing: 20, + headingRowHeight: 0, + dataRowMinHeight: 30, + dataRowMaxHeight: 34, + columnSpacing: 10, ), child: DataTable( showCheckboxColumn: false, @@ -303,19 +309,18 @@ class _MonthlyViewState extends State { width: width, child: Align( alignment: align, - child: Text(label, textAlign: _toTextAlign(align)), + child: Text( + label, + textAlign: _toTextAlign(align), + style: const TextStyle(fontSize: _fontSize), + ), ), ), ); return [ - // 0 Datum (rechtsbündig) c('Datum', align: Alignment.centerRight, width: _wDate), - - // 1 Feiertag (links) c('Feiertag', align: Alignment.centerLeft, width: _wHoliday), - - // 2..11 Zeitspalten (zentriert) c('Start 1', align: Alignment.center, width: _wTime), c('Ende 1', align: Alignment.center, width: _wTime), c('Start 2', align: Alignment.center, width: _wTime), @@ -326,24 +331,19 @@ class _MonthlyViewState extends State { c('Ende 4', align: Alignment.center, width: _wTime), c('Start 5', align: Alignment.center, width: _wTime), c('Ende 5', align: Alignment.center, width: _wTime), - - // 12 Code (zentriert) c('Code', align: Alignment.center, width: _wCode), - - // 13..16 Kennzahlen (rechtsbündig) c('Soll', align: Alignment.centerRight, width: _wNumber), c('Ist', align: Alignment.centerRight, width: _wNumber), c('Differenz', align: Alignment.centerRight, width: _wNumber), c('Differenz gesamt', align: Alignment.centerRight, width: _wNumber), - - // 17 Datum (linksbündig) c('Datum', align: Alignment.centerLeft, width: _wDate), ]; } List _buildEditableCells(int dayIndex, WorkDay day, int runningDiff) { - final leftLabel = leftDayLabel(day.date); - final rightLabel = rightDayLabel(day.date); + // Gewünscht: links Wochentag zuerst, rechts Datum zuerst. + final leftLabel = rightDayLabel(day.date); // Wochentag Datum + final rightLabel = leftDayLabel(day.date); // Datum Wochentag final hName = _holidays[ymd(day.date)] ?? ''; String slotText(int slot, bool isStart) { @@ -362,15 +362,21 @@ class _MonthlyViewState extends State { // 0: Datum (rechts) cells.add(DataCell(SizedBox( width: _wDate, - child: Align(alignment: Alignment.centerRight, child: Text(leftLabel)), + child: Align( + alignment: Alignment.centerRight, + child: Text(leftLabel, style: const TextStyle(fontSize: _fontSize)), + ), ))); - // 1: Feiertag-Name + // 1: Feiertag cells.add(DataCell(SizedBox( width: _wHoliday, - child: Align(alignment: Alignment.centerLeft, child: Text(hName)), + child: Align( + alignment: Alignment.centerLeft, + child: Text(hName, style: const TextStyle(fontSize: _fontSize)), + ), ))); - // 2..11: editierbare Zeiten (zentriert) + // 2..11: Zeiten for (int slot = 0; slot < 5; slot++) { // Start final keyS = 't_${dayIndex}_${slot}_s'; @@ -398,7 +404,7 @@ class _MonthlyViewState extends State { if (valid && (text.isEmpty || text.length == 5)) { _commitTime(dayIndex, slot, true, text); } else { - setState(() {}); // live neu berechnen + setState(() {}); } }, onSubmitted: (text) { @@ -436,7 +442,7 @@ class _MonthlyViewState extends State { if (valid && (text.isEmpty || text.length == 5)) { _commitTime(dayIndex, slot, false, text); } else { - setState(() {}); // live neu berechnen + setState(() {}); } }, onSubmitted: (text) { @@ -449,13 +455,13 @@ class _MonthlyViewState extends State { ))); } - // 12: Code (Dropdown) — zentriert (Kurzname geschlossen, Langname in Liste) + // 12: Code cells.add(DataCell(SizedBox( width: _wCode, child: Align(alignment: Alignment.center, child: _codeDropdown(dayIndex, day)), ))); - // 13..16: Kennzahlen (rechts) – mit Override + // 13..16: Kennzahlen (mit Override) final worked = _workedFor(day); final soll = minutesToHHMM(day.targetMinutes); final ist = minutesToHHMM(worked); @@ -464,25 +470,28 @@ class _MonthlyViewState extends State { cells.add(DataCell(SizedBox( width: _wNumber, - child: Align(alignment: Alignment.centerRight, child: mono(soll)), + child: Align(alignment: Alignment.centerRight, child: _monoSmall(soll)), ))); cells.add(DataCell(SizedBox( width: _wNumber, - child: Align(alignment: Alignment.centerRight, child: mono(ist)), + child: Align(alignment: Alignment.centerRight, child: _monoSmall(ist)), ))); cells.add(DataCell(SizedBox( width: _wNumber, - child: Align(alignment: Alignment.centerRight, child: mono(diff)), + child: Align(alignment: Alignment.centerRight, child: _monoSmall(diff)), ))); cells.add(DataCell(SizedBox( width: _wNumber, - child: Align(alignment: Alignment.centerRight, child: mono(diffSum)), + child: Align(alignment: Alignment.centerRight, child: _monoSmall(diffSum)), ))); // 17: Datum (links) cells.add(DataCell(SizedBox( width: _wDate, - child: Align(alignment: Alignment.centerLeft, child: Text(rightLabel)), + child: Align( + alignment: Alignment.centerLeft, + child: Text(rightLabel, style: const TextStyle(fontSize: _fontSize)), + ), ))); assert(cells.length == 18, 'Row has ${cells.length} cells but expected 18.'); @@ -490,25 +499,23 @@ class _MonthlyViewState extends State { } Widget _codeDropdown(int dayIndex, WorkDay day) { - final value = day.code; // null => kein Code (—) + final value = day.code; final values = [null, ...kAbsenceCodes]; return DropdownButton( isExpanded: true, value: value, - // Langnamen in der aufgeklappten Liste: items: values.map((v) { final label = v == null ? '—' : codeLabel(v); return DropdownMenuItem( value: v, - child: Text(label, textAlign: TextAlign.center), + child: Text(label, textAlign: TextAlign.center, style: const TextStyle(fontSize: _fontSize)), ); }).toList(), - // Kurznamen (zentriert) in der geschlossenen Anzeige: selectedItemBuilder: (context) { return values.map((v) { final shortText = v ?? '—'; - return Center(child: Text(shortText)); + return Center(child: Text(shortText, style: const TextStyle(fontSize: _fontSize))); }).toList(); }, onChanged: (newCode) { @@ -624,7 +631,7 @@ class _MonthlyViewState extends State { ); } - // rahmenlose Eingabefelder, nur dezente Füllung bei invalid + // kompakte, rahmenlose Eingabefelder Widget _timeField({ required Key key, required TextEditingController controller, @@ -643,12 +650,15 @@ class _MonthlyViewState extends State { controller: controller, focusNode: focusNode, textAlign: TextAlign.center, - style: const TextStyle(fontFeatures: [FontFeature.tabularFigures()]), + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + fontSize: _fontSize, + ), keyboardType: TextInputType.datetime, inputFormatters: const [HHmmInputFormatter()], decoration: const InputDecoration( isDense: true, - contentPadding: EdgeInsets.symmetric(horizontal: 6, vertical: 6), + contentPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 2), border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, @@ -662,6 +672,14 @@ 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; @@ -696,7 +714,7 @@ class _MonthHeader extends StatelessWidget { child: TextButton.icon( onPressed: loading ? null : onPickMonth, icon: const Icon(Icons.calendar_month), - label: Text(title, style: Theme.of(context).textTheme.titleLarge), + label: Text(title, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontSize: 16)), style: TextButton.styleFrom( foregroundColor: Theme.of(context).colorScheme.onSurface, ),