initial commit
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user