|
|
|
@ -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,10 +73,11 @@ 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 =
|
|
|
|
|
|
|
|
{}; // Zeilen, die eben gespeichert haben (Häkchen kurz anzeigen)
|
|
|
|
final Map<int, String> _rowSaveError = {}; // Zeilenfehler
|
|
|
|
final Map<int, String> _rowSaveError = {}; // Zeilenfehler
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
@override
|
|
|
|
@ -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;
|
|
|
|
@ -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
|
|
|
|
@ -207,7 +228,8 @@ class _MonthlyViewState extends State<MonthlyView> {
|
|
|
|
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(
|
|
|
|
|
|
|
|
int dayIndex, WorkDay day, int runningDiff) {
|
|
|
|
final leftLabel = rightDayLabel(day.date); // "Mo 01.09."
|
|
|
|
final leftLabel = rightDayLabel(day.date); // "Mo 01.09."
|
|
|
|
final rightLabel = leftDayLabel(day.date); // "01.09. Mo"
|
|
|
|
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)),
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -998,7 +1164,7 @@ class _MonthlySummaryFooter extends StatelessWidget {
|
|
|
|
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;
|
|
|
|
@ -1045,7 +1211,8 @@ class _MonthlySummaryFooter extends StatelessWidget {
|
|
|
|
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(),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
],
|
|
|
|
|