|
|
|
|
@ -52,16 +52,32 @@ class _MonthlyViewState extends State<MonthlyView> {
|
|
|
|
|
DateTime.sunday: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
late final ScrollController _hCtrl;
|
|
|
|
|
late final ScrollController _vCtrl;
|
|
|
|
|
// Scroll-Controller
|
|
|
|
|
late final ScrollController _hHeaderCtrl; // Header (nur mitgeführt)
|
|
|
|
|
late final ScrollController _hBodyCtrl; // Body (führend)
|
|
|
|
|
late final ScrollController _vCtrl; // Body vertikal
|
|
|
|
|
bool _syncingH = false;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
final now = DateTime.now();
|
|
|
|
|
_monthStart = DateTime(now.year, now.month, 1);
|
|
|
|
|
_hCtrl = ScrollController();
|
|
|
|
|
|
|
|
|
|
_hHeaderCtrl = ScrollController();
|
|
|
|
|
_hBodyCtrl = ScrollController();
|
|
|
|
|
_vCtrl = ScrollController();
|
|
|
|
|
|
|
|
|
|
// Body scrollt -> Header folgt
|
|
|
|
|
_hBodyCtrl.addListener(() {
|
|
|
|
|
if (_syncingH) return;
|
|
|
|
|
_syncingH = true;
|
|
|
|
|
if (_hHeaderCtrl.hasClients) {
|
|
|
|
|
_hHeaderCtrl.jumpTo(_hBodyCtrl.offset);
|
|
|
|
|
}
|
|
|
|
|
_syncingH = false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_client = http.Client();
|
|
|
|
|
_bookingApi = BookingApi(client: _client);
|
|
|
|
|
_dailyApi = DailyWorkingApi(client: _client);
|
|
|
|
|
@ -76,7 +92,8 @@ class _MonthlyViewState extends State<MonthlyView> {
|
|
|
|
|
for (final c in _controllers.values) {
|
|
|
|
|
c.dispose();
|
|
|
|
|
}
|
|
|
|
|
_hCtrl.dispose();
|
|
|
|
|
_hHeaderCtrl.dispose();
|
|
|
|
|
_hBodyCtrl.dispose();
|
|
|
|
|
_vCtrl.dispose();
|
|
|
|
|
_client.close();
|
|
|
|
|
super.dispose();
|
|
|
|
|
@ -235,42 +252,34 @@ class _MonthlyViewState extends State<MonthlyView> {
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// FIXIERTE KOPFZEILE
|
|
|
|
|
Scrollbar(
|
|
|
|
|
controller: _hCtrl,
|
|
|
|
|
notificationPredicate: (n) => n.metrics.axis == Axis.horizontal,
|
|
|
|
|
thumbVisibility: true,
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
controller: _hCtrl,
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
child: ConstrainedBox(
|
|
|
|
|
constraints: BoxConstraints(minWidth: _tableMinWidth),
|
|
|
|
|
child: DataTableTheme(
|
|
|
|
|
data: DataTableThemeData(
|
|
|
|
|
headingRowHeight: 30,
|
|
|
|
|
columnSpacing: 10,
|
|
|
|
|
headingTextStyle: const TextStyle(fontWeight: FontWeight.w700).copyWith(
|
|
|
|
|
fontSize: _fontSize,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
child: DataTable(
|
|
|
|
|
showCheckboxColumn: false,
|
|
|
|
|
columns: headerColumns,
|
|
|
|
|
rows: const <DataRow>[],
|
|
|
|
|
// FIXIERTE KOPFZEILE (mitgeführt, nicht interaktiv)
|
|
|
|
|
SingleChildScrollView(
|
|
|
|
|
controller: _hHeaderCtrl,
|
|
|
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
child: ConstrainedBox(
|
|
|
|
|
constraints: BoxConstraints(minWidth: _tableMinWidth),
|
|
|
|
|
child: DataTableTheme(
|
|
|
|
|
data: DataTableThemeData(
|
|
|
|
|
headingRowHeight: 30,
|
|
|
|
|
columnSpacing: 10,
|
|
|
|
|
headingTextStyle: const TextStyle(fontWeight: FontWeight.w700).copyWith(
|
|
|
|
|
fontSize: _fontSize,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
child: const _HeaderOnlyDataTable(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// BODY
|
|
|
|
|
// BODY (horiz. & vert. Scroll; horizontale Scrollbar hier)
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Scrollbar(
|
|
|
|
|
controller: _hCtrl,
|
|
|
|
|
controller: _hBodyCtrl,
|
|
|
|
|
notificationPredicate: (n) => n.metrics.axis == Axis.horizontal,
|
|
|
|
|
thumbVisibility: true,
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
controller: _hCtrl,
|
|
|
|
|
controller: _hBodyCtrl,
|
|
|
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
child: ConstrainedBox(
|
|
|
|
|
@ -283,7 +292,7 @@ class _MonthlyViewState extends State<MonthlyView> {
|
|
|
|
|
controller: _vCtrl,
|
|
|
|
|
child: DataTableTheme(
|
|
|
|
|
data: const DataTableThemeData(
|
|
|
|
|
headingRowHeight: 0,
|
|
|
|
|
headingRowHeight: 0, // Header im Body ausblenden
|
|
|
|
|
dataRowMinHeight: 30,
|
|
|
|
|
dataRowMaxHeight: 34,
|
|
|
|
|
columnSpacing: 10,
|
|
|
|
|
@ -498,6 +507,15 @@ class _MonthlyViewState extends State<MonthlyView> {
|
|
|
|
|
return cells;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// kleines Mono-Text-Widget für Zahlen
|
|
|
|
|
Widget _monoSmall(String s) => Text(
|
|
|
|
|
s,
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: _fontSize,
|
|
|
|
|
fontFeatures: [FontFeature.tabularFigures()],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Widget _codeDropdown(int dayIndex, WorkDay day) {
|
|
|
|
|
final value = day.code;
|
|
|
|
|
final values = <String?>[null, ...kAbsenceCodes];
|
|
|
|
|
@ -672,14 +690,6 @@ class _MonthlyViewState extends State<MonthlyView> {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _monoSmall(String s) => Text(
|
|
|
|
|
s,
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: _fontSize,
|
|
|
|
|
fontFeatures: [FontFeature.tabularFigures()],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
TextAlign _toTextAlign(Alignment a) {
|
|
|
|
|
if (a == Alignment.centerRight) return TextAlign.right;
|
|
|
|
|
if (a == Alignment.centerLeft) return TextAlign.left;
|
|
|
|
|
@ -687,6 +697,23 @@ Widget _monoSmall(String s) => Text(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Minimaler Header-Table, damit die Kopfzeile fixiert bleiben kann.
|
|
|
|
|
/// Nutzt die gleichen Spalten wie der Body, aber ohne Rows.
|
|
|
|
|
class _HeaderOnlyDataTable extends StatelessWidget {
|
|
|
|
|
const _HeaderOnlyDataTable();
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final state = context.findAncestorStateOfType<_MonthlyViewState>()!;
|
|
|
|
|
final cols = state._buildColumns();
|
|
|
|
|
return DataTable(
|
|
|
|
|
showCheckboxColumn: false,
|
|
|
|
|
columns: cols,
|
|
|
|
|
rows: const <DataRow>[],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _MonthHeader extends StatelessWidget {
|
|
|
|
|
final DateTime month;
|
|
|
|
|
final VoidCallback onPrev;
|
|
|
|
|
|