Monthy calculation

54736fa680
Herwig Birke 2 months ago
parent e84d463212
commit f679ea672b

@ -1,3 +1,4 @@
import 'dart:typed_data';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../models/work_day.dart'; import '../models/work_day.dart';

@ -1,4 +1,5 @@
import 'dart:async'; // << neu: für Timer (Debounce) import 'dart:async';
import 'dart:convert'; // <-- für jsonEncode (Debug)
import 'dart:ui' show FontFeature; import 'dart:ui' show FontFeature;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -27,6 +28,9 @@ class _MonthlyViewState extends State<MonthlyView> {
static const double _wNumber = 76; static const double _wNumber = 76;
static const double _fontSize = 12; static const double _fontSize = 12;
// API Base (nur für Debug-Ausgabe)
static const String _apiUrl = 'https://api.windesign.at/workinghours.php';
// Codes, die Zeiten leeren/sperren // Codes, die Zeiten leeren/sperren
static const Set<String> _lockCodes = {'G', 'K', 'U', 'SU'}; static const Set<String> _lockCodes = {'G', 'K', 'U', 'SU'};
@ -34,7 +38,8 @@ class _MonthlyViewState extends State<MonthlyView> {
final Set<String> _invalidCells = <String>{}; final Set<String> _invalidCells = <String>{};
final Map<String, FocusNode> _focusNodes = {}; final Map<String, FocusNode> _focusNodes = {};
final Map<String, TextEditingController> _controllers = {}; final Map<String, TextEditingController> _controllers = {};
FocusNode _nodeFor(String key) => _focusNodes.putIfAbsent(key, () => FocusNode()); FocusNode _nodeFor(String key) =>
_focusNodes.putIfAbsent(key, () => FocusNode());
TextEditingController _controllerFor(String key, String initial) => TextEditingController _controllerFor(String key, String initial) =>
_controllers.putIfAbsent(key, () => TextEditingController(text: initial)); _controllers.putIfAbsent(key, () => TextEditingController(text: initial));
@ -68,11 +73,12 @@ class _MonthlyViewState extends State<MonthlyView> {
late final ScrollController _vCtrl; late final ScrollController _vCtrl;
bool _syncingH = false; bool _syncingH = false;
// --- NEU: Debounce + Save-Status pro Zeile --- // Save-Status pro Zeile
final Map<int, Timer> _saveTimers = {}; // rowIndex -> Timer final Map<int, Timer> _saveTimers = {}; // rowIndex -> Timer
final Set<int> _savingRows = {}; // Zeilen, die gerade speichern final Set<int> _savingRows = {}; // Zeilen, die gerade speichern
final Set<int> _justSavedRows = {}; // Zeilen, die eben gespeichert haben (Häkchen kurz anzeigen) final Set<int> _justSavedRows =
final Map<int, String> _rowSaveError = {}; // Zeilenfehler {}; // Zeilen, die eben gespeichert haben (Häkchen kurz anzeigen)
final Map<int, String> _rowSaveError = {}; // Zeilenfehler
@override @override
void initState() { void initState() {
@ -118,6 +124,9 @@ class _MonthlyViewState extends State<MonthlyView> {
} }
Future<void> _loadMonth(DateTime m) async { Future<void> _loadMonth(DateTime m) async {
// DEBUG
print('[monthly] LOAD month=${m.year}-${m.month.toString().padLeft(2, '0')}');
setState(() { setState(() {
_loading = true; _loading = true;
_error = null; _error = null;
@ -125,14 +134,14 @@ class _MonthlyViewState extends State<MonthlyView> {
}); });
try { try {
final results = await Future.wait([ final results = await Future.wait([
_bookingApi.getBookingList(m), // List<WorkDay> _bookingApi.getBookingList(m), // List<WorkDay>
_dailyApi.getDailyMinutes(), // Map<int,int> _dailyApi.getDailyMinutes(), // Map<int,int>
_bookingApi.getMonthStart(m), // MonthStart _bookingApi.getMonthStart(m), // MonthStart
]); ]);
final apiDays = results[0] as List<WorkDay>; final apiDays = results[0] as List<WorkDay>;
final plan = results[1] as Map<int, int>; final plan = results[1] as Map<int, int>;
final mStart = results[2] as MonthStart; final mStart = results[2] as MonthStart;
final holidayMap = buildHolidayMapAT(m.year); final holidayMap = buildHolidayMapAT(m.year);
@ -140,10 +149,12 @@ class _MonthlyViewState extends State<MonthlyView> {
final withTargets = filled.map((d) { final withTargets = filled.map((d) {
final isHoliday = holidayMap.containsKey(ymd(d.date)); final isHoliday = holidayMap.containsKey(ymd(d.date));
// Feiertage haben immer Soll 0 // Feiertage haben immer Soll 0
final baseTarget = isHoliday ? 0 : (plan[d.date.weekday] ?? d.targetMinutes); final baseTarget =
isHoliday ? 0 : (plan[d.date.weekday] ?? d.targetMinutes);
// U/SU/K ebenfalls Soll 0 // U/SU/K ebenfalls Soll 0
final code = d.code; final code = d.code;
final target = (code == 'U' || code == 'SU' || code == 'K') ? 0 : baseTarget; final target =
(code == 'U' || code == 'SU' || code == 'K') ? 0 : baseTarget;
return WorkDay( return WorkDay(
date: d.date, date: d.date,
intervals: d.intervals, intervals: d.intervals,
@ -158,7 +169,8 @@ class _MonthlyViewState extends State<MonthlyView> {
_days = withTargets; _days = withTargets;
_dailyPlan = plan; _dailyPlan = plan;
_monthStartInfo = mStart; _monthStartInfo = mStart;
_carryBaseMinutes = mStart.carryBaseMinutes; // starthours + overtime + correction _carryBaseMinutes =
mStart.carryBaseMinutes; // starthours + overtime + correction
_loading = false; _loading = false;
// Status-Maps leeren (neuer Monat) // Status-Maps leeren (neuer Monat)
@ -167,8 +179,16 @@ class _MonthlyViewState extends State<MonthlyView> {
_rowSaveError.clear(); _rowSaveError.clear();
}); });
// DEBUG: geladene Monats-Startdaten
print('[monthly] monthStart loaded for ${ymd(_monthStart)} '
'carryBase=${_carryBaseMinutes} '
'startvacation=${_monthStartInfo?.startVacationUnits ?? 0} '
'overtime=${_monthStartInfo?.overtimeMinutes ?? 0} '
'correction=${_monthStartInfo?.correctionMinutes ?? 0}');
_syncControllersWithDays(); _syncControllersWithDays();
} catch (e) { } catch (e) {
print('[monthly] LOAD ERROR: $e');
setState(() { setState(() {
_error = e.toString(); _error = e.toString();
_loading = false; _loading = false;
@ -193,7 +213,8 @@ class _MonthlyViewState extends State<MonthlyView> {
} }
} }
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 'G': case 'G':
return const Color(0xFFBFBFFF); // Gleitzeit return const Color(0xFFBFBFFF); // Gleitzeit
@ -202,12 +223,13 @@ class _MonthlyViewState extends State<MonthlyView> {
case 'SU': case 'SU':
return const Color(0xFF7F7FFF); // Sonderurlaub return const Color(0xFF7F7FFF); // Sonderurlaub
case 'K': case 'K':
return Colors.yellow; // Krankenstand return Colors.yellow; // Krankenstand
case 'T': case 'T':
return Colors.red; // Training return Colors.red; // Training
} }
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;
if (isWeekend) return weekendBg; if (isWeekend) return weekendBg;
return null; return null;
@ -215,7 +237,6 @@ class _MonthlyViewState extends State<MonthlyView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final headerColumns = _buildColumns();
final bodyColumns = _buildColumns(); final bodyColumns = _buildColumns();
final theme = Theme.of(context); final theme = Theme.of(context);
@ -223,7 +244,8 @@ class _MonthlyViewState extends State<MonthlyView> {
final weekendBg = Colors.grey.withOpacity(0.30); final weekendBg = Colors.grey.withOpacity(0.30);
// Live-Effective-Tage (inkl. Eingabetexte + Sperrlogik) // Live-Effective-Tage (inkl. Eingabetexte + Sperrlogik)
final effectiveDays = List<WorkDay>.generate(_days.length, (i) => _effectiveDay(i, _days[i])); final effectiveDays =
List<WorkDay>.generate(_days.length, (i) => _effectiveDay(i, _days[i]));
// Tagesdifferenzen & kumuliert (Start mit Monatssaldo aus API) // Tagesdifferenzen & kumuliert (Start mit Monatssaldo aus API)
final diffs = <int>[]; final diffs = <int>[];
@ -257,7 +279,8 @@ class _MonthlyViewState extends State<MonthlyView> {
}; };
final correctionMin = _monthStartInfo?.correctionMinutes ?? 0; final correctionMin = _monthStartInfo?.correctionMinutes ?? 0;
final nextCarryMin = cumulative.isNotEmpty ? cumulative.last : _carryBaseMinutes; final nextCarryMin =
cumulative.isNotEmpty ? cumulative.last : _carryBaseMinutes;
final rows = List<DataRow>.generate(_days.length, (i) { final rows = List<DataRow>.generate(_days.length, (i) {
final day = effectiveDays[i]; final day = effectiveDays[i];
@ -274,8 +297,10 @@ class _MonthlyViewState extends State<MonthlyView> {
_MonthHeader( _MonthHeader(
month: _monthStart, month: _monthStart,
loading: _loading, loading: _loading,
onPrev: () => _loadMonth(DateTime(_monthStart.year, _monthStart.month - 1, 1)), onPrev: () =>
onNext: () => _loadMonth(DateTime(_monthStart.year, _monthStart.month + 1, 1)), _loadMonth(DateTime(_monthStart.year, _monthStart.month - 1, 1)),
onNext: () =>
_loadMonth(DateTime(_monthStart.year, _monthStart.month + 1, 1)),
onPickMonth: () async { onPickMonth: () async {
final picked = await showDatePicker( final picked = await showDatePicker(
context: context, context: context,
@ -300,8 +325,10 @@ class _MonthlyViewState extends State<MonthlyView> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Text('Fehler beim Laden:', style: TextStyle(fontSize: _fontSize)), Text('Fehler beim Laden:',
SelectableText(_error ?? '', style: const TextStyle(fontSize: _fontSize)), style: TextStyle(fontSize: _fontSize)),
SelectableText(_error ?? '',
style: const TextStyle(fontSize: _fontSize)),
], ],
), ),
actions: <Widget>[ actions: <Widget>[
@ -324,7 +351,8 @@ class _MonthlyViewState extends State<MonthlyView> {
data: DataTableThemeData( data: DataTableThemeData(
headingRowHeight: 30, headingRowHeight: 30,
columnSpacing: 10, columnSpacing: 10,
headingTextStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: _fontSize), headingTextStyle: const TextStyle(
fontWeight: FontWeight.w700, fontSize: _fontSize),
), ),
child: const _HeaderOnlyDataTable(), child: const _HeaderOnlyDataTable(),
), ),
@ -397,7 +425,9 @@ class _MonthlyViewState extends State<MonthlyView> {
} }
List<DataColumn> _buildColumns() { List<DataColumn> _buildColumns() {
DataColumn c(String label, {Alignment align = Alignment.center, double? width}) => DataColumn( DataColumn c(String label,
{Alignment align = Alignment.center, double? width}) =>
DataColumn(
label: SizedBox( label: SizedBox(
width: width, width: width,
child: Align( child: Align(
@ -433,14 +463,16 @@ class _MonthlyViewState extends State<MonthlyView> {
]; ];
} }
List<DataCell> _buildEditableCells(int dayIndex, WorkDay day, int runningDiff) { List<DataCell> _buildEditableCells(
final leftLabel = rightDayLabel(day.date); // "Mo 01.09." int dayIndex, WorkDay day, int runningDiff) {
final rightLabel = leftDayLabel(day.date); // "01.09. Mo" final leftLabel = rightDayLabel(day.date); // "Mo 01.09."
final rightLabel = leftDayLabel(day.date); // "01.09. Mo"
final hName = _holidays[ymd(day.date)] ?? ''; final hName = _holidays[ymd(day.date)] ?? '';
final bool lockTimes = day.code != null && _lockCodes.contains(day.code); final bool lockTimes = day.code != null && _lockCodes.contains(day.code);
final bool isHoliday = _holidays.containsKey(ymd(day.date)); final bool isHoliday = _holidays.containsKey(ymd(day.date));
final bool isWeekend = day.date.weekday == DateTime.saturday || day.date.weekday == DateTime.sunday; final bool isWeekend = day.date.weekday == DateTime.saturday ||
day.date.weekday == DateTime.sunday;
final bool codeDisabled = isHoliday || isWeekend; final bool codeDisabled = isHoliday || isWeekend;
String slotText(int slot, bool isStart) { String slotText(int slot, bool isStart) {
@ -480,7 +512,7 @@ class _MonthlyViewState extends State<MonthlyView> {
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
_rowStatusBadge(dayIndex), // << neu: Spinner/Häkchen/Fault _rowStatusBadge(dayIndex), // Spinner/Häkchen/Fehler
], ],
), ),
), ),
@ -603,7 +635,8 @@ class _MonthlyViewState extends State<MonthlyView> {
))); )));
cells.add(DataCell(SizedBox( cells.add(DataCell(SizedBox(
width: _wNumber, width: _wNumber,
child: Align(alignment: Alignment.centerRight, child: _monoSmall(diffSum)), child:
Align(alignment: Alignment.centerRight, child: _monoSmall(diffSum)),
))); )));
// 17: Datum (linksbündig) // 17: Datum (linksbündig)
@ -615,7 +648,8 @@ class _MonthlyViewState extends State<MonthlyView> {
), ),
))); )));
assert(cells.length == 18, 'Row has ${cells.length} cells but expected 18.'); assert(
cells.length == 18, 'Row has ${cells.length} cells but expected 18.');
return cells; return cells;
} }
@ -628,7 +662,7 @@ class _MonthlyViewState extends State<MonthlyView> {
), ),
); );
// --- NEU: kleiner Status-Badge pro Zeile --- // kleiner Status-Badge pro Zeile
Widget _rowStatusBadge(int row) { Widget _rowStatusBadge(int row) {
if (_savingRows.contains(row)) { if (_savingRows.contains(row)) {
return const SizedBox( return const SizedBox(
@ -638,7 +672,8 @@ class _MonthlyViewState extends State<MonthlyView> {
); );
} }
if (_rowSaveError.containsKey(row)) { if (_rowSaveError.containsKey(row)) {
return Icon(Icons.error_outline, size: 14, color: Theme.of(context).colorScheme.error); return Icon(Icons.error_outline,
size: 14, color: Theme.of(context).colorScheme.error);
} }
if (_justSavedRows.contains(row)) { if (_justSavedRows.contains(row)) {
return const Icon(Icons.check_circle, size: 14, color: Colors.green); return const Icon(Icons.check_circle, size: 14, color: Colors.green);
@ -657,20 +692,30 @@ class _MonthlyViewState extends State<MonthlyView> {
final label = v == null ? '' : codeLabel(v); // langer Name im Dropdown final label = v == null ? '' : codeLabel(v); // langer Name im Dropdown
return DropdownMenuItem<String?>( return DropdownMenuItem<String?>(
value: v, value: v,
child: Text(label, textAlign: TextAlign.center, style: const TextStyle(fontSize: _fontSize)), child: Text(label,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: _fontSize)),
); );
}).toList(), }).toList(),
selectedItemBuilder: (context) { selectedItemBuilder: (context) {
return values.map((v) { return values.map((v) {
final shortText = v ?? ''; // kurzer Name in der Tabelle final shortText = v ?? ''; // kurzer Name in der Tabelle
return Center(child: Text(shortText, style: const TextStyle(fontSize: _fontSize))); return Center(
child:
Text(shortText, style: const TextStyle(fontSize: _fontSize)));
}).toList(); }).toList();
}, },
onChanged: disabled onChanged: disabled
? null ? null
: (newCode) { : (newCode) {
final d = _days[dayIndex]; final d = _days[dayIndex];
final bool willLock = newCode != null && _lockCodes.contains(newCode); final bool willLock =
newCode != null && _lockCodes.contains(newCode);
// DEBUG
print('[monthly] CODE change day=${ymd(d.date)} newCode=${newCode ?? '-'} '
'(lockTimes=$willLock)');
setState(() { setState(() {
final newDays = List<WorkDay>.from(_days); final newDays = List<WorkDay>.from(_days);
newDays[dayIndex] = WorkDay( newDays[dayIndex] = WorkDay(
@ -693,7 +738,7 @@ class _MonthlyViewState extends State<MonthlyView> {
} }
}); });
_scheduleSave(dayIndex); // << neu: debounce statt sofort speichern _scheduleSave(dayIndex);
}, },
); );
} }
@ -715,6 +760,11 @@ class _MonthlyViewState extends State<MonthlyView> {
if (d.code != null && _lockCodes.contains(d.code)) return; if (d.code != null && _lockCodes.contains(d.code)) return;
// DEBUG
print('[monthly] TIME commit day=${ymd(d.date)} '
'slot=${slot + 1} ${isStart ? 'start' : 'end'}="$text" '
'parsed=${t != null ? fmtTimeOfDay(t) : '-'}');
final starts = List<TimeOfDay?>.filled(5, null); final starts = List<TimeOfDay?>.filled(5, null);
final ends = List<TimeOfDay?>.filled(5, null); final ends = List<TimeOfDay?>.filled(5, null);
@ -747,21 +797,20 @@ class _MonthlyViewState extends State<MonthlyView> {
_days = newDays; _days = newDays;
}); });
_scheduleSave(dayIndex); // << neu: debounce _scheduleSave(dayIndex);
} }
// --- NEU: Debounce + Save-Status-Handling --- // Debounce für Tages-Save
void _scheduleSave(int dayIndex) { void _scheduleSave(int dayIndex) {
// alten Timer stoppen
_saveTimers[dayIndex]?.cancel(); _saveTimers[dayIndex]?.cancel();
// neuen Timer setzen
_saveTimers[dayIndex] = Timer(const Duration(milliseconds: 500), () async { _saveTimers[dayIndex] = Timer(const Duration(milliseconds: 500), () async {
// DEBUG
print('[monthly] SAVE (debounced) row=$dayIndex date=${ymd(_days[dayIndex].date)}');
await _saveDay(dayIndex); await _saveDay(dayIndex);
}); });
} }
Future<void> _saveDay(int dayIndex) async { Future<void> _saveDay(int dayIndex) async {
// evtl. laufenden Debounce-Timer löschen (wir speichern jetzt)
_saveTimers[dayIndex]?.cancel(); _saveTimers[dayIndex]?.cancel();
_saveTimers.remove(dayIndex); _saveTimers.remove(dayIndex);
@ -773,7 +822,19 @@ class _MonthlyViewState extends State<MonthlyView> {
try { try {
final effective = _effectiveDay(dayIndex, _days[dayIndex]); final effective = _effectiveDay(dayIndex, _days[dayIndex]);
await _bookingApi.saveDay(effective);
// --- DEBUG: booking/saveDay Aufruf + Payload spiegeln ---
final payload = _debugDayPayload(effective);
print('[api] POST $_apiUrl');
print('[api] body=${jsonEncode(payload)}');
await _bookingApi.saveDay(effective); // tatsächlicher API-Call
// DEBUG
print('[api] booking/saveDay OK date=${ymd(effective.date)}');
// Folgemonat mitrechnen/speichern
await _saveMonthStartNow();
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
@ -789,12 +850,12 @@ class _MonthlyViewState extends State<MonthlyView> {
}); });
}); });
} catch (e) { } catch (e) {
print('[api] booking/saveDay ERROR: $e');
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_savingRows.remove(dayIndex); _savingRows.remove(dayIndex);
_rowSaveError[dayIndex] = e.toString(); _rowSaveError[dayIndex] = e.toString();
}); });
// optional non-intrusive Snack
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Speichern fehlgeschlagen: $e'), content: Text('Speichern fehlgeschlagen: $e'),
@ -805,6 +866,59 @@ class _MonthlyViewState extends State<MonthlyView> {
} }
} }
Future<void> _saveMonthStartNow() async {
// 1) Folgemonat bestimmen
final nextMonth = DateTime(_monthStart.year, _monthStart.month + 1, 1);
// 2) Effektive Tage (inkl. Eingabefelder berücksichtigen)
final effectiveDays =
List<WorkDay>.generate(_days.length, (i) => _effectiveDay(i, _days[i]));
// 3) Saldo des Monats berechnen (Start + Summe(IST - SOLL))
int saldoMin = _carryBaseMinutes;
for (final d in effectiveDays) {
final worked = _workedFor(d);
final target = d.targetMinutes;
saldoMin += worked - target;
}
// 4) Urlaubs-Übertrag berechnen
final startVacation = _monthStartInfo?.startVacationUnits ?? 0;
final usedVacation = _days.where((d) => d.code == 'U').length;
final carryVacation = startVacation - usedVacation;
// 5) Overtime/Correction aus Startdaten übernehmen
final overtime = _monthStartInfo?.overtimeMinutes ?? 0;
final correction = _monthStartInfo?.correctionMinutes ?? 0;
// --- DEBUG: monthlybooking/saveStart Aufruf + Payload spiegeln ---
final payload = {
'module': 'monthlybooking',
'function': 'saveStart',
'date':
'${nextMonth.year.toString().padLeft(4, '0')}-${nextMonth.month.toString().padLeft(2, '0')}-01',
'starthours': saldoMin,
'startvacation': carryVacation,
'overtime': overtime,
'correction': correction,
};
print('[api] POST $_apiUrl');
print('[api] body=${jsonEncode(payload)}');
try {
await _bookingApi.saveMonthStart(
nextMonth,
starthours: saldoMin,
startvacation: carryVacation,
overtime: overtime,
correction: correction,
);
print('[api] monthlybooking/saveStart OK for ${ymd(nextMonth)}');
} catch (e) {
print('[api] monthlybooking/saveStart ERROR: $e');
}
}
void _syncControllersWithDays() { void _syncControllersWithDays() {
for (int i = 0; i < _days.length; i++) { for (int i = 0; i < _days.length; i++) {
final lock = _days[i].code != null && _lockCodes.contains(_days[i].code); final lock = _days[i].code != null && _lockCodes.contains(_days[i].code);
@ -823,8 +937,12 @@ class _MonthlyViewState extends State<MonthlyView> {
: ''; : '';
final cs = _controllers[sKey]; final cs = _controllers[sKey];
final ce = _controllers[eKey]; final ce = _controllers[eKey];
if (cs != null && cs.text != sText && !(_focusNodes[sKey]?.hasFocus ?? false)) cs.text = sText; if (cs != null &&
if (ce != null && ce.text != eText && !(_focusNodes[eKey]?.hasFocus ?? false)) ce.text = eText; cs.text != sText &&
!(_focusNodes[sKey]?.hasFocus ?? false)) cs.text = sText;
if (ce != null &&
ce.text != eText &&
!(_focusNodes[eKey]?.hasFocus ?? false)) ce.text = eText;
} }
} }
} }
@ -866,8 +984,12 @@ class _MonthlyViewState extends State<MonthlyView> {
final sText = _controllers[sKey]?.text; final sText = _controllers[sKey]?.text;
final eText = _controllers[eKey]?.text; final eText = _controllers[eKey]?.text;
final s = (sText != null && sText.isNotEmpty) ? parseTextHHMM(sText) : baseSlot(base, slot, true); final s = (sText != null && sText.isNotEmpty)
final e = (eText != null && eText.isNotEmpty) ? parseTextHHMM(eText) : baseSlot(base, slot, false); ? parseTextHHMM(sText)
: baseSlot(base, slot, true);
final e = (eText != null && eText.isNotEmpty)
? parseTextHHMM(eText)
: baseSlot(base, slot, false);
if (s != null && e != null) intervals.add(WorkInterval(s, e)); if (s != null && e != null) intervals.add(WorkInterval(s, e));
} }
@ -929,6 +1051,40 @@ class _MonthlyViewState extends State<MonthlyView> {
if (a == Alignment.centerLeft) return TextAlign.left; if (a == Alignment.centerLeft) return TextAlign.left;
return TextAlign.center; return TextAlign.center;
} }
// ------------------------ DEBUG-Helper ------------------------
Map<String, dynamic> _debugDayPayload(WorkDay day) {
// Intervalle als 5 Slots wie im echten POST
final starts = List<String?>.filled(5, null);
final ends = List<String?>.filled(5, null);
for (int i = 0; i < day.intervals.length && i < 5; i++) {
starts[i] = fmtTimeOfDay(day.intervals[i].start);
ends[i] = fmtTimeOfDay(day.intervals[i].end);
}
final lock = _lockCodes.contains(day.code);
final y = day.date.year.toString().padLeft(4, '0');
final m = day.date.month.toString().padLeft(2, '0');
final d = day.date.day.toString().padLeft(2, '0');
return {
'module': 'booking',
'function': 'saveDay',
'date': '$y-$m-$d',
'code': day.code, // null wird als NULL gespeichert
'come1': lock ? null : starts[0],
'leave1': lock ? null : ends[0],
'come2': lock ? null : starts[1],
'leave2': lock ? null : ends[1],
'come3': lock ? null : starts[2],
'leave3': lock ? null : ends[2],
'come4': lock ? null : starts[3],
'leave4': lock ? null : ends[3],
'come5': lock ? null : starts[4],
'leave5': lock ? null : ends[4],
};
}
} }
/// Kopfzeile als separate DataTable (fixiert) /// Kopfzeile als separate DataTable (fixiert)
@ -969,16 +1125,26 @@ class _MonthHeader extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Row(children: [ child: Row(children: [
IconButton(onPressed: loading ? null : onPrev, icon: const Icon(Icons.chevron_left)), IconButton(
onPressed: loading ? null : onPrev,
icon: const Icon(Icons.chevron_left)),
Expanded( Expanded(
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?.copyWith(fontSize: 16)), label: Text(title,
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(fontSize: 16)),
), ),
), ),
IconButton(onPressed: loading ? null : onReload, icon: const Icon(Icons.refresh)), IconButton(
IconButton(onPressed: loading ? null : onNext, icon: const Icon(Icons.chevron_right)), onPressed: loading ? null : onReload,
icon: const Icon(Icons.refresh)),
IconButton(
onPressed: loading ? null : onNext,
icon: const Icon(Icons.chevron_right)),
]), ]),
); );
} }
@ -991,14 +1157,14 @@ class _MonthlySummaryFooter extends StatelessWidget {
// Linke Spalte (Strings bereits formatiert) // Linke Spalte (Strings bereits formatiert)
final String uebertragStartText; // Startsaldo (Minuten HH:MM) final String uebertragStartText; // Startsaldo (Minuten HH:MM)
final String sollText; // Summe Soll (HH:MM) final String sollText; // Summe Soll (HH:MM)
final String istText; // Summe Ist (HH:MM) final String istText; // Summe Ist (HH:MM)
final String correctionText; // Correction (HH:MM, ±) final String correctionText; // Correction (HH:MM, ±)
final String saldoText; // IST - SOLL (±HH:MM) final String saldoText; // IST - SOLL (±HH:MM)
final String paidOvertimeText; // folgt später () final String paidOvertimeText; // folgt später ()
final String uebertragNextText; // letzter Differenz gesamt (±HH:MM) final String uebertragNextText; // letzter Differenz gesamt (±HH:MM)
final String restUrlaubText; // startvacation (Zahl) final String restUrlaubText; // startvacation (Zahl)
final String urlaubUebertragText;// startvacation - used final String urlaubUebertragText; // startvacation - used
// Rechte Spalte (Counts) // Rechte Spalte (Counts)
final int countGleitzeit; final int countGleitzeit;
@ -1036,16 +1202,17 @@ class _MonthlySummaryFooter extends StatelessWidget {
// Monatslabels: Vormonat für Überträge/Resturlaub, aktueller Monat für Soll/Ist // Monatslabels: Vormonat für Überträge/Resturlaub, aktueller Monat für Soll/Ist
final prev = DateTime(month.year, month.month - 1, 1); final prev = DateTime(month.year, month.month - 1, 1);
final prevLabel = monthTitle(prev); final prevLabel = monthTitle(prev);
final curLabel = monthLabel; final curLabel = monthLabel;
// Layout-Konstanten // Layout-Konstanten
const double leftValueWidth = 120; // Werte-Breite links (rechtsbündig) const double leftValueWidth = 120; // Werte-Breite links (rechtsbündig)
const double rightValueWidth = 80; // Werte-Breite rechts (rechtsbündig) const double rightValueWidth = 80; // Werte-Breite rechts (rechtsbündig)
const double colGap = 24; // Abstand zwischen Spalten const double colGap = 24; // Abstand zwischen Spalten
const double rowGap = 2; // Zeilenabstand const double rowGap = 2; // Zeilenabstand
const double footerMaxWidth = 720; // maximale Footer-Breite const double footerMaxWidth = 720; // maximale Footer-Breite
final labelStyle = TextStyle(fontSize: fontSize, color: Theme.of(context).colorScheme.onSurface); final labelStyle = TextStyle(
fontSize: fontSize, color: Theme.of(context).colorScheme.onSurface);
final valueStyle = TextStyle( final valueStyle = TextStyle(
fontSize: fontSize, fontSize: fontSize,
fontFeatures: const [FontFeature.tabularFigures()], fontFeatures: const [FontFeature.tabularFigures()],
@ -1097,7 +1264,8 @@ class _MonthlySummaryFooter extends StatelessWidget {
width: leftValueWidth, width: leftValueWidth,
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Text(value, style: valueStyle, textAlign: TextAlign.right), child:
Text(value, style: valueStyle, textAlign: TextAlign.right),
), ),
), ),
], ],
@ -1116,7 +1284,8 @@ class _MonthlySummaryFooter extends StatelessWidget {
width: rightValueWidth, width: rightValueWidth,
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Text(value, style: valueStyle, textAlign: TextAlign.right), child:
Text(value, style: valueStyle, textAlign: TextAlign.right),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -1158,7 +1327,8 @@ class _MonthlySummaryFooter extends StatelessWidget {
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: rightItems.map((e) => rightRow(e.$1, e.$2)).toList(), children:
rightItems.map((e) => rightRow(e.$1, e.$2)).toList(),
), ),
), ),
], ],

@ -5,6 +5,10 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import file_picker
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
} }

@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -41,6 +49,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.18.0"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -49,6 +65,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "1.0.8"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -57,6 +81,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f
url: "https://pub.dev"
source: hosted
version: "10.3.3"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -70,11 +110,24 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "1c2b787f99bdca1f3718543f81d38aa1b124817dfeb9fb196201bea85b6134bf"
url: "https://pub.dev"
source: hosted
version: "2.0.26"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@ -147,6 +200,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.15.0" version: "1.15.0"
open_filex:
dependency: "direct main"
description:
name: open_filex
sha256: "9976da61b6a72302cf3b1efbce259200cd40232643a467aac7370addf94d6900"
url: "https://pub.dev"
source: hosted
version: "4.7.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -155,6 +216,78 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
url: "https://pub.dev"
source: hosted
version: "2.2.15"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -240,6 +373,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
url: "https://pub.dev"
source: hosted
version: "5.10.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks: sdks:
dart: ">=3.5.4 <4.0.0" dart: ">=3.5.4 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54" flutter: ">=3.24.0"

@ -35,7 +35,10 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
http: ^1.5.0 http: any
path_provider: any
open_filex: any
file_picker: any
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

Loading…
Cancel
Save