Files
patrickandClaude Sonnet 5 200709d416 feat: Übersicht-Tab (Zeitkonto, Urlaubskonto, Zeiten-Historie)
Bottom-Navigation (MainShell) mit zwei Tabs: Stempeln (bisheriger
HomeScreen) + neue Übersicht (read-only): Zeitkonto (gearbeitet/Soll/Saldo
via GET /time/balance/me), Urlaubskonto (Anspruch/genommen/verfügbar via
GET /absences/balance) und die letzten Zeiteinträge (GET /time/entries).
Kein Beantragen von Urlaub, keine Korrektur – nur Anzeige. intl-Locale-Init
bewusst vermieden (Crash-Risiko wie beim letzten Mal), Datum manuell
formatiert. v0.1.8+9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-10 00:21:52 +02:00

103 lines
3.1 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'app_version.dart';
import 'services/api_client.dart';
import 'services/biometric_service.dart';
import 'screens/login_screen.dart';
import 'screens/main_shell.dart';
import 'screens/lock_screen.dart';
Future<void> main() async {
// Release-Build zeigt bei unbehandelten Fehlern sonst nur eine leere
// graue/weiße Fläche ohne Text (Flutters Default-ErrorWidget im Release-
// Modus). Für den Feldtest hier bewusst sichtbar machen, inkl. App-Version,
// damit gemeldete Fehler einem Build zuzuordnen sind.
ErrorWidget.builder = (FlutterErrorDetails details) {
return Material(
color: Colors.red[900],
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Fehler (App $kAppVersion)',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 8),
Text(details.exceptionAsString(), style: const TextStyle(color: Colors.white70, fontSize: 12)),
],
),
),
),
),
);
};
runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
await ApiClient.instance.init();
runApp(const TimeMasterApp());
}, (error, stack) {
debugPrint('Unbehandelter Fehler (App $kAppVersion): $error\n$stack');
});
}
class TimeMasterApp extends StatelessWidget {
const TimeMasterApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'TimeMaster',
theme: ThemeData(
colorSchemeSeed: Colors.indigo,
useMaterial3: true,
),
home: const _SessionGate(),
);
}
}
class _SessionGate extends StatelessWidget {
const _SessionGate();
@override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: ApiClient.instance.hasSession(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Scaffold(
body: Center(child: Text('Startfehler: ${snapshot.error}')),
);
}
if (!snapshot.hasData) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
return snapshot.data! ? const _LockGate() : const LoginScreen();
},
);
}
}
/// Prüft, ob das Gerät Biometrie/PIN unterstützt. Falls ja: erst entsperren,
/// sonst direkt zu MainShell (kein Ausschluss von Geräten ohne Biometrie).
class _LockGate extends StatelessWidget {
const _LockGate();
@override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: BiometricService().isAvailable(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
return snapshot.data! ? const LockScreen() : const MainShell();
},
);
}
}