This commit is contained in:
2025-10-03 08:29:57 +02:00
parent 1e90222d36
commit e84d463212
2 changed files with 278 additions and 162 deletions
+91 -1
View File
@@ -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<void> 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<String?>.filled(5, null);
final ends = List<String?>.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 = <String, dynamic>{
'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<String, dynamic>;
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<void> 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<String, dynamic>;
if (map['error'] == true) {
throw Exception('monthlybooking/saveStart error: ${map['errmsg']}');
}
}
}
+187 -161
View File
@@ -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<MonthlyView> {
final Set<String> _invalidCells = <String>{};
final Map<String, FocusNode> _focusNodes = {};
final Map<String, TextEditingController> _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<MonthlyView> {
late final ScrollController _vCtrl;
bool _syncingH = false;
// --- NEU: Debounce + Save-Status pro Zeile ---
final Map<int, Timer> _saveTimers = {}; // rowIndex -> Timer
final Set<int> _savingRows = {}; // Zeilen, die gerade speichern
final Set<int> _justSavedRows = {}; // Zeilen, die eben gespeichert haben (Häkchen kurz anzeigen)
final Map<int, String> _rowSaveError = {}; // Zeilenfehler
@override
void initState() {
super.initState();
@@ -101,6 +107,9 @@ class _MonthlyViewState extends State<MonthlyView> {
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<MonthlyView> {
});
try {
final results = await Future.wait([
_bookingApi.getBookingList(m),
_dailyApi.getDailyMinutes(),
_bookingApi.getMonthStart(m),
_bookingApi.getBookingList(m), // List<WorkDay>
_dailyApi.getDailyMinutes(), // Map<int,int>
_bookingApi.getMonthStart(m), // MonthStart
]);
final apiDays = results[0] as List<WorkDay>;
final plan = results[1] as Map<int, int>;
final mStart = results[2] as MonthStart;
final plan = results[1] as Map<int, int>;
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<MonthlyView> {
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<MonthlyView> {
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<MonthlyView> {
final weekendBg = Colors.grey.withOpacity(0.30);
// Live-„Effective“-Tage (inkl. Eingabetexte + Sperrlogik)
final effectiveDays =
List<WorkDay>.generate(_days.length, (i) => _effectiveDay(i, _days[i]));
final effectiveDays = List<WorkDay>.generate(_days.length, (i) => _effectiveDay(i, _days[i]));
// Tagesdifferenzen & kumuliert (Start mit Monatssaldo aus API)
final diffs = <int>[];
@@ -228,7 +242,7 @@ class _MonthlyViewState extends State<MonthlyView> {
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<MonthlyView> {
};
final correctionMin = _monthStartInfo?.correctionMinutes ?? 0;
final nextCarryMin =
cumulative.isNotEmpty ? cumulative.last : _carryBaseMinutes;
final nextCarryMin = cumulative.isNotEmpty ? cumulative.last : _carryBaseMinutes;
final rows = List<DataRow>.generate(_days.length, (i) {
final day = effectiveDays[i];
@@ -261,10 +274,8 @@ class _MonthlyViewState extends State<MonthlyView> {
_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<MonthlyView> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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: <Widget>[
@@ -315,8 +324,7 @@ class _MonthlyViewState extends State<MonthlyView> {
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<MonthlyView> {
),
),
// Footer
// Footer (zentriert)
const Divider(height: 1),
_MonthlySummaryFooter(
month: _monthStart,
@@ -372,7 +380,7 @@ class _MonthlyViewState extends State<MonthlyView> {
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<MonthlyView> {
}
List<DataColumn> _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<MonthlyView> {
];
}
List<DataCell> _buildEditableCells(
int dayIndex, WorkDay day, int runningDiff) {
final leftLabel = rightDayLabel(day.date); // "Mo 01.09."
final rightLabel = leftDayLabel(day.date); // "01.09. Mo"
List<DataCell> _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<MonthlyView> {
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<MonthlyView> {
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<MonthlyView> {
)));
}
// 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<MonthlyView> {
)));
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<MonthlyView> {
),
)));
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<MonthlyView> {
),
);
// --- 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 = <String?>[null, ...kAbsenceCodes];
final values = <String?>[null, ...kAbsenceCodes]; // ['G','K','U','SU','T']
return DropdownButton<String?>(
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<String?>(
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<WorkDay>.from(_days);
newDays[dayIndex] = WorkDay(
date: d.date,
intervals: willLock ? <WorkInterval>[] : d.intervals,
targetMinutes: d.targetMinutes,
targetMinutes: _dailyPlan[d.date.weekday] ?? d.targetMinutes,
code: newCode,
);
_days = newDays;
@@ -665,6 +692,8 @@ class _MonthlyViewState extends State<MonthlyView> {
}
}
});
_scheduleSave(dayIndex); // << neu: debounce statt sofort speichern
},
);
}
@@ -717,6 +746,63 @@ class _MonthlyViewState extends State<MonthlyView> {
);
_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<void> _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<MonthlyView> {
: '';
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<MonthlyView> {
// 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<MonthlyView> {
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<MonthlyView> {
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(),
);
}
}