initial commit

54736fa680
Herwig Birke 4 months ago
parent ba67074188
commit 27a89e57dd

@ -17,12 +17,13 @@ class MonthlyView extends StatefulWidget {
} }
class _MonthlyViewState extends State<MonthlyView> { class _MonthlyViewState extends State<MonthlyView> {
// Spaltenbreiten (Header & Body identisch -> exakte Ausrichtung) // Kompaktere Layout-Parameter
static const double _wDate = 120; static const double _wDate = 96;
static const double _wHoliday = 160; static const double _wHoliday = 120;
static const double _wTime = 84; static const double _wTime = 60;
static const double _wCode = 110; static const double _wCode = 70;
static const double _wNumber = 92; static const double _wNumber = 76;
static const double _fontSize = 12;
// Edit/Fokus/Controller je Feld // Edit/Fokus/Controller je Feld
final Set<String> _invalidCells = <String>{}; final Set<String> _invalidCells = <String>{};
@ -127,24 +128,28 @@ class _MonthlyViewState extends State<MonthlyView> {
double get _tableMinWidth => double get _tableMinWidth =>
_wDate + _wHoliday + (10 * _wTime) + _wCode + (4 * _wNumber) + _wDate; _wDate + _wHoliday + (10 * _wTime) + _wCode + (4 * _wNumber) + _wDate;
// Override für Ist-Minuten nach Code // Ist-Override für U/SU/K/T
int _workedFor(WorkDay d) { int _workedFor(WorkDay d) {
final c = d.code; final c = d.code;
if (c == 'U' || c == 'SU' || c == 'K' || c == 'T') { if (c == 'U' || c == 'SU' || c == 'K' || c == 'T') {
return d.targetMinutes; // Urlaub/Sonderurlaub/Krankenstand/Training -> Ist = Soll return d.targetMinutes;
} }
return d.workedMinutes; // normal aus Intervallen return d.workedMinutes;
} }
Color? _rowColorFor(WorkDay d, {required Color? holidayBg, required Color? weekendBg}) { Color? _rowColorFor(WorkDay d, {required Color? holidayBg, required Color? weekendBg}) {
switch (d.code) { switch (d.code) {
case 'GZ': return const Color(0xFFBFBFFF); case 'GZ':
case 'U': return const Color(0xFF7F7FFF); return const Color(0xFFBFBFFF); // 0xBFBFFF mit vollem Alpha
case 'SU': return const Color(0xFF7F7FFF); case 'U':
case 'K': return Colors.yellow; return const Color(0xFF7F7FFF); // 0x7F7FFF mit vollem Alpha
case 'T': return Colors.red; case 'SU':
return const Color(0xFF7F7FFF);
case 'K':
return Colors.yellow;
case 'T':
return Colors.red;
} }
// kein Code -> ggf. Feiertag/Wochenende
final isHoliday = _holidays.containsKey(ymd(d.date)); 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 (isHoliday) return holidayBg;
@ -155,16 +160,15 @@ class _MonthlyViewState extends State<MonthlyView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final headerColumns = _buildColumns(); final headerColumns = _buildColumns();
final bodyColumns = _buildColumns(); // gleiche Struktur (Body blendet Header aus) final bodyColumns = _buildColumns();
final theme = Theme.of(context); final theme = Theme.of(context);
final holidayBg = theme.colorScheme.secondaryContainer.withOpacity(0.45); final holidayBg = theme.colorScheme.secondaryContainer.withOpacity(0.45);
final weekendBg = Colors.grey.withOpacity(0.30); final weekendBg = Colors.grey.withOpacity(0.30);
// Effektive Tage (inkl. Live-Edits)
final effectiveDays = List<WorkDay>.generate(_days.length, (i) => _effectiveDay(i, _days[i])); final effectiveDays = List<WorkDay>.generate(_days.length, (i) => _effectiveDay(i, _days[i]));
// kumulative Differenz basierend auf Override- oder Intervall-Arbeitszeit // diffs & kumuliert (mit Override)
final diffs = <int>[]; final diffs = <int>[];
for (final d in effectiveDays) { for (final d in effectiveDays) {
final worked = _workedFor(d); final worked = _workedFor(d);
@ -218,20 +222,20 @@ class _MonthlyViewState extends State<MonthlyView> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
const Text('Fehler beim Laden:'), Text('Fehler beim Laden:', style: TextStyle(fontSize: _fontSize)),
SelectableText(_error ?? ''), SelectableText(_error ?? '', style: const TextStyle(fontSize: _fontSize)),
], ],
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => _loadMonth(_monthStart), onPressed: () => _loadMonth(_monthStart),
child: const Text('Erneut versuchen'), child: Text('Erneut versuchen', style: const TextStyle(fontSize: _fontSize)),
), ),
], ],
), ),
), ),
// FIXIERTE KOPFZEILE (nur horizontal scrollend) // FIXIERTE KOPFZEILE
Scrollbar( Scrollbar(
controller: _hCtrl, controller: _hCtrl,
notificationPredicate: (n) => n.metrics.axis == Axis.horizontal, notificationPredicate: (n) => n.metrics.axis == Axis.horizontal,
@ -242,22 +246,24 @@ class _MonthlyViewState extends State<MonthlyView> {
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(minWidth: _tableMinWidth), constraints: BoxConstraints(minWidth: _tableMinWidth),
child: DataTableTheme( child: DataTableTheme(
data: const DataTableThemeData( data: DataTableThemeData(
headingRowHeight: 42, headingRowHeight: 30,
columnSpacing: 20, columnSpacing: 10,
headingTextStyle: TextStyle(fontWeight: FontWeight.w700), headingTextStyle: const TextStyle(fontWeight: FontWeight.w700).copyWith(
fontSize: _fontSize,
),
), ),
child: DataTable( child: DataTable(
showCheckboxColumn: false, showCheckboxColumn: false,
columns: headerColumns, columns: headerColumns,
rows: const <DataRow>[], // nur Header anzeigen rows: const <DataRow>[],
), ),
), ),
), ),
), ),
), ),
// BODY (horiz. & vert. Scroll, aber Header im Body ausgeblendet) // BODY
Expanded( Expanded(
child: Scrollbar( child: Scrollbar(
controller: _hCtrl, controller: _hCtrl,
@ -265,7 +271,7 @@ class _MonthlyViewState extends State<MonthlyView> {
thumbVisibility: true, thumbVisibility: true,
child: SingleChildScrollView( child: SingleChildScrollView(
controller: _hCtrl, controller: _hCtrl,
padding: const EdgeInsets.only(bottom: 24), padding: const EdgeInsets.only(bottom: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(minWidth: _tableMinWidth), constraints: BoxConstraints(minWidth: _tableMinWidth),
@ -277,10 +283,10 @@ class _MonthlyViewState extends State<MonthlyView> {
controller: _vCtrl, controller: _vCtrl,
child: DataTableTheme( child: DataTableTheme(
data: const DataTableThemeData( data: const DataTableThemeData(
headingRowHeight: 0, // Header hier ausblenden headingRowHeight: 0,
dataRowMinHeight: 44, dataRowMinHeight: 30,
dataRowMaxHeight: 54, dataRowMaxHeight: 34,
columnSpacing: 20, columnSpacing: 10,
), ),
child: DataTable( child: DataTable(
showCheckboxColumn: false, showCheckboxColumn: false,
@ -303,19 +309,18 @@ class _MonthlyViewState extends State<MonthlyView> {
width: width, width: width,
child: Align( child: Align(
alignment: align, alignment: align,
child: Text(label, textAlign: _toTextAlign(align)), child: Text(
label,
textAlign: _toTextAlign(align),
style: const TextStyle(fontSize: _fontSize),
),
), ),
), ),
); );
return [ return [
// 0 Datum (rechtsbündig)
c('Datum', align: Alignment.centerRight, width: _wDate), c('Datum', align: Alignment.centerRight, width: _wDate),
// 1 Feiertag (links)
c('Feiertag', align: Alignment.centerLeft, width: _wHoliday), c('Feiertag', align: Alignment.centerLeft, width: _wHoliday),
// 2..11 Zeitspalten (zentriert)
c('Start 1', align: Alignment.center, width: _wTime), c('Start 1', align: Alignment.center, width: _wTime),
c('Ende 1', align: Alignment.center, width: _wTime), c('Ende 1', align: Alignment.center, width: _wTime),
c('Start 2', align: Alignment.center, width: _wTime), c('Start 2', align: Alignment.center, width: _wTime),
@ -326,24 +331,19 @@ class _MonthlyViewState extends State<MonthlyView> {
c('Ende 4', align: Alignment.center, width: _wTime), c('Ende 4', align: Alignment.center, width: _wTime),
c('Start 5', align: Alignment.center, width: _wTime), c('Start 5', align: Alignment.center, width: _wTime),
c('Ende 5', align: Alignment.center, width: _wTime), c('Ende 5', align: Alignment.center, width: _wTime),
// 12 Code (zentriert)
c('Code', align: Alignment.center, width: _wCode), c('Code', align: Alignment.center, width: _wCode),
// 13..16 Kennzahlen (rechtsbündig)
c('Soll', align: Alignment.centerRight, width: _wNumber), c('Soll', align: Alignment.centerRight, width: _wNumber),
c('Ist', align: Alignment.centerRight, width: _wNumber), c('Ist', align: Alignment.centerRight, width: _wNumber),
c('Differenz', align: Alignment.centerRight, width: _wNumber), c('Differenz', align: Alignment.centerRight, width: _wNumber),
c('Differenz gesamt', align: Alignment.centerRight, width: _wNumber), c('Differenz gesamt', align: Alignment.centerRight, width: _wNumber),
// 17 Datum (linksbündig)
c('Datum', align: Alignment.centerLeft, width: _wDate), c('Datum', align: Alignment.centerLeft, width: _wDate),
]; ];
} }
List<DataCell> _buildEditableCells(int dayIndex, WorkDay day, int runningDiff) { List<DataCell> _buildEditableCells(int dayIndex, WorkDay day, int runningDiff) {
final leftLabel = leftDayLabel(day.date); // Gewünscht: links Wochentag zuerst, rechts Datum zuerst.
final rightLabel = rightDayLabel(day.date); final leftLabel = rightDayLabel(day.date); // Wochentag Datum
final rightLabel = leftDayLabel(day.date); // Datum Wochentag
final hName = _holidays[ymd(day.date)] ?? ''; final hName = _holidays[ymd(day.date)] ?? '';
String slotText(int slot, bool isStart) { String slotText(int slot, bool isStart) {
@ -362,15 +362,21 @@ class _MonthlyViewState extends State<MonthlyView> {
// 0: Datum (rechts) // 0: Datum (rechts)
cells.add(DataCell(SizedBox( cells.add(DataCell(SizedBox(
width: _wDate, width: _wDate,
child: Align(alignment: Alignment.centerRight, child: Text(leftLabel)), child: Align(
alignment: Alignment.centerRight,
child: Text(leftLabel, style: const TextStyle(fontSize: _fontSize)),
),
))); )));
// 1: Feiertag-Name // 1: Feiertag
cells.add(DataCell(SizedBox( cells.add(DataCell(SizedBox(
width: _wHoliday, width: _wHoliday,
child: Align(alignment: Alignment.centerLeft, child: Text(hName)), child: Align(
alignment: Alignment.centerLeft,
child: Text(hName, style: const TextStyle(fontSize: _fontSize)),
),
))); )));
// 2..11: editierbare Zeiten (zentriert) // 2..11: Zeiten
for (int slot = 0; slot < 5; slot++) { for (int slot = 0; slot < 5; slot++) {
// Start // Start
final keyS = 't_${dayIndex}_${slot}_s'; final keyS = 't_${dayIndex}_${slot}_s';
@ -398,7 +404,7 @@ class _MonthlyViewState extends State<MonthlyView> {
if (valid && (text.isEmpty || text.length == 5)) { if (valid && (text.isEmpty || text.length == 5)) {
_commitTime(dayIndex, slot, true, text); _commitTime(dayIndex, slot, true, text);
} else { } else {
setState(() {}); // live neu berechnen setState(() {});
} }
}, },
onSubmitted: (text) { onSubmitted: (text) {
@ -436,7 +442,7 @@ class _MonthlyViewState extends State<MonthlyView> {
if (valid && (text.isEmpty || text.length == 5)) { if (valid && (text.isEmpty || text.length == 5)) {
_commitTime(dayIndex, slot, false, text); _commitTime(dayIndex, slot, false, text);
} else { } else {
setState(() {}); // live neu berechnen setState(() {});
} }
}, },
onSubmitted: (text) { onSubmitted: (text) {
@ -449,13 +455,13 @@ class _MonthlyViewState extends State<MonthlyView> {
))); )));
} }
// 12: Code (Dropdown) zentriert (Kurzname geschlossen, Langname in Liste) // 12: Code
cells.add(DataCell(SizedBox( cells.add(DataCell(SizedBox(
width: _wCode, width: _wCode,
child: Align(alignment: Alignment.center, child: _codeDropdown(dayIndex, day)), child: Align(alignment: Alignment.center, child: _codeDropdown(dayIndex, day)),
))); )));
// 13..16: Kennzahlen (rechts) mit Override // 13..16: Kennzahlen (mit Override)
final worked = _workedFor(day); final worked = _workedFor(day);
final soll = minutesToHHMM(day.targetMinutes); final soll = minutesToHHMM(day.targetMinutes);
final ist = minutesToHHMM(worked); final ist = minutesToHHMM(worked);
@ -464,25 +470,28 @@ class _MonthlyViewState extends State<MonthlyView> {
cells.add(DataCell(SizedBox( cells.add(DataCell(SizedBox(
width: _wNumber, width: _wNumber,
child: Align(alignment: Alignment.centerRight, child: mono(soll)), child: Align(alignment: Alignment.centerRight, child: _monoSmall(soll)),
))); )));
cells.add(DataCell(SizedBox( cells.add(DataCell(SizedBox(
width: _wNumber, width: _wNumber,
child: Align(alignment: Alignment.centerRight, child: mono(ist)), child: Align(alignment: Alignment.centerRight, child: _monoSmall(ist)),
))); )));
cells.add(DataCell(SizedBox( cells.add(DataCell(SizedBox(
width: _wNumber, width: _wNumber,
child: Align(alignment: Alignment.centerRight, child: mono(diff)), child: Align(alignment: Alignment.centerRight, child: _monoSmall(diff)),
))); )));
cells.add(DataCell(SizedBox( cells.add(DataCell(SizedBox(
width: _wNumber, width: _wNumber,
child: Align(alignment: Alignment.centerRight, child: mono(diffSum)), child: Align(alignment: Alignment.centerRight, child: _monoSmall(diffSum)),
))); )));
// 17: Datum (links) // 17: Datum (links)
cells.add(DataCell(SizedBox( cells.add(DataCell(SizedBox(
width: _wDate, width: _wDate,
child: Align(alignment: Alignment.centerLeft, child: Text(rightLabel)), child: Align(
alignment: Alignment.centerLeft,
child: Text(rightLabel, style: const TextStyle(fontSize: _fontSize)),
),
))); )));
assert(cells.length == 18, 'Row has ${cells.length} cells but expected 18.'); assert(cells.length == 18, 'Row has ${cells.length} cells but expected 18.');
@ -490,25 +499,23 @@ class _MonthlyViewState extends State<MonthlyView> {
} }
Widget _codeDropdown(int dayIndex, WorkDay day) { Widget _codeDropdown(int dayIndex, WorkDay day) {
final value = day.code; // null => kein Code () final value = day.code;
final values = <String?>[null, ...kAbsenceCodes]; final values = <String?>[null, ...kAbsenceCodes];
return DropdownButton<String?>( return DropdownButton<String?>(
isExpanded: true, isExpanded: true,
value: value, value: value,
// Langnamen in der aufgeklappten Liste:
items: values.map((v) { items: values.map((v) {
final label = v == null ? '' : codeLabel(v); final label = v == null ? '' : codeLabel(v);
return DropdownMenuItem<String?>( return DropdownMenuItem<String?>(
value: v, value: v,
child: Text(label, textAlign: TextAlign.center), child: Text(label, textAlign: TextAlign.center, style: const TextStyle(fontSize: _fontSize)),
); );
}).toList(), }).toList(),
// Kurznamen (zentriert) in der geschlossenen Anzeige:
selectedItemBuilder: (context) { selectedItemBuilder: (context) {
return values.map((v) { return values.map((v) {
final shortText = v ?? ''; final shortText = v ?? '';
return Center(child: Text(shortText)); return Center(child: Text(shortText, style: const TextStyle(fontSize: _fontSize)));
}).toList(); }).toList();
}, },
onChanged: (newCode) { onChanged: (newCode) {
@ -624,7 +631,7 @@ class _MonthlyViewState extends State<MonthlyView> {
); );
} }
// rahmenlose Eingabefelder, nur dezente Füllung bei invalid // kompakte, rahmenlose Eingabefelder
Widget _timeField({ Widget _timeField({
required Key key, required Key key,
required TextEditingController controller, required TextEditingController controller,
@ -643,12 +650,15 @@ class _MonthlyViewState extends State<MonthlyView> {
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(fontFeatures: [FontFeature.tabularFigures()]), style: const TextStyle(
fontFeatures: [FontFeature.tabularFigures()],
fontSize: _fontSize,
),
keyboardType: TextInputType.datetime, keyboardType: TextInputType.datetime,
inputFormatters: const [HHmmInputFormatter()], inputFormatters: const [HHmmInputFormatter()],
decoration: const InputDecoration( decoration: const InputDecoration(
isDense: true, isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 6, vertical: 6), contentPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 2),
border: InputBorder.none, border: InputBorder.none,
enabledBorder: InputBorder.none, enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none, focusedBorder: InputBorder.none,
@ -662,6 +672,14 @@ class _MonthlyViewState extends State<MonthlyView> {
); );
} }
Widget _monoSmall(String s) => Text(
s,
style: const TextStyle(
fontSize: _fontSize,
fontFeatures: [FontFeature.tabularFigures()],
),
);
TextAlign _toTextAlign(Alignment a) { TextAlign _toTextAlign(Alignment a) {
if (a == Alignment.centerRight) return TextAlign.right; if (a == Alignment.centerRight) return TextAlign.right;
if (a == Alignment.centerLeft) return TextAlign.left; if (a == Alignment.centerLeft) return TextAlign.left;
@ -696,7 +714,7 @@ class _MonthHeader extends StatelessWidget {
child: TextButton.icon( child: TextButton.icon(
onPressed: loading ? null : onPickMonth, onPressed: loading ? null : onPickMonth,
icon: const Icon(Icons.calendar_month), icon: const Icon(Icons.calendar_month),
label: Text(title, style: Theme.of(context).textTheme.titleLarge), label: Text(title, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontSize: 16)),
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onSurface, foregroundColor: Theme.of(context).colorScheme.onSurface,
), ),

Loading…
Cancel
Save