footer
This commit is contained in:
+73
-93
@@ -1,119 +1,99 @@
|
||||
import 'dart:ui' show FontFeature;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../models/work_day.dart';
|
||||
|
||||
String monthTitle(DateTime m) {
|
||||
const months = [
|
||||
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
|
||||
];
|
||||
return '${months[m.month - 1]} ${m.year}';
|
||||
}
|
||||
/// Kurzcode-Liste & Label
|
||||
const List<String> kAbsenceCodes = ['G', 'K', 'U', 'SU', 'T'];
|
||||
|
||||
String leftDayLabel(DateTime d) => '${ddmm(d)} ${weekdayShort(d)}';
|
||||
String rightDayLabel(DateTime d) => '${weekdayShort(d)} ${ddmm(d)}';
|
||||
|
||||
String weekdayShort(DateTime d) {
|
||||
switch (d.weekday) {
|
||||
case DateTime.monday: return 'Mo';
|
||||
case DateTime.tuesday: return 'Di';
|
||||
case DateTime.wednesday: return 'Mi';
|
||||
case DateTime.thursday: return 'Do';
|
||||
case DateTime.friday: return 'Fr';
|
||||
case DateTime.saturday: return 'Sa';
|
||||
case DateTime.sunday: return 'So';
|
||||
default: return '';
|
||||
String codeLabel(String code) {
|
||||
switch (code) {
|
||||
case 'G': return 'Gleitzeit';
|
||||
case 'K': return 'Krankenstand';
|
||||
case 'U': return 'Urlaub';
|
||||
case 'SU': return 'Sonderurlaub';
|
||||
case 'T': return 'Training';
|
||||
default: return code;
|
||||
}
|
||||
}
|
||||
|
||||
String ddmm(DateTime d) =>
|
||||
'${d.day.toString().padLeft(2, '0')}.${d.month.toString().padLeft(2, '0')}';
|
||||
|
||||
Widget mono(String s) =>
|
||||
Text(s, style: const TextStyle(fontFeatures: [FontFeature.tabularFigures()]));
|
||||
|
||||
String fmtTimeOfDay(TimeOfDay t) =>
|
||||
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
String minutesToHHMM(int minutes) {
|
||||
final h = minutes ~/ 60;
|
||||
final m = minutes % 60;
|
||||
return '${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String minutesToSignedHHMM(int minutes) {
|
||||
final sign = minutes < 0 ? '-' : '+';
|
||||
final absMin = minutes.abs();
|
||||
final h = absMin ~/ 60;
|
||||
final m = absMin % 60;
|
||||
return '$sign${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
TimeOfDay? parseHHMM(dynamic v) {
|
||||
if (v == null) return null;
|
||||
final s = v.toString().trim();
|
||||
if (s.isEmpty) return null;
|
||||
final parts = s.split(':');
|
||||
/// Zeit-Helfer
|
||||
TimeOfDay? parseDbTime(String? hhmmss) {
|
||||
if (hhmmss == null || hhmmss.isEmpty) return null;
|
||||
final parts = hhmmss.split(':');
|
||||
if (parts.length < 2) return null;
|
||||
final h = int.tryParse(parts[0]) ?? 0;
|
||||
final m = int.tryParse(parts[1]) ?? 0;
|
||||
if (h < 0 || h > 23 || m < 0 || m > 59) return null;
|
||||
return TimeOfDay(hour: h, minute: m);
|
||||
}
|
||||
|
||||
String fmtTimeOfDay(TimeOfDay? t) {
|
||||
if (t == null) return '';
|
||||
final hh = t.hour.toString().padLeft(2, '0');
|
||||
final mm = t.minute.toString().padLeft(2, '0');
|
||||
return '$hh:$mm';
|
||||
}
|
||||
|
||||
TimeOfDay? parseTextHHMM(String s) {
|
||||
final t = s.trim();
|
||||
if (t.length != 5 || t[2] != ':') return null;
|
||||
final h = int.tryParse(t.substring(0, 2));
|
||||
final m = int.tryParse(t.substring(3, 5));
|
||||
if (s.length != 5 || s[2] != ':') return null;
|
||||
final h = int.tryParse(s.substring(0, 2));
|
||||
final m = int.tryParse(s.substring(3, 5));
|
||||
if (h == null || m == null) return null;
|
||||
if (h < 0 || h > 23 || m < 0 || m > 59) return null;
|
||||
return TimeOfDay(hour: h, minute: m);
|
||||
}
|
||||
|
||||
String minutesToHHMM(int minutes) {
|
||||
final sign = minutes < 0 ? '-' : '';
|
||||
final m = minutes.abs();
|
||||
final hh = (m ~/ 60).toString().padLeft(2, '0');
|
||||
final mm = (m % 60).toString().padLeft(2, '0');
|
||||
return '$sign$hh:$mm';
|
||||
}
|
||||
|
||||
String minutesToSignedHHMM(int minutes) => minutesToHHMM(minutes);
|
||||
|
||||
/// Datums-Formatierungen
|
||||
String ymd(DateTime d) =>
|
||||
'${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
|
||||
|
||||
// Füllt alle Tage des Monats auf
|
||||
List<WorkDay> fillMonth(DateTime monthStart, List<WorkDay> existing) {
|
||||
final map = <String, WorkDay>{for (final w in existing) ymd(w.date): w};
|
||||
final nextMonth = DateTime(monthStart.year, monthStart.month + 1, 1);
|
||||
final out = <WorkDay>[];
|
||||
for (DateTime d = monthStart; d.isBefore(nextMonth); d = d.add(const Duration(days: 1))) {
|
||||
final key = ymd(d);
|
||||
final wd = map[key];
|
||||
if (wd != null) {
|
||||
out.add(wd);
|
||||
const _weekdayShort = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
|
||||
String _shortWeekday(DateTime d) => _weekdayShort[(d.weekday - 1) % 7];
|
||||
String _shortDate(DateTime d) =>
|
||||
'${d.day.toString().padLeft(2, '0')}.${d.month.toString().padLeft(2, '0')}.';
|
||||
|
||||
/// Links in der Tabelle (Tag vor Datum)
|
||||
String rightDayLabel(DateTime d) => '${_shortWeekday(d)} ${_shortDate(d)}';
|
||||
|
||||
/// Rechts in der Tabelle (Datum vor Tag)
|
||||
String leftDayLabel(DateTime d) => '${_shortDate(d)} ${_shortWeekday(d)}';
|
||||
|
||||
const _monthNames = [
|
||||
'Jänner', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
|
||||
];
|
||||
|
||||
String monthTitle(DateTime m) => '${_monthNames[m.month - 1]} ${m.year}';
|
||||
|
||||
/// Füllt den kompletten Monat mit leeren Tagen, ersetzt vorhandene Einträge
|
||||
List<WorkDay> fillMonth(DateTime monthStart, List<WorkDay> fromApi) {
|
||||
final m0 = DateTime(monthStart.year, monthStart.month, 1);
|
||||
final m1 = DateTime(monthStart.year, monthStart.month + 1, 1);
|
||||
final byDay = <String, WorkDay>{for (final d in fromApi) ymd(d.date): d};
|
||||
|
||||
final days = <WorkDay>[];
|
||||
for (DateTime d = m0; d.isBefore(m1); d = d.add(const Duration(days: 1))) {
|
||||
final k = ymd(d);
|
||||
if (byDay.containsKey(k)) {
|
||||
final v = byDay[k]!;
|
||||
days.add(WorkDay(
|
||||
date: d,
|
||||
intervals: v.intervals,
|
||||
targetMinutes: v.targetMinutes,
|
||||
code: v.code,
|
||||
));
|
||||
} else {
|
||||
final isWeekend = d.weekday == DateTime.saturday || d.weekday == DateTime.sunday;
|
||||
final target = isWeekend ? 0 : 8 * 60;
|
||||
out.add(WorkDay(date: d, intervals: const [], targetMinutes: target));
|
||||
days.add(WorkDay(date: d, intervals: const [], targetMinutes: 0, code: null));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// "HH:MM:SS" → Minuten (Sekunden ignoriert). Ungültig => null.
|
||||
int? minutesFromHHMMSS(String? s) {
|
||||
if (s == null) return null;
|
||||
final parts = s.split(':');
|
||||
if (parts.length != 3) return null;
|
||||
final h = int.tryParse(parts[0]) ?? 0;
|
||||
final m = int.tryParse(parts[1]) ?? 0;
|
||||
return h * 60 + m;
|
||||
}
|
||||
|
||||
// Abwesenheitscodes und Labels
|
||||
const List<String> kAbsenceCodes = ['G', 'K', 'U', 'SU', 'T'];
|
||||
|
||||
String codeLabel(String? code) {
|
||||
switch (code) {
|
||||
case 'G': return 'Gleitzeit';
|
||||
case 'K': return 'Krankenstand';
|
||||
case 'U': return 'Urlaub';
|
||||
case 'SU': return 'Sonderurlaub';
|
||||
case 'T': return 'Training';
|
||||
default: return '—';
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import '../utils/helpers.dart';
|
||||
|
||||
/// Sehr vereinfachte österreichische Feiertage (bundesweit).
|
||||
/// Key = 'YYYY-MM-DD', Value = Name.
|
||||
Map<String, String> buildHolidayMapAT(int year) {
|
||||
final Map<String, String> m = {};
|
||||
|
||||
DateTime easter = _easterSunday(year);
|
||||
DateTime easterMon = easter.add(const Duration(days: 1));
|
||||
DateTime ascension = easter.add(const Duration(days: 39));
|
||||
DateTime whitMon = easter.add(const Duration(days: 50));
|
||||
DateTime corpusChristi = easter.add(const Duration(days: 60));
|
||||
|
||||
void add(DateTime d, String name) => m[ymd(d)] = name;
|
||||
|
||||
add(DateTime(year, 1, 1), 'Neujahr');
|
||||
add(DateTime(year, 1, 6), 'Heilige Drei Könige');
|
||||
add(easterMon, 'Ostermontag');
|
||||
add(DateTime(year, 5, 1), 'Staatsfeiertag');
|
||||
add(ascension, 'Christi Himmelfahrt');
|
||||
add(whitMon, 'Pfingstmontag');
|
||||
add(corpusChristi, 'Fronleichnam');
|
||||
add(DateTime(year, 8, 15), 'Mariä Himmelfahrt');
|
||||
add(DateTime(year, 10, 26), 'Nationalfeiertag');
|
||||
add(DateTime(year, 11, 1), 'Allerheiligen');
|
||||
@@ -14,15 +27,10 @@ Map<String, String> buildHolidayMapAT(int year) {
|
||||
add(DateTime(year, 12, 25), 'Christtag');
|
||||
add(DateTime(year, 12, 26), 'Stefanitag');
|
||||
|
||||
final easter = _easterSunday(year);
|
||||
add(easter.add(const Duration(days: 1)), 'Ostermontag');
|
||||
add(easter.add(const Duration(days: 39)), 'Christi Himmelfahrt');
|
||||
add(easter.add(const Duration(days: 50)), 'Pfingstmontag');
|
||||
add(easter.add(const Duration(days: 60)), 'Fronleichnam');
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
/// Gaußsche Osterformel (Gregorianisch)
|
||||
DateTime _easterSunday(int year) {
|
||||
final a = year % 19;
|
||||
final b = year ~/ 100;
|
||||
@@ -36,7 +44,7 @@ DateTime _easterSunday(int year) {
|
||||
final k = c % 4;
|
||||
final l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||||
final m = (a + 11 * h + 22 * l) ~/ 451;
|
||||
final month = (h + l - 7 * m + 114) ~/ 31; // 3=March, 4=April
|
||||
final month = (h + l - 7 * m + 114) ~/ 31; // 3=March, 4=April
|
||||
final day = ((h + l - 7 * m + 114) % 31) + 1;
|
||||
return DateTime(year, month, day);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,57 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Tippt „HHMM“ und formatiert live zu „HH:MM“ (nur Ziffern erlaubt).
|
||||
/// Formatiert Eingaben als HH:mm (max. 5 Zeichen),
|
||||
/// lässt nur Ziffern und optional ":" an Pos 2 zu.
|
||||
/// Fügt bei Bedarf ":" automatisch ein.
|
||||
class HHmmInputFormatter extends TextInputFormatter {
|
||||
const HHmmInputFormatter();
|
||||
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
||||
String digits = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');
|
||||
if (digits.isEmpty) {
|
||||
return const TextEditingValue(text: '', selection: TextSelection.collapsed(offset: 0));
|
||||
}
|
||||
if (digits.length > 4) digits = digits.substring(0, 4);
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
var text = newValue.text;
|
||||
|
||||
String text;
|
||||
if (digits.length <= 2) {
|
||||
text = digits;
|
||||
} else {
|
||||
final hh = digits.substring(0, 2);
|
||||
final mm = digits.substring(2);
|
||||
text = '$hh:$mm';
|
||||
// nur Ziffern und ":" erlauben
|
||||
final filtered = StringBuffer();
|
||||
for (int i = 0; i < text.length; i++) {
|
||||
final c = text[i];
|
||||
if ((c.codeUnitAt(0) >= 48 && c.codeUnitAt(0) <= 57) || c == ':') {
|
||||
filtered.write(c);
|
||||
}
|
||||
}
|
||||
text = filtered.toString();
|
||||
|
||||
// Länge begrenzen
|
||||
if (text.length > 5) text = text.substring(0, 5);
|
||||
|
||||
// ":" automatisch einfügen
|
||||
if (text.length >= 3) {
|
||||
if (text[2] != ':') {
|
||||
text = text.replaceRange(2, 2, ':');
|
||||
}
|
||||
}
|
||||
|
||||
final offset = text.length;
|
||||
return TextEditingValue(text: text, selection: TextSelection.collapsed(offset: offset));
|
||||
// nur ein ":" erlauben, und nur an Stelle 2
|
||||
if (text.contains(':')) {
|
||||
final idx = text.indexOf(':');
|
||||
if (idx != 2) {
|
||||
text = text.replaceAll(':', '');
|
||||
if (text.length >= 2) {
|
||||
text = '${text.substring(0, 2)}:${text.substring(2)}';
|
||||
}
|
||||
} else {
|
||||
// weitere ":" entfernen
|
||||
final rest = text.substring(3).replaceAll(':', '');
|
||||
text = text.substring(0, 3) + rest;
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor ans Ende
|
||||
return TextEditingValue(
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(offset: text.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/work_interval.dart';
|
||||
|
||||
int _toMinutes(TimeOfDay t) => t.hour * 60 + t.minute;
|
||||
|
||||
/// Effektive Arbeitsminuten eines Tages nach Pausenregel:
|
||||
/// - <= 6h Arbeit: keine Pflichtpause
|
||||
/// - > 6h Arbeit: 30 Min Pflichtpause, reduziert um die Summe der Lücken
|
||||
/// zwischen Intervallen (maximal bis 0 reduzierbar).
|
||||
int effectiveWorkedMinutes(
|
||||
List<WorkInterval> intervals, {
|
||||
int requiredBreakOver6h = 30,
|
||||
}) {
|
||||
if (intervals.isEmpty) return 0;
|
||||
|
||||
// sortieren
|
||||
final sorted = [...intervals]
|
||||
..sort((a, b) => _toMinutes(a.start).compareTo(_toMinutes(b.start)));
|
||||
|
||||
// Arbeitszeit summieren
|
||||
int worked = 0;
|
||||
for (final iv in sorted) {
|
||||
final w = _toMinutes(iv.end) - _toMinutes(iv.start);
|
||||
if (w > 0) worked += w;
|
||||
}
|
||||
|
||||
if (worked <= 6 * 60) return worked;
|
||||
|
||||
// Lücken summieren
|
||||
int gaps = 0;
|
||||
for (var i = 0; i < sorted.length - 1; i++) {
|
||||
final gap = _toMinutes(sorted[i + 1].start) - _toMinutes(sorted[i].end);
|
||||
if (gap > 0) gaps += gap;
|
||||
}
|
||||
|
||||
final extraBreak = (requiredBreakOver6h - gaps).clamp(0, requiredBreakOver6h);
|
||||
final effective = worked - extraBreak;
|
||||
return effective < 0 ? 0 : effective;
|
||||
}
|
||||
Reference in New Issue
Block a user