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

Loading…
Cancel
Save