diff --git a/lib/screens/monthly_view.dart b/lib/screens/monthly_view.dart index 2eafb82..28421cb 100644 --- a/lib/screens/monthly_view.dart +++ b/lib/screens/monthly_view.dart @@ -25,6 +25,9 @@ class _MonthlyViewState extends State { static const double _wNumber = 76; static const double _fontSize = 12; + // Codes, die Zeiten sperren/ leeren + static const Set _lockCodes = {'G', 'K', 'U', 'SU'}; + // Edit/Fokus/Controller je Feld final Set _invalidCells = {}; final Map _focusNodes = {}; @@ -156,16 +159,16 @@ class _MonthlyViewState extends State { Color? _rowColorFor(WorkDay d, {required Color? holidayBg, required Color? weekendBg}) { switch (d.code) { - case 'GZ': - return const Color(0xFFBFBFFF); // 0xBFBFFF mit vollem Alpha + case 'G': + return const Color(0xFFBFBFFF); // Gleitzeit case 'U': - return const Color(0xFF7F7FFF); // 0x7F7FFF mit vollem Alpha + return const Color(0xFF7F7FFF); // Urlaub case 'SU': - return const Color(0xFF7F7FFF); + return const Color(0xFF7F7FFF); // Sonderurlaub case 'K': - return Colors.yellow; + return Colors.yellow; // Krankenstand case 'T': - return Colors.red; + return Colors.red; // Training } final isHoliday = _holidays.containsKey(ymd(d.date)); final isWeekend = d.date.weekday == DateTime.saturday || d.date.weekday == DateTime.sunday; @@ -355,7 +358,16 @@ class _MonthlyViewState extends State { final rightLabel = leftDayLabel(day.date); // Datum Wochentag final hName = _holidays[ymd(day.date)] ?? ''; + // Sperre Zeiten, wenn Code in _lockCodes + final bool lockTimes = day.code != null && _lockCodes.contains(day.code); + + // Ist Wochenende/Feiertag => Code nicht änderbar + final bool isHoliday = _holidays.containsKey(ymd(day.date)); + final bool isWeekend = day.date.weekday == DateTime.saturday || day.date.weekday == DateTime.sunday; + final bool codeDisabled = isHoliday || isWeekend; + String slotText(int slot, bool isStart) { + if (lockTimes) return ''; if (slot < day.intervals.length) { final p = day.intervals[slot]; return fmtTimeOfDay(isStart ? p.start : p.end); @@ -385,12 +397,13 @@ class _MonthlyViewState extends State { ), ))); - // 2..11: Zeiten + // 2..11: Zeiten (evtl. gesperrt & geleert) for (int slot = 0; slot < 5; slot++) { // Start final keyS = 't_${dayIndex}_${slot}_s'; final fnS = _nodeFor(keyS); final ctrlS = _controllerFor(keyS, slotText(slot, true)); + if (lockTimes && ctrlS.text.isNotEmpty) ctrlS.text = ''; cells.add(DataCell(SizedBox( width: _wTime, child: Align( @@ -399,8 +412,10 @@ class _MonthlyViewState extends State { key: ValueKey(keyS), controller: ctrlS, focusNode: fnS, + enabled: !lockTimes, invalid: isInvalid(slot, true), onChanged: (text) { + if (lockTimes) return; final k = _cellKey(dayIndex, slot, true); final valid = text.isEmpty || _isValidHHMM(text); setState(() { @@ -417,6 +432,7 @@ class _MonthlyViewState extends State { } }, onSubmitted: (text) { + if (lockTimes) return; if (text.isEmpty || _isValidHHMM(text)) { _commitTime(dayIndex, slot, true, text); } @@ -429,6 +445,7 @@ class _MonthlyViewState extends State { final keyE = 't_${dayIndex}_${slot}_e'; final fnE = _nodeFor(keyE); final ctrlE = _controllerFor(keyE, slotText(slot, false)); + if (lockTimes && ctrlE.text.isNotEmpty) ctrlE.text = ''; cells.add(DataCell(SizedBox( width: _wTime, child: Align( @@ -437,8 +454,10 @@ class _MonthlyViewState extends State { key: ValueKey(keyE), controller: ctrlE, focusNode: fnE, + enabled: !lockTimes, invalid: isInvalid(slot, false), onChanged: (text) { + if (lockTimes) return; final k = _cellKey(dayIndex, slot, false); final valid = text.isEmpty || _isValidHHMM(text); setState(() { @@ -455,6 +474,7 @@ class _MonthlyViewState extends State { } }, onSubmitted: (text) { + if (lockTimes) return; if (text.isEmpty || _isValidHHMM(text)) { _commitTime(dayIndex, slot, false, text); } @@ -464,10 +484,13 @@ class _MonthlyViewState extends State { ))); } - // 12: Code + // 12: Code (Dropdown) — zentriert (disable bei Sa/So/Feiertag) cells.add(DataCell(SizedBox( width: _wCode, - child: Align(alignment: Alignment.center, child: _codeDropdown(dayIndex, day)), + child: Align( + alignment: Alignment.center, + child: _codeDropdown(dayIndex, day, disabled: codeDisabled), + ), ))); // 13..16: Kennzahlen (mit Override) @@ -516,8 +539,8 @@ class _MonthlyViewState extends State { ), ); - Widget _codeDropdown(int dayIndex, WorkDay day) { - final value = day.code; + Widget _codeDropdown(int dayIndex, WorkDay day, {required bool disabled}) { + final value = day.code; // null => — final values = [null, ...kAbsenceCodes]; return DropdownButton( @@ -536,19 +559,37 @@ class _MonthlyViewState extends State { return Center(child: Text(shortText, style: const TextStyle(fontSize: _fontSize))); }).toList(); }, - onChanged: (newCode) { - final d = _days[dayIndex]; - setState(() { - final newDays = List.from(_days); - newDays[dayIndex] = WorkDay( - date: d.date, - intervals: d.intervals, - targetMinutes: d.targetMinutes, - code: newCode, - ); - _days = newDays; - }); - }, + onChanged: disabled + ? null + : (newCode) { + final d = _days[dayIndex]; + // Wenn neuer Code ein Lock-Code ist: Zeiten löschen & sperren + 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, + code: newCode, + ); + _days = newDays; + + if (willLock) { + // Controller leeren & Invalid-Flags löschen + for (int slot = 0; slot < 5; slot++) { + final sKey = 't_${dayIndex}_${slot}_s'; + final eKey = 't_${dayIndex}_${slot}_e'; + final cs = _controllers[sKey]; + final ce = _controllers[eKey]; + if (cs != null && cs.text.isNotEmpty) cs.text = ''; + if (ce != null && ce.text.isNotEmpty) ce.text = ''; + _invalidCells.remove(_cellKey(dayIndex, slot, true)); + _invalidCells.remove(_cellKey(dayIndex, slot, false)); + } + } + }); + }, ); } @@ -567,6 +608,9 @@ class _MonthlyViewState extends State { final t = text.trim().isEmpty ? null : parseTextHHMM(text); final d = _days[dayIndex]; + // Wenn Code sperrt: ignorieren (Sicherheit) + if (d.code != null && _lockCodes.contains(d.code)) return; + final starts = List.filled(5, null); final ends = List.filled(5, null); @@ -602,13 +646,20 @@ class _MonthlyViewState extends State { void _syncControllersWithDays() { for (int i = 0; i < _days.length; i++) { + final lock = _days[i].code != null && _lockCodes.contains(_days[i].code); for (int slot = 0; slot < 5; slot++) { final sKey = 't_${i}_${slot}_s'; final eKey = 't_${i}_${slot}_e'; - final sText = - (slot < _days[i].intervals.length) ? fmtTimeOfDay(_days[i].intervals[slot].start) : ''; - final eText = - (slot < _days[i].intervals.length) ? fmtTimeOfDay(_days[i].intervals[slot].end) : ''; + final sText = lock + ? '' + : (slot < _days[i].intervals.length) + ? fmtTimeOfDay(_days[i].intervals[slot].start) + : ''; + final eText = lock + ? '' + : (slot < _days[i].intervals.length) + ? fmtTimeOfDay(_days[i].intervals[slot].end) + : ''; final cs = _controllers[sKey]; final ce = _controllers[eKey]; if (cs != null && cs.text != sText && !(_focusNodes[sKey]?.hasFocus ?? false)) cs.text = sText; @@ -618,6 +669,12 @@ class _MonthlyViewState extends State { } WorkDay _effectiveDay(int row, WorkDay base) { + // Wenn Code sperrt, sind die Intervalle leer (unabhängig von Controller-Text) + if (base.code != null && _lockCodes.contains(base.code)) { + final target = _dailyPlan[base.date.weekday] ?? base.targetMinutes; + return WorkDay(date: base.date, intervals: const [], targetMinutes: target, code: base.code); + } + TimeOfDay? baseSlot(WorkDay d, int slot, bool isStart) { if (slot >= d.intervals.length) return null; final iv = d.intervals[slot]; @@ -656,6 +713,7 @@ class _MonthlyViewState extends State { required bool invalid, required ValueChanged onChanged, required ValueChanged onSubmitted, + bool enabled = true, FocusNode? focusNode, }) { final theme = Theme.of(context); @@ -667,6 +725,7 @@ class _MonthlyViewState extends State { key: key, controller: controller, focusNode: focusNode, + enabled: enabled, textAlign: TextAlign.center, style: const TextStyle( fontFeatures: [FontFeature.tabularFigures()], @@ -680,6 +739,7 @@ class _MonthlyViewState extends State { border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, ).copyWith( filled: invalid, fillColor: invalid ? errorFill : null, diff --git a/lib/utils/helpers.dart b/lib/utils/helpers.dart index 099ee2a..9b2d79c 100644 --- a/lib/utils/helpers.dart +++ b/lib/utils/helpers.dart @@ -105,11 +105,11 @@ int? minutesFromHHMMSS(String? s) { } // Abwesenheitscodes und Labels -const List kAbsenceCodes = ['GZ', 'K', 'U', 'SU', 'T']; +const List kAbsenceCodes = ['G', 'K', 'U', 'SU', 'T']; String codeLabel(String? code) { switch (code) { - case 'GZ': return 'Gleitzeit'; + case 'G': return 'Gleitzeit'; case 'K': return 'Krankenstand'; case 'U': return 'Urlaub'; case 'SU': return 'Sonderurlaub';