import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'graphql_config.dart'; /// Die Hauptfunktion der App, die den Startpunkt der Anwendung darstellt. /// Initialisiert die MaterialApp mit dem TodoListScreen als Home-Widget. void main() async { // Initialisiere GraphQL await initHiveForFlutter(); runApp(const MyApp()); } /// Das Root-Widget der Anwendung. /// Konfiguriert das grundlegende Theme und die MaterialApp-Einstellungen. class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return GraphQLProvider( client: ValueNotifier(graphQLClient), child: MaterialApp( title: 'To-Do Liste', debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color.fromARGB(255, 13, 245, 117), ), useMaterial3: true, ), home: const AuthWrapper(), ), ); } } /// Wrapper-Widget, das die Authentifizierung überprüft und entsprechend /// den LoginScreen oder den TodoListScreen anzeigt. class AuthWrapper extends StatefulWidget { const AuthWrapper({super.key}); @override State createState() => _AuthWrapperState(); } class _AuthWrapperState extends State { final _storage = const FlutterSecureStorage(); bool _isAuthenticated = false; @override void initState() { super.initState(); _checkAuth(); } Future _checkAuth() async { final token = await _storage.read(key: 'auth_token'); setState(() { _isAuthenticated = token != null; }); } @override Widget build(BuildContext context) { return _isAuthenticated ? const TodoListScreen() : LoginScreen( onLogin: () async { await _checkAuth(); }, ); } } /// Login-Screen für die Authentifizierung class LoginScreen extends StatefulWidget { final VoidCallback onLogin; const LoginScreen({super.key, required this.onLogin}); @override State createState() => _LoginScreenState(); } class _LoginScreenState extends State { final _storage = const FlutterSecureStorage(); final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); bool _isLoading = false; @override void dispose() { _usernameController.dispose(); _passwordController.dispose(); super.dispose(); } //if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) { //ScaffoldMessenger.of(context).showSnackBar( //const SnackBar(content: Text('Bitte füllen Sie alle Felder aus')), //); Future _login() async { // Feste User anmeldedaten: const validUsername = "user"; const validPassword = "password"; if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Bitte füllen Sie alle Felder aus')), ); return; } else if (_usernameController.text != validUsername || _passwordController.text != validPassword) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Falsche Anmeldedaten'))); return; } await _storage.write( key: 'auth_token', value: 'dummy_token_${_usernameController.text}', ); widget.onLogin(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Anmeldung'), backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextField( controller: _usernameController, decoration: const InputDecoration( labelText: 'Benutzername', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: _passwordController, decoration: const InputDecoration( labelText: 'Passwort', border: OutlineInputBorder(), ), obscureText: true, ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isLoading ? null : _login, child: _isLoading ? const CircularProgressIndicator() : const Text('Anmelden'), ), ), ], ), ), ); } } /// Das Haupt-Widget der To-Do Liste. /// Implementiert als StatefulWidget, um den Zustand der Aufgabenliste zu verwalten. class TodoListScreen extends StatefulWidget { const TodoListScreen({super.key}); @override State createState() => _TodoListScreenState(); } /// Der State des TodoListScreen-Widgets. /// Verwaltet die Liste der Aufgaben und die zugehörigen Funktionen. class _TodoListScreenState extends State { /// Liste der Todo-Items final List _todos = []; /// Controller für das Texteingabefeld final TextEditingController _textController = TextEditingController(); final _storage = const FlutterSecureStorage(); DateTime _selectedDate = DateTime.now(); @override void initState() { super.initState(); _loadTodos(); } Future _loadTodos() async { try { final String jsonString = await rootBundle.loadString( 'assets/todos.json', ); final Map jsonData = json.decode(jsonString); final List todosJson = jsonData['todos'] as List; setState(() { _todos.clear(); _todos.addAll( todosJson.map((json) => TodoItem.fromJson(json)).toList(), ); }); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Fehler beim Laden der Todos: $e')), ); } } Future _selectDate(BuildContext context) async { final DateTime? picked = await showDatePicker( context: context, initialDate: _selectedDate, firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365)), ); if (picked != null && picked != _selectedDate) { setState(() { _selectedDate = picked; }); } } /// Fügt eine neue Aufgabe zur Liste hinzu. /// [title] Der Titel der neuen Aufgabe void _addTodo(String title) { if (title.isEmpty) return; setState(() { final newId = _todos.isEmpty ? 1 : _todos.map((e) => e.id).reduce((a, b) => a > b ? a : b) + 1; _todos.add(TodoItem(id: newId, title: title, deadline: _selectedDate)); _textController.clear(); }); } /// Entfernt eine Aufgabe aus der Liste. /// [index] Der Index der zu entfernenden Aufgabe void _removeTodo(int index) { setState(() { _todos.removeAt(index); }); } /// Wechselt den Status einer Aufgabe zwischen erledigt und nicht erledigt. /// [index] Der Index der zu ändernden Aufgabe void _toggleTodo(int index) { setState(() { _todos[index].isCompleted = !_todos[index].isCompleted; }); } Future _logout() async { await _storage.delete(key: 'auth_token'); if (mounted) { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (context) => const AuthWrapper()), ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('To-Do Liste'), backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, actions: [ IconButton(icon: const Icon(Icons.logout), onPressed: _logout), ], ), body: Query( options: QueryOptions( document: gql(getTodosQuery), pollInterval: const Duration(seconds: 5), // Aktualisiere alle 5 Sekunden ), builder: (QueryResult result, {VoidCallback? refetch, FetchMore? fetchMore}) { if (result.hasException) { return Center( child: Text('Fehler: ${result.exception.toString()}'), ); } if (result.isLoading) { return const Center( child: CircularProgressIndicator(), ); } // Aktualisiere die Todos aus dem GraphQL-Ergebnis final todos = (result.data?['todos'] as List?)?.map((todo) { return TodoItem.fromJson(todo); }).toList() ?? []; return Column( children: [ Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Row( children: [ Expanded( child: TextField( controller: _textController, decoration: const InputDecoration( hintText: 'Neue Aufgabe eingeben', border: OutlineInputBorder(), ), ), ), const SizedBox(width: 16), Mutation( options: MutationOptions( document: gql(addTodoMutation), update: (cache, result) { if (result?.data != null) { refetch?.call(); } }, ), builder: (RunMutation runMutation, QueryResult? result) { return ElevatedButton( onPressed: () { if (_textController.text.isEmpty) return; runMutation({ 'name': _textController.text, 'deadline': DateFormat('yyyy-MM-dd') .format(_selectedDate), }); _textController.clear(); }, child: const Text('Hinzufügen'), ); }, ), ], ), const SizedBox(height: 16), Row( children: [ Text( 'Deadline: ${DateFormat('dd.MM.yyyy').format(_selectedDate)}'), IconButton( icon: const Icon(Icons.calendar_today), onPressed: () => _selectDate(context), ), ], ), ], ), ), Expanded( child: ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { final todo = todos[index]; return Mutation( options: MutationOptions( document: gql(updateTodoStatusMutation), update: (cache, result) { if (result?.data != null) { refetch?.call(); } }, ), builder: (RunMutation runMutation, QueryResult? result) { return ListTile( leading: Checkbox( value: todo.isCompleted, onChanged: (bool? value) { if (value != null) { runMutation({ 'id': todo.id, 'status': value, }); } }, ), title: Text( todo.title, style: TextStyle( decoration: todo.isCompleted ? TextDecoration.lineThrough : null, ), ), subtitle: Text( 'Deadline: ${DateFormat('dd.MM.yyyy').format(todo.deadline)}', style: TextStyle( color: todo.deadline.isBefore(DateTime.now()) && !todo.isCompleted ? Colors.red : null, ), ), trailing: Mutation( options: MutationOptions( document: gql(deleteTodoMutation), update: (cache, result) { if (result?.data != null) { refetch?.call(); } }, ), builder: (RunMutation runMutation, QueryResult? result) { return IconButton( icon: const Icon(Icons.delete), onPressed: () { runMutation({ 'id': todo.id, }); }, ); }, ), ); }, ); }, ), ), ], ); }, ), ); } } /// Repräsentiert eine einzelne Aufgabe in der To-Do Liste. /// Enthält den Titel der Aufgabe und ihren Erledigungsstatus. class TodoItem { final int id; String title; bool isCompleted; DateTime deadline; TodoItem({ required this.id, required this.title, this.isCompleted = false, required this.deadline, }); factory TodoItem.fromJson(Map json) { return TodoItem( id: json['id'] as int, title: json['name'] as String, isCompleted: json['status'] as bool, deadline: DateTime.parse(json['deadline'] as String), ); } //Map toJson() { // return { // 'id': id, // 'name': title, // 'status': isCompleted, // 'deadline': DateFormat('yyyy-MM-dd').format(deadline), // }; //} }