Monthy calculation
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/work_day.dart';
|
||||
|
||||
+244
-74
@@ -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 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
@@ -27,6 +28,9 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
static const double _wNumber = 76;
|
||||
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
|
||||
static const Set<String> _lockCodes = {'G', 'K', 'U', 'SU'};
|
||||
|
||||
@@ -34,7 +38,8 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
final Set<String> _invalidCells = <String>{};
|
||||
final Map<String, FocusNode> _focusNodes = {};
|
||||
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) =>
|
||||
_controllers.putIfAbsent(key, () => TextEditingController(text: initial));
|
||||
|
||||
@@ -68,11 +73,12 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
late final ScrollController _vCtrl;
|
||||
bool _syncingH = false;
|
||||
|
||||
// --- NEU: Debounce + Save-Status pro Zeile ---
|
||||
final Map<int, Timer> _saveTimers = {}; // rowIndex -> Timer
|
||||
final Set<int> _savingRows = {}; // Zeilen, die gerade speichern
|
||||
final Set<int> _justSavedRows = {}; // Zeilen, die eben gespeichert haben (Häkchen kurz anzeigen)
|
||||
final Map<int, String> _rowSaveError = {}; // Zeilenfehler
|
||||
// Save-Status pro Zeile
|
||||
final Map<int, Timer> _saveTimers = {}; // rowIndex -> Timer
|
||||
final Set<int> _savingRows = {}; // Zeilen, die gerade speichern
|
||||
final Set<int> _justSavedRows =
|
||||
{}; // Zeilen, die eben gespeichert haben (Häkchen kurz anzeigen)
|
||||
final Map<int, String> _rowSaveError = {}; // Zeilenfehler
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -118,6 +124,9 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
}
|
||||
|
||||
Future<void> _loadMonth(DateTime m) async {
|
||||
// DEBUG
|
||||
print('[monthly] LOAD month=${m.year}-${m.month.toString().padLeft(2, '0')}');
|
||||
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
@@ -125,14 +134,14 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
});
|
||||
try {
|
||||
final results = await Future.wait([
|
||||
_bookingApi.getBookingList(m), // List<WorkDay>
|
||||
_dailyApi.getDailyMinutes(), // Map<int,int>
|
||||
_bookingApi.getMonthStart(m), // MonthStart
|
||||
_bookingApi.getBookingList(m), // List<WorkDay>
|
||||
_dailyApi.getDailyMinutes(), // Map<int,int>
|
||||
_bookingApi.getMonthStart(m), // MonthStart
|
||||
]);
|
||||
|
||||
final apiDays = results[0] as List<WorkDay>;
|
||||
final plan = results[1] as Map<int, int>;
|
||||
final mStart = results[2] as MonthStart;
|
||||
final plan = results[1] as Map<int, int>;
|
||||
final mStart = results[2] as MonthStart;
|
||||
|
||||
final holidayMap = buildHolidayMapAT(m.year);
|
||||
|
||||
@@ -140,10 +149,12 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
final withTargets = filled.map((d) {
|
||||
final isHoliday = holidayMap.containsKey(ymd(d.date));
|
||||
// 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
|
||||
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(
|
||||
date: d.date,
|
||||
intervals: d.intervals,
|
||||
@@ -158,7 +169,8 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
_days = withTargets;
|
||||
_dailyPlan = plan;
|
||||
_monthStartInfo = mStart;
|
||||
_carryBaseMinutes = mStart.carryBaseMinutes; // starthours + overtime + correction
|
||||
_carryBaseMinutes =
|
||||
mStart.carryBaseMinutes; // starthours + overtime + correction
|
||||
_loading = false;
|
||||
|
||||
// Status-Maps leeren (neuer Monat)
|
||||
@@ -167,8 +179,16 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
_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();
|
||||
} catch (e) {
|
||||
print('[monthly] LOAD ERROR: $e');
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_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) {
|
||||
case 'G':
|
||||
return const Color(0xFFBFBFFF); // Gleitzeit
|
||||
@@ -202,12 +223,13 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
case 'SU':
|
||||
return const Color(0xFF7F7FFF); // Sonderurlaub
|
||||
case 'K':
|
||||
return Colors.yellow; // Krankenstand
|
||||
return Colors.yellow; // Krankenstand
|
||||
case 'T':
|
||||
return Colors.red; // Training
|
||||
return Colors.red; // Training
|
||||
}
|
||||
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 (isWeekend) return weekendBg;
|
||||
return null;
|
||||
@@ -215,7 +237,6 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final headerColumns = _buildColumns();
|
||||
final bodyColumns = _buildColumns();
|
||||
|
||||
final theme = Theme.of(context);
|
||||
@@ -223,7 +244,8 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
final weekendBg = Colors.grey.withOpacity(0.30);
|
||||
|
||||
// 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)
|
||||
final diffs = <int>[];
|
||||
@@ -257,7 +279,8 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
};
|
||||
|
||||
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 day = effectiveDays[i];
|
||||
@@ -274,8 +297,10 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
_MonthHeader(
|
||||
month: _monthStart,
|
||||
loading: _loading,
|
||||
onPrev: () => _loadMonth(DateTime(_monthStart.year, _monthStart.month - 1, 1)),
|
||||
onNext: () => _loadMonth(DateTime(_monthStart.year, _monthStart.month + 1, 1)),
|
||||
onPrev: () =>
|
||||
_loadMonth(DateTime(_monthStart.year, _monthStart.month - 1, 1)),
|
||||
onNext: () =>
|
||||
_loadMonth(DateTime(_monthStart.year, _monthStart.month + 1, 1)),
|
||||
onPickMonth: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
@@ -300,8 +325,10 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text('Fehler beim Laden:', style: TextStyle(fontSize: _fontSize)),
|
||||
SelectableText(_error ?? '', style: const TextStyle(fontSize: _fontSize)),
|
||||
Text('Fehler beim Laden:',
|
||||
style: TextStyle(fontSize: _fontSize)),
|
||||
SelectableText(_error ?? '',
|
||||
style: const TextStyle(fontSize: _fontSize)),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
@@ -324,7 +351,8 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
data: DataTableThemeData(
|
||||
headingRowHeight: 30,
|
||||
columnSpacing: 10,
|
||||
headingTextStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: _fontSize),
|
||||
headingTextStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w700, fontSize: _fontSize),
|
||||
),
|
||||
child: const _HeaderOnlyDataTable(),
|
||||
),
|
||||
@@ -397,7 +425,9 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
}
|
||||
|
||||
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(
|
||||
width: width,
|
||||
child: Align(
|
||||
@@ -433,14 +463,16 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
];
|
||||
}
|
||||
|
||||
List<DataCell> _buildEditableCells(int dayIndex, WorkDay day, int runningDiff) {
|
||||
final leftLabel = rightDayLabel(day.date); // "Mo 01.09."
|
||||
final rightLabel = leftDayLabel(day.date); // "01.09. Mo"
|
||||
List<DataCell> _buildEditableCells(
|
||||
int dayIndex, WorkDay day, int runningDiff) {
|
||||
final leftLabel = rightDayLabel(day.date); // "Mo 01.09."
|
||||
final rightLabel = leftDayLabel(day.date); // "01.09. Mo"
|
||||
final hName = _holidays[ymd(day.date)] ?? '';
|
||||
|
||||
final bool lockTimes = day.code != null && _lockCodes.contains(day.code);
|
||||
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;
|
||||
|
||||
String slotText(int slot, bool isStart) {
|
||||
@@ -480,7 +512,7 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
),
|
||||
),
|
||||
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(
|
||||
width: _wNumber,
|
||||
child: Align(alignment: Alignment.centerRight, child: _monoSmall(diffSum)),
|
||||
child:
|
||||
Align(alignment: Alignment.centerRight, child: _monoSmall(diffSum)),
|
||||
)));
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -628,7 +662,7 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
),
|
||||
);
|
||||
|
||||
// --- NEU: kleiner Status-Badge pro Zeile ---
|
||||
// kleiner Status-Badge pro Zeile
|
||||
Widget _rowStatusBadge(int row) {
|
||||
if (_savingRows.contains(row)) {
|
||||
return const SizedBox(
|
||||
@@ -638,7 +672,8 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
);
|
||||
}
|
||||
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)) {
|
||||
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
|
||||
return DropdownMenuItem<String?>(
|
||||
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(),
|
||||
selectedItemBuilder: (context) {
|
||||
return values.map((v) {
|
||||
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();
|
||||
},
|
||||
onChanged: disabled
|
||||
? null
|
||||
: (newCode) {
|
||||
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(() {
|
||||
final newDays = List<WorkDay>.from(_days);
|
||||
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;
|
||||
|
||||
// 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 ends = List<TimeOfDay?>.filled(5, null);
|
||||
|
||||
@@ -747,21 +797,20 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
_days = newDays;
|
||||
});
|
||||
|
||||
_scheduleSave(dayIndex); // << neu: debounce
|
||||
_scheduleSave(dayIndex);
|
||||
}
|
||||
|
||||
// --- NEU: Debounce + Save-Status-Handling ---
|
||||
// Debounce für Tages-Save
|
||||
void _scheduleSave(int dayIndex) {
|
||||
// alten Timer stoppen
|
||||
_saveTimers[dayIndex]?.cancel();
|
||||
// neuen Timer setzen
|
||||
_saveTimers[dayIndex] = Timer(const Duration(milliseconds: 500), () async {
|
||||
// DEBUG
|
||||
print('[monthly] SAVE (debounced) row=$dayIndex date=${ymd(_days[dayIndex].date)}');
|
||||
await _saveDay(dayIndex);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveDay(int dayIndex) async {
|
||||
// evtl. laufenden Debounce-Timer löschen (wir speichern jetzt)
|
||||
_saveTimers[dayIndex]?.cancel();
|
||||
_saveTimers.remove(dayIndex);
|
||||
|
||||
@@ -773,7 +822,19 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
|
||||
try {
|
||||
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;
|
||||
setState(() {
|
||||
@@ -789,12 +850,12 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
print('[api] booking/saveDay ERROR: $e');
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_savingRows.remove(dayIndex);
|
||||
_rowSaveError[dayIndex] = e.toString();
|
||||
});
|
||||
// optional non-intrusive Snack
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
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() {
|
||||
for (int i = 0; i < _days.length; i++) {
|
||||
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 ce = _controllers[eKey];
|
||||
if (cs != null && cs.text != sText && !(_focusNodes[sKey]?.hasFocus ?? false)) cs.text = sText;
|
||||
if (ce != null && ce.text != eText && !(_focusNodes[eKey]?.hasFocus ?? false)) ce.text = eText;
|
||||
if (cs != null &&
|
||||
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 eText = _controllers[eKey]?.text;
|
||||
|
||||
final s = (sText != null && sText.isNotEmpty) ? parseTextHHMM(sText) : baseSlot(base, slot, true);
|
||||
final e = (eText != null && eText.isNotEmpty) ? parseTextHHMM(eText) : baseSlot(base, slot, false);
|
||||
final s = (sText != null && sText.isNotEmpty)
|
||||
? 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));
|
||||
}
|
||||
@@ -929,6 +1051,40 @@ class _MonthlyViewState extends State<MonthlyView> {
|
||||
if (a == Alignment.centerLeft) return TextAlign.left;
|
||||
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)
|
||||
@@ -969,16 +1125,26 @@ class _MonthHeader extends StatelessWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
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(
|
||||
child: TextButton.icon(
|
||||
onPressed: loading ? null : onPickMonth,
|
||||
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(onPressed: loading ? null : onNext, icon: const Icon(Icons.chevron_right)),
|
||||
IconButton(
|
||||
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)
|
||||
final String uebertragStartText; // Startsaldo (Minuten → HH:MM)
|
||||
final String sollText; // Summe Soll (HH:MM)
|
||||
final String istText; // Summe Ist (HH:MM)
|
||||
final String correctionText; // Correction (HH:MM, ±)
|
||||
final String saldoText; // IST - SOLL (±HH:MM)
|
||||
final String paidOvertimeText; // folgt später (—)
|
||||
final String uebertragNextText; // letzter „Differenz gesamt“ (±HH:MM)
|
||||
final String restUrlaubText; // startvacation (Zahl)
|
||||
final String urlaubUebertragText;// startvacation - used
|
||||
final String sollText; // Summe Soll (HH:MM)
|
||||
final String istText; // Summe Ist (HH:MM)
|
||||
final String correctionText; // Correction (HH:MM, ±)
|
||||
final String saldoText; // IST - SOLL (±HH:MM)
|
||||
final String paidOvertimeText; // folgt später (—)
|
||||
final String uebertragNextText; // letzter „Differenz gesamt“ (±HH:MM)
|
||||
final String restUrlaubText; // startvacation (Zahl)
|
||||
final String urlaubUebertragText; // startvacation - used
|
||||
|
||||
// Rechte Spalte (Counts)
|
||||
final int countGleitzeit;
|
||||
@@ -1036,16 +1202,17 @@ class _MonthlySummaryFooter extends StatelessWidget {
|
||||
// Monatslabels: Vormonat für Überträge/Resturlaub, aktueller Monat für Soll/Ist
|
||||
final prev = DateTime(month.year, month.month - 1, 1);
|
||||
final prevLabel = monthTitle(prev);
|
||||
final curLabel = monthLabel;
|
||||
final curLabel = monthLabel;
|
||||
|
||||
// Layout-Konstanten
|
||||
const double leftValueWidth = 120; // Werte-Breite links (rechtsbündig)
|
||||
const double rightValueWidth = 80; // Werte-Breite rechts (rechtsbündig)
|
||||
const double colGap = 24; // Abstand zwischen Spalten
|
||||
const double rowGap = 2; // Zeilenabstand
|
||||
const double footerMaxWidth = 720; // maximale Footer-Breite
|
||||
const double leftValueWidth = 120; // Werte-Breite links (rechtsbündig)
|
||||
const double rightValueWidth = 80; // Werte-Breite rechts (rechtsbündig)
|
||||
const double colGap = 24; // Abstand zwischen Spalten
|
||||
const double rowGap = 2; // Zeilenabstand
|
||||
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(
|
||||
fontSize: fontSize,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
@@ -1097,7 +1264,8 @@ class _MonthlySummaryFooter extends StatelessWidget {
|
||||
width: leftValueWidth,
|
||||
child: Align(
|
||||
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,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(value, style: valueStyle, textAlign: TextAlign.right),
|
||||
child:
|
||||
Text(value, style: valueStyle, textAlign: TextAlign.right),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -1158,7 +1327,8 @@ class _MonthlySummaryFooter extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Column(
|
||||
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 Foundation
|
||||
|
||||
import file_picker
|
||||
import path_provider_foundation
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
}
|
||||
|
||||
+158
-1
@@ -1,6 +1,14 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -41,6 +49,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -49,6 +65,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -57,6 +81,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -70,11 +110,24 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -147,6 +200,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -155,6 +216,78 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -240,6 +373,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dart: ">=3.5.4 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
flutter: ">=3.24.0"
|
||||
|
||||
+4
-1
@@ -35,7 +35,10 @@ dependencies:
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
http: ^1.5.0
|
||||
http: any
|
||||
path_provider: any
|
||||
open_filex: any
|
||||
file_picker: any
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user