initial commit

This commit is contained in:
2025-09-02 08:21:00 +02:00
parent 073ced647c
commit f212f9feb3
2 changed files with 90 additions and 30 deletions
+88 -28
View File
@@ -25,6 +25,9 @@ class _MonthlyViewState extends State<MonthlyView> {
static const double _wNumber = 76;
static const double _fontSize = 12;
// Codes, die Zeiten sperren/ leeren
static const Set<String> _lockCodes = {'G', 'K', 'U', 'SU'};
// Edit/Fokus/Controller je Feld
final Set<String> _invalidCells = <String>{};
final Map<String, FocusNode> _focusNodes = {};
@@ -156,16 +159,16 @@ class _MonthlyViewState extends State<MonthlyView> {
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<MonthlyView> {
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<MonthlyView> {
),
)));
// 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<MonthlyView> {
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<MonthlyView> {
}
},
onSubmitted: (text) {
if (lockTimes) return;
if (text.isEmpty || _isValidHHMM(text)) {
_commitTime(dayIndex, slot, true, text);
}
@@ -429,6 +445,7 @@ class _MonthlyViewState extends State<MonthlyView> {
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<MonthlyView> {
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<MonthlyView> {
}
},
onSubmitted: (text) {
if (lockTimes) return;
if (text.isEmpty || _isValidHHMM(text)) {
_commitTime(dayIndex, slot, false, text);
}
@@ -464,10 +484,13 @@ class _MonthlyViewState extends State<MonthlyView> {
)));
}
// 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<MonthlyView> {
),
);
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 = <String?>[null, ...kAbsenceCodes];
return DropdownButton<String?>(
@@ -536,19 +559,37 @@ class _MonthlyViewState extends State<MonthlyView> {
return Center(child: Text(shortText, style: const TextStyle(fontSize: _fontSize)));
}).toList();
},
onChanged: (newCode) {
final d = _days[dayIndex];
setState(() {
final newDays = List<WorkDay>.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<WorkDay>.from(_days);
newDays[dayIndex] = WorkDay(
date: d.date,
intervals: willLock ? <WorkInterval>[] : 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<MonthlyView> {
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<TimeOfDay?>.filled(5, null);
final ends = List<TimeOfDay?>.filled(5, null);
@@ -602,13 +646,20 @@ class _MonthlyViewState extends State<MonthlyView> {
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<MonthlyView> {
}
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<MonthlyView> {
required bool invalid,
required ValueChanged<String> onChanged,
required ValueChanged<String> onSubmitted,
bool enabled = true,
FocusNode? focusNode,
}) {
final theme = Theme.of(context);
@@ -667,6 +725,7 @@ class _MonthlyViewState extends State<MonthlyView> {
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<MonthlyView> {
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
disabledBorder: InputBorder.none,
).copyWith(
filled: invalid,
fillColor: invalid ? errorFill : null,
+2 -2
View File
@@ -105,11 +105,11 @@ int? minutesFromHHMMSS(String? s) {
}
// Abwesenheitscodes und Labels
const List<String> kAbsenceCodes = ['GZ', 'K', 'U', 'SU', 'T'];
const List<String> 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';