From e84d463212011923dc70c9f69548287bb20bd6f5 Mon Sep 17 00:00:00 2001 From: Herwig Birke Date: Fri, 3 Oct 2025 08:29:57 +0200 Subject: [PATCH] Cleanup --- lib/api/booking_api.dart | 92 ++++++++- lib/screens/monthly_view.dart | 348 ++++++++++++++++++---------------- 2 files changed, 278 insertions(+), 162 deletions(-) diff --git a/lib/api/booking_api.dart b/lib/api/booking_api.dart index 50217e9..d666803 100644 --- a/lib/api/booking_api.dart +++ b/lib/api/booking_api.dart @@ -57,7 +57,7 @@ class BookingApi { items.add(WorkDay( date: DateTime(date.year, date.month, date.day), intervals: intervals, - targetMinutes: 0, // wird im UI mit dem Tagesplan ersetzt + targetMinutes: 0, // wird im UI mit Tagesplan/Feiertag ersetzt code: code, )); } @@ -83,4 +83,94 @@ class BookingApi { } return MonthStart.fromJson(map); } + + /// Einen Tag speichern: date (YYYY-MM-DD), code, come1..leave5 ("HH:mm" oder null/leer) + Future saveDay(WorkDay day) async { + final y = day.date.year.toString().padLeft(4, '0'); + final m = day.date.month.toString().padLeft(2, '0'); + final d = day.date.day.toString().padLeft(2, '0'); + final dateStr = '$y-$m-$d'; + + // Intervalle in 5 Slots abbilden + final starts = List.filled(5, null); + final ends = List.filled(5, null); + for (int i = 0; i < day.intervals.length && i < 5; i++) { + starts[i] = fmtTimeOfDay(day.intervals[i].start); + ends[i] = fmtTimeOfDay(day.intervals[i].end); + } + + // Codes, die Zeiten serverseitig leeren + final lockCodes = {'G', 'U', 'SU', 'K'}; + final payload = { + 'module': 'booking', + 'function': 'saveDay', + 'date': dateStr, + 'code': day.code, // null → wird als NULL gespeichert + 'come1': lockCodes.contains(day.code) ? null : starts[0], + 'leave1': lockCodes.contains(day.code) ? null : ends[0], + 'come2': lockCodes.contains(day.code) ? null : starts[1], + 'leave2': lockCodes.contains(day.code) ? null : ends[1], + 'come3': lockCodes.contains(day.code) ? null : starts[2], + 'leave3': lockCodes.contains(day.code) ? null : ends[2], + 'come4': lockCodes.contains(day.code) ? null : starts[3], + 'leave4': lockCodes.contains(day.code) ? null : ends[3], + 'come5': lockCodes.contains(day.code) ? null : starts[4], + 'leave5': lockCodes.contains(day.code) ? null : ends[4], + }; + + final uri = Uri.parse('https://api.windesign.at/workinghours.php'); + final res = await client.post( + uri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(payload), + ); + + if (res.statusCode != 200) { + throw Exception('booking/saveDay failed: ${res.statusCode} ${res.body}'); + } + final map = jsonDecode(res.body) as Map; + if (map['error'] == true) { + throw Exception('booking/saveDay error: ${map['errmsg']}'); + } + } + + /// Startwerte für einen Monat speichern (Upsert auf monthlybooking). + /// `monthStart` = 1. des Monats. + Future saveMonthStart( + DateTime monthStart, { + required int starthours, + required int startvacation, + int overtime = 0, + int correction = 0, + }) async { + final y = monthStart.year.toString().padLeft(4, '0'); + final m = monthStart.month.toString().padLeft(2, '0'); + final d = '01'; // normalize + final uri = Uri.parse('https://api.windesign.at/workinghours.php'); + + final payload = { + 'module': 'monthlybooking', + 'function': 'saveStart', + 'date': '$y-$m-$d', + 'starthours': starthours, + 'startvacation': startvacation, + 'overtime': overtime, + 'correction': correction, + }; + + final res = await client.post( + uri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(payload), + ); + + if (res.statusCode != 200) { + throw Exception('monthlybooking/saveStart failed: ${res.statusCode} ${res.body}'); + } + final map = jsonDecode(res.body) as Map; + if (map['error'] == true) { + throw Exception('monthlybooking/saveStart error: ${map['errmsg']}'); + } + } + } diff --git a/lib/screens/monthly_view.dart b/lib/screens/monthly_view.dart index 56702b1..b9d983f 100644 --- a/lib/screens/monthly_view.dart +++ b/lib/screens/monthly_view.dart @@ -1,3 +1,4 @@ +import 'dart:async'; // << neu: für Timer (Debounce) import 'dart:ui' show FontFeature; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; @@ -33,8 +34,7 @@ class _MonthlyViewState extends State { final Set _invalidCells = {}; final Map _focusNodes = {}; final Map _controllers = {}; - FocusNode _nodeFor(String key) => - _focusNodes.putIfAbsent(key, () => FocusNode()); + FocusNode _nodeFor(String key) => _focusNodes.putIfAbsent(key, () => FocusNode()); TextEditingController _controllerFor(String key, String initial) => _controllers.putIfAbsent(key, () => TextEditingController(text: initial)); @@ -68,6 +68,12 @@ class _MonthlyViewState extends State { late final ScrollController _vCtrl; bool _syncingH = false; + // --- NEU: Debounce + Save-Status pro Zeile --- + final Map _saveTimers = {}; // rowIndex -> Timer + final Set _savingRows = {}; // Zeilen, die gerade speichern + final Set _justSavedRows = {}; // Zeilen, die eben gespeichert haben (Häkchen kurz anzeigen) + final Map _rowSaveError = {}; // Zeilenfehler + @override void initState() { super.initState(); @@ -101,6 +107,9 @@ class _MonthlyViewState extends State { for (final c in _controllers.values) { c.dispose(); } + for (final t in _saveTimers.values) { + t.cancel(); + } _hHeaderCtrl.dispose(); _hBodyCtrl.dispose(); _vCtrl.dispose(); @@ -116,38 +125,46 @@ class _MonthlyViewState extends State { }); try { final results = await Future.wait([ - _bookingApi.getBookingList(m), - _dailyApi.getDailyMinutes(), - _bookingApi.getMonthStart(m), + _bookingApi.getBookingList(m), // List + _dailyApi.getDailyMinutes(), // Map + _bookingApi.getMonthStart(m), // MonthStart ]); final apiDays = results[0] as List; - final plan = results[1] as Map; - final mStart = results[2] as MonthStart; + final plan = results[1] as Map; + final mStart = results[2] as MonthStart; - final holidayMap = buildHolidayMapAT(m.year); // << neu + final holidayMap = buildHolidayMapAT(m.year); final filled = fillMonth(m, apiDays); final withTargets = filled.map((d) { - final isHoliday = holidayMap.containsKey(ymd(d.date)); // << neu - final target = - isHoliday ? 0 : (plan[d.date.weekday] ?? d.targetMinutes); + final isHoliday = holidayMap.containsKey(ymd(d.date)); + // Feiertage haben immer Soll 0 + final baseTarget = isHoliday ? 0 : (plan[d.date.weekday] ?? d.targetMinutes); + // U/SU/K ebenfalls Soll 0 + final code = d.code; + final target = (code == 'U' || code == 'SU' || code == 'K') ? 0 : baseTarget; return WorkDay( date: d.date, intervals: d.intervals, targetMinutes: target, - code: d.code, + code: code, ); }).toList(); setState(() { _monthStart = DateTime(m.year, m.month, 1); - _holidays = holidayMap; // << aus holidayMap setzen + _holidays = holidayMap; _days = withTargets; _dailyPlan = plan; _monthStartInfo = mStart; - _carryBaseMinutes = mStart.carryBaseMinutes; + _carryBaseMinutes = mStart.carryBaseMinutes; // starthours + overtime + correction _loading = false; + + // Status-Maps leeren (neuer Monat) + _savingRows.clear(); + _justSavedRows.clear(); + _rowSaveError.clear(); }); _syncControllersWithDays(); @@ -162,22 +179,21 @@ class _MonthlyViewState extends State { double get _tableMinWidth => _wDate + _wHoliday + (10 * _wTime) + _wCode + (4 * _wNumber) + _wDate; - // Ist-Override für U/SU/K/T + // Ist-Override int _workedFor(WorkDay d) { switch (d.code) { case 'U': case 'SU': - case 'K': // neu: Krank => IST = 0 - return 0; + case 'K': + return 0; // Ist = 0 case 'T': - return d.targetMinutes; + return d.targetMinutes; // Training = Soll default: - return d.workedMinutes; + return d.workedMinutes; // regulär per Intervals (inkl. Pausenregel) } } - Color? _rowColorFor(WorkDay d, - {required Color? holidayBg, required Color? weekendBg}) { + Color? _rowColorFor(WorkDay d, {required Color? holidayBg, required Color? weekendBg}) { switch (d.code) { case 'G': return const Color(0xFFBFBFFF); // Gleitzeit @@ -186,13 +202,12 @@ class _MonthlyViewState extends State { case 'SU': return const Color(0xFF7F7FFF); // Sonderurlaub case 'K': - return Colors.yellow; // Krankenstand + return Colors.yellow; // Krankenstand case 'T': - return Colors.red; // Training + return Colors.red; // Training } final isHoliday = _holidays.containsKey(ymd(d.date)); - final isWeekend = d.date.weekday == DateTime.saturday || - d.date.weekday == DateTime.sunday; + final isWeekend = d.date.weekday == DateTime.saturday || d.date.weekday == DateTime.sunday; if (isHoliday) return holidayBg; if (isWeekend) return weekendBg; return null; @@ -208,8 +223,7 @@ class _MonthlyViewState extends State { final weekendBg = Colors.grey.withOpacity(0.30); // Live-„Effective“-Tage (inkl. Eingabetexte + Sperrlogik) - final effectiveDays = - List.generate(_days.length, (i) => _effectiveDay(i, _days[i])); + final effectiveDays = List.generate(_days.length, (i) => _effectiveDay(i, _days[i])); // Tagesdifferenzen & kumuliert (Start mit Monatssaldo aus API) final diffs = []; @@ -228,7 +242,7 @@ class _MonthlyViewState extends State { cumulative.add(sum); } - // Footer-Werte berechnen + // Footer-Werte final monthLabel = monthTitle(_monthStart); final startVacation = _monthStartInfo?.startVacationUnits ?? 0; final usedVacation = _days.where((d) => d.code == 'U').length; @@ -243,8 +257,7 @@ class _MonthlyViewState extends State { }; final correctionMin = _monthStartInfo?.correctionMinutes ?? 0; - final nextCarryMin = - cumulative.isNotEmpty ? cumulative.last : _carryBaseMinutes; + final nextCarryMin = cumulative.isNotEmpty ? cumulative.last : _carryBaseMinutes; final rows = List.generate(_days.length, (i) { final day = effectiveDays[i]; @@ -261,10 +274,8 @@ class _MonthlyViewState extends State { _MonthHeader( month: _monthStart, loading: _loading, - onPrev: () => - _loadMonth(DateTime(_monthStart.year, _monthStart.month - 1, 1)), - onNext: () => - _loadMonth(DateTime(_monthStart.year, _monthStart.month + 1, 1)), + onPrev: () => _loadMonth(DateTime(_monthStart.year, _monthStart.month - 1, 1)), + onNext: () => _loadMonth(DateTime(_monthStart.year, _monthStart.month + 1, 1)), onPickMonth: () async { final picked = await showDatePicker( context: context, @@ -289,10 +300,8 @@ class _MonthlyViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text('Fehler beim Laden:', - style: TextStyle(fontSize: _fontSize)), - SelectableText(_error ?? '', - style: const TextStyle(fontSize: _fontSize)), + Text('Fehler beim Laden:', style: TextStyle(fontSize: _fontSize)), + SelectableText(_error ?? '', style: const TextStyle(fontSize: _fontSize)), ], ), actions: [ @@ -315,8 +324,7 @@ class _MonthlyViewState extends State { data: DataTableThemeData( headingRowHeight: 30, columnSpacing: 10, - headingTextStyle: const TextStyle( - fontWeight: FontWeight.w700, fontSize: _fontSize), + headingTextStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: _fontSize), ), child: const _HeaderOnlyDataTable(), ), @@ -361,7 +369,7 @@ class _MonthlyViewState extends State { ), ), - // Footer + // Footer (zentriert) const Divider(height: 1), _MonthlySummaryFooter( month: _monthStart, @@ -372,7 +380,7 @@ class _MonthlyViewState extends State { istText: minutesToHHMM(istTotal), correctionText: minutesToSignedHHMM(correctionMin), saldoText: minutesToSignedHHMM(istTotal - sollTotal), - paidOvertimeText: '—', // folgt später + paidOvertimeText: '—', uebertragNextText: minutesToSignedHHMM(nextCarryMin), restUrlaubText: '$startVacation', urlaubUebertragText: '$vacationCarry', @@ -389,9 +397,7 @@ class _MonthlyViewState extends State { } List _buildColumns() { - DataColumn c(String label, - {Alignment align = Alignment.center, double? width}) => - DataColumn( + DataColumn c(String label, {Alignment align = Alignment.center, double? width}) => DataColumn( label: SizedBox( width: width, child: Align( @@ -427,16 +433,14 @@ class _MonthlyViewState extends State { ]; } - List _buildEditableCells( - int dayIndex, WorkDay day, int runningDiff) { - final leftLabel = rightDayLabel(day.date); // "Mo 01.09." - final rightLabel = leftDayLabel(day.date); // "01.09. Mo" + List _buildEditableCells(int dayIndex, WorkDay day, int runningDiff) { + final leftLabel = rightDayLabel(day.date); // "Mo 01.09." + final rightLabel = leftDayLabel(day.date); // "01.09. Mo" final hName = _holidays[ymd(day.date)] ?? ''; final bool lockTimes = day.code != null && _lockCodes.contains(day.code); final bool isHoliday = _holidays.containsKey(ymd(day.date)); - final bool isWeekend = day.date.weekday == DateTime.saturday || - day.date.weekday == DateTime.sunday; + final bool isWeekend = day.date.weekday == DateTime.saturday || day.date.weekday == DateTime.sunday; final bool codeDisabled = isHoliday || isWeekend; String slotText(int slot, bool isStart) { @@ -461,16 +465,28 @@ class _MonthlyViewState extends State { child: Text(leftLabel, style: const TextStyle(fontSize: _fontSize)), ), ))); - // 1: Feiertag + // 1: Feiertag + Save-Status-Icon (rechts) cells.add(DataCell(SizedBox( width: _wHoliday, child: Align( alignment: Alignment.centerLeft, - child: Text(hName, style: const TextStyle(fontSize: _fontSize)), + child: Row( + children: [ + Expanded( + child: Text( + hName, + style: const TextStyle(fontSize: _fontSize), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + _rowStatusBadge(dayIndex), // << neu: Spinner/Häkchen/Fault + ], + ), ), ))); - // 2..11: Zeiten + // 2..11: Zeiten (zentriert) for (int slot = 0; slot < 5; slot++) { // Start final keyS = 't_${dayIndex}_${slot}_s'; @@ -501,7 +517,7 @@ class _MonthlyViewState extends State { if (valid && (text.isEmpty || text.length == 5)) { _commitTime(dayIndex, slot, true, text); } else { - setState(() {}); + setState(() {}); // Repaint (Fehlermarkierung) } }, onSubmitted: (text) { @@ -557,7 +573,7 @@ class _MonthlyViewState extends State { ))); } - // 12: Code (Dropdown, am Wochenende/Feiertag gesperrt) + // 12: Code (Dropdown zentriert, am Wochenende/Feiertag gesperrt) cells.add(DataCell(SizedBox( width: _wCode, child: Align( @@ -587,8 +603,7 @@ class _MonthlyViewState extends State { ))); cells.add(DataCell(SizedBox( width: _wNumber, - child: - Align(alignment: Alignment.centerRight, child: _monoSmall(diffSum)), + child: Align(alignment: Alignment.centerRight, child: _monoSmall(diffSum)), ))); // 17: Datum (linksbündig) @@ -600,8 +615,7 @@ class _MonthlyViewState extends State { ), ))); - assert( - cells.length == 18, 'Row has ${cells.length} cells but expected 18.'); + assert(cells.length == 18, 'Row has ${cells.length} cells but expected 18.'); return cells; } @@ -614,42 +628,55 @@ class _MonthlyViewState extends State { ), ); + // --- NEU: kleiner Status-Badge pro Zeile --- + Widget _rowStatusBadge(int row) { + if (_savingRows.contains(row)) { + return const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ); + } + if (_rowSaveError.containsKey(row)) { + return Icon(Icons.error_outline, size: 14, color: Theme.of(context).colorScheme.error); + } + if (_justSavedRows.contains(row)) { + return const Icon(Icons.check_circle, size: 14, color: Colors.green); + } + return const SizedBox(width: 14, height: 14); + } + Widget _codeDropdown(int dayIndex, WorkDay day, {required bool disabled}) { final value = day.code; // null => — - final values = [null, ...kAbsenceCodes]; + final values = [null, ...kAbsenceCodes]; // ['G','K','U','SU','T'] return DropdownButton( isExpanded: true, value: value, items: values.map((v) { - final label = v == null ? '—' : codeLabel(v); + final label = v == null ? '—' : codeLabel(v); // langer Name im Dropdown return DropdownMenuItem( value: v, - child: Text(label, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: _fontSize)), + child: Text(label, textAlign: TextAlign.center, style: const TextStyle(fontSize: _fontSize)), ); }).toList(), selectedItemBuilder: (context) { return values.map((v) { - final shortText = v ?? '—'; - return Center( - child: - Text(shortText, style: const TextStyle(fontSize: _fontSize))); + final shortText = v ?? '—'; // kurzer Name in der Tabelle + return Center(child: Text(shortText, style: const TextStyle(fontSize: _fontSize))); }).toList(); }, onChanged: disabled ? null : (newCode) { final d = _days[dayIndex]; - final bool willLock = - newCode != null && _lockCodes.contains(newCode); + final bool willLock = newCode != null && _lockCodes.contains(newCode); setState(() { final newDays = List.from(_days); newDays[dayIndex] = WorkDay( date: d.date, intervals: willLock ? [] : d.intervals, - targetMinutes: d.targetMinutes, + targetMinutes: _dailyPlan[d.date.weekday] ?? d.targetMinutes, code: newCode, ); _days = newDays; @@ -665,6 +692,8 @@ class _MonthlyViewState extends State { } } }); + + _scheduleSave(dayIndex); // << neu: debounce statt sofort speichern }, ); } @@ -717,6 +746,63 @@ class _MonthlyViewState extends State { ); _days = newDays; }); + + _scheduleSave(dayIndex); // << neu: debounce + } + + // --- NEU: Debounce + Save-Status-Handling --- + void _scheduleSave(int dayIndex) { + // alten Timer stoppen + _saveTimers[dayIndex]?.cancel(); + // neuen Timer setzen + _saveTimers[dayIndex] = Timer(const Duration(milliseconds: 500), () async { + await _saveDay(dayIndex); + }); + } + + Future _saveDay(int dayIndex) async { + // evtl. laufenden Debounce-Timer löschen (wir speichern jetzt) + _saveTimers[dayIndex]?.cancel(); + _saveTimers.remove(dayIndex); + + setState(() { + _savingRows.add(dayIndex); + _rowSaveError.remove(dayIndex); + _justSavedRows.remove(dayIndex); + }); + + try { + final effective = _effectiveDay(dayIndex, _days[dayIndex]); + await _bookingApi.saveDay(effective); + + if (!mounted) return; + setState(() { + _savingRows.remove(dayIndex); + _justSavedRows.add(dayIndex); + }); + + // Häkchen nach kurzer Zeit wieder ausblenden + Timer(const Duration(milliseconds: 1200), () { + if (!mounted) return; + setState(() { + _justSavedRows.remove(dayIndex); + }); + }); + } catch (e) { + if (!mounted) return; + setState(() { + _savingRows.remove(dayIndex); + _rowSaveError[dayIndex] = e.toString(); + }); + // optional non-intrusive Snack + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Speichern fehlgeschlagen: $e'), + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 2), + ), + ); + } } void _syncControllersWithDays() { @@ -737,12 +823,8 @@ class _MonthlyViewState extends State { : ''; final cs = _controllers[sKey]; final ce = _controllers[eKey]; - if (cs != null && - cs.text != sText && - !(_focusNodes[sKey]?.hasFocus ?? false)) cs.text = sText; - if (ce != null && - ce.text != eText && - !(_focusNodes[eKey]?.hasFocus ?? false)) ce.text = eText; + if (cs != null && cs.text != sText && !(_focusNodes[sKey]?.hasFocus ?? false)) cs.text = sText; + if (ce != null && ce.text != eText && !(_focusNodes[eKey]?.hasFocus ?? false)) ce.text = eText; } } } @@ -753,12 +835,10 @@ class _MonthlyViewState extends State { // Basisziel (Tagesplan) final baseTarget = _dailyPlan[base.date.weekday] ?? base.targetMinutes; - // Zielzeit bestimmen: Feiertag -> 0, sonst bei U/SU -> 0, sonst Tagesplan + // Zielzeit bestimmen int targetFor(WorkDay b) { if (isHoliday) return 0; - if (b.code == 'U' || b.code == 'SU' || b.code == 'K') { - return 0; // <- neu: Soll = 0 bei U/SU - } + if (b.code == 'U' || b.code == 'SU' || b.code == 'K') return 0; return baseTarget; } @@ -786,12 +866,8 @@ class _MonthlyViewState extends State { final sText = _controllers[sKey]?.text; final eText = _controllers[eKey]?.text; - final s = (sText != null && sText.isNotEmpty) - ? parseTextHHMM(sText) - : baseSlot(base, slot, true); - final e = (eText != null && eText.isNotEmpty) - ? parseTextHHMM(eText) - : baseSlot(base, slot, false); + final s = (sText != null && sText.isNotEmpty) ? parseTextHHMM(sText) : baseSlot(base, slot, true); + final e = (eText != null && eText.isNotEmpty) ? parseTextHHMM(eText) : baseSlot(base, slot, false); if (s != null && e != null) intervals.add(WorkInterval(s, e)); } @@ -799,7 +875,7 @@ class _MonthlyViewState extends State { return WorkDay( date: base.date, intervals: intervals, - targetMinutes: targetFor(base), // <- neu angewendet + targetMinutes: targetFor(base), code: base.code, ); } @@ -893,26 +969,16 @@ class _MonthHeader extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Row(children: [ - IconButton( - onPressed: loading ? null : onPrev, - icon: const Icon(Icons.chevron_left)), + IconButton(onPressed: loading ? null : onPrev, icon: const Icon(Icons.chevron_left)), Expanded( child: TextButton.icon( onPressed: loading ? null : onPickMonth, icon: const Icon(Icons.calendar_month), - label: Text(title, - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith(fontSize: 16)), + label: Text(title, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontSize: 16)), ), ), - IconButton( - onPressed: loading ? null : onReload, - icon: const Icon(Icons.refresh)), - IconButton( - onPressed: loading ? null : onNext, - icon: const Icon(Icons.chevron_right)), + IconButton(onPressed: loading ? null : onReload, icon: const Icon(Icons.refresh)), + IconButton(onPressed: loading ? null : onNext, icon: const Icon(Icons.chevron_right)), ]), ); } @@ -925,14 +991,14 @@ class _MonthlySummaryFooter extends StatelessWidget { // Linke Spalte (Strings bereits formatiert) final String uebertragStartText; // Startsaldo (Minuten → HH:MM) - final String sollText; // Summe Soll (HH:MM) - final String istText; // Summe Ist (HH:MM) - final String correctionText; // Correction (HH:MM, ±) - final String saldoText; // IST - SOLL (±HH:MM) - final String paidOvertimeText; // folgt später (—) - final String uebertragNextText; // letzter „Differenz gesamt“ (±HH:MM) - final String restUrlaubText; // startvacation (Zahl) - final String urlaubUebertragText; // startvacation - used + final String sollText; // Summe Soll (HH:MM) + final String istText; // Summe Ist (HH:MM) + final String correctionText; // Correction (HH:MM, ±) + final String saldoText; // IST - SOLL (±HH:MM) + final String paidOvertimeText; // folgt später (—) + final String uebertragNextText; // letzter „Differenz gesamt“ (±HH:MM) + final String restUrlaubText; // startvacation (Zahl) + final String urlaubUebertragText;// startvacation - used // Rechte Spalte (Counts) final int countGleitzeit; @@ -970,17 +1036,16 @@ class _MonthlySummaryFooter extends StatelessWidget { // Monatslabels: Vormonat für Überträge/Resturlaub, aktueller Monat für Soll/Ist final prev = DateTime(month.year, month.month - 1, 1); final prevLabel = monthTitle(prev); - final curLabel = monthLabel; + final curLabel = monthLabel; // Layout-Konstanten - const double leftValueWidth = 120; // Werte-Breite links (rechtsbündig) - const double rightValueWidth = 80; // Werte-Breite rechts (rechtsbündig) - const double colGap = 24; // Abstand zwischen Spalten - const double rowGap = 2; // Zeilenabstand - const double footerMaxWidth = 720; // maximale Footer-Breite + const double leftValueWidth = 120; // Werte-Breite links (rechtsbündig) + const double rightValueWidth = 80; // Werte-Breite rechts (rechtsbündig) + const double colGap = 24; // Abstand zwischen Spalten + const double rowGap = 2; // Zeilenabstand + const double footerMaxWidth = 720; // maximale Footer-Breite - final labelStyle = TextStyle( - fontSize: fontSize, color: Theme.of(context).colorScheme.onSurface); + final labelStyle = TextStyle(fontSize: fontSize, color: Theme.of(context).colorScheme.onSurface); final valueStyle = TextStyle( fontSize: fontSize, fontFeatures: const [FontFeature.tabularFigures()], @@ -1032,8 +1097,7 @@ class _MonthlySummaryFooter extends StatelessWidget { width: leftValueWidth, child: Align( alignment: Alignment.centerRight, - child: - Text(value, style: valueStyle, textAlign: TextAlign.right), + child: Text(value, style: valueStyle, textAlign: TextAlign.right), ), ), ], @@ -1052,8 +1116,7 @@ class _MonthlySummaryFooter extends StatelessWidget { width: rightValueWidth, child: Align( alignment: Alignment.centerRight, - child: - Text(value, style: valueStyle, textAlign: TextAlign.right), + child: Text(value, style: valueStyle, textAlign: TextAlign.right), ), ), const SizedBox(width: 12), @@ -1095,8 +1158,7 @@ class _MonthlySummaryFooter extends StatelessWidget { Expanded( child: Column( mainAxisSize: MainAxisSize.min, - children: - rightItems.map((e) => rightRow(e.$1, e.$2)).toList(), + children: rightItems.map((e) => rightRow(e.$1, e.$2)).toList(), ), ), ], @@ -1106,39 +1168,3 @@ class _MonthlySummaryFooter extends StatelessWidget { ); } } - -class _Metric { - final String label; - final String value; - const _Metric(this.label, this.value); -} - -class _MetricList extends StatelessWidget { - final List<_Metric> items; - final TextStyle labelStyle; - final TextStyle valueStyle; - const _MetricList({ - required this.items, - required this.labelStyle, - required this.valueStyle, - super.key, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: items - .map((m) => Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - children: [ - Expanded(child: Text(m.label, style: labelStyle)), - const SizedBox(width: 12), - Text(m.value, style: valueStyle), - ], - ), - )) - .toList(), - ); - } -}