From 5f2de5e67128218f6e75ba13de50589ee7c12b5d Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 10 Aug 2026 15:15:11 +0200 Subject: [PATCH] - android app - code import fix --- .../presentation/club_workflows_page.dart | 244 +++++++++++++ backend/services/pdfParserService.js | 4 +- .../.github/workflows/quality.yml | 0 .../desktop-app => desktop-app}/.gitignore | 0 .../desktop-app => desktop-app}/.metadata | 0 .../desktop-app => desktop-app}/README.md | 0 .../analysis_options.yaml | 0 .../lib/core/config/app_environment.dart | 0 .../lib/core/logging/app_logger.dart | 0 .../lib/core/navigation/app_router.dart | 0 .../lib/core/network/api_client.dart | 27 ++ .../lib/core/network/api_exception.dart | 0 .../lib/core/session/app_session.dart | 0 .../lib/core/theme/app_theme.dart | 0 .../lib/core/widgets/async_content.dart | 0 .../lib/core/widgets/product_mark.dart | 0 .../lib/data/README.md | 0 .../lib/data/auth/auth_repository.dart | 0 .../lib/data/auth/token_storage.dart | 0 .../club_overview_repository.dart | 0 .../club_workflows_repository.dart | 35 ++ .../lib/data/clubs/club_repository.dart | 0 .../data/training/training_repository.dart | 31 ++ .../lib/domain/README.md | 0 .../lib/domain/models/club.dart | 0 .../lib/domain/models/club_permissions.dart | 0 .../auth/presentation/login_page.dart | 0 .../presentation/password_reset_pages.dart | 0 .../presentation/club_selection_page.dart | 0 .../presentation/club_overview_page.dart | 0 .../shell/presentation/app_shell.dart | 11 + .../presentation/training_workbench_page.dart | 330 ++++++++++++++++++ .../desktop-app => desktop-app}/lib/main.dart | 0 .../lib/presentation/README.md | 0 .../linux/.gitignore | 0 .../linux/CMakeLists.txt | 12 + .../linux/flutter/CMakeLists.txt | 0 .../flutter/generated_plugin_registrant.cc | 0 .../flutter/generated_plugin_registrant.h | 0 .../linux/flutter/generated_plugins.cmake | 0 .../linux/runner/CMakeLists.txt | 0 .../linux/runner/main.cc | 0 .../linux/runner/my_application.cc | 0 .../linux/runner/my_application.h | 0 .../macos/.gitignore | 0 .../macos/Flutter/Flutter-Debug.xcconfig | 0 .../macos/Flutter/Flutter-Release.xcconfig | 0 .../Flutter/GeneratedPluginRegistrant.swift | 0 .../macos/Runner.xcodeproj/project.pbxproj | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../macos/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/app_icon_1024.png | Bin .../AppIcon.appiconset/app_icon_128.png | Bin .../AppIcon.appiconset/app_icon_16.png | Bin .../AppIcon.appiconset/app_icon_256.png | Bin .../AppIcon.appiconset/app_icon_32.png | Bin .../AppIcon.appiconset/app_icon_512.png | Bin .../AppIcon.appiconset/app_icon_64.png | Bin .../macos/Runner/Base.lproj/MainMenu.xib | 0 .../macos/Runner/Configs/AppInfo.xcconfig | 0 .../macos/Runner/Configs/Debug.xcconfig | 0 .../macos/Runner/Configs/Release.xcconfig | 0 .../macos/Runner/Configs/Warnings.xcconfig | 0 .../macos/Runner/DebugProfile.entitlements | 0 .../macos/Runner/Info.plist | 0 .../macos/Runner/MainFlutterWindow.swift | 0 .../macos/Runner/Release.entitlements | 0 .../macos/RunnerTests/RunnerTests.swift | 0 .../desktop-app => desktop-app}/pubspec.lock | 0 .../desktop-app => desktop-app}/pubspec.yaml | 0 .../test/widget_test.dart | 0 .../windows/.gitignore | 0 .../windows/CMakeLists.txt | 0 .../windows/flutter/CMakeLists.txt | 0 .../flutter/generated_plugin_registrant.cc | 0 .../flutter/generated_plugin_registrant.h | 0 .../windows/flutter/generated_plugins.cmake | 0 .../windows/runner/CMakeLists.txt | 0 .../windows/runner/Runner.rc | 0 .../windows/runner/flutter_window.cpp | 0 .../windows/runner/flutter_window.h | 0 .../windows/runner/main.cpp | 0 .../windows/runner/resource.h | 0 .../windows/runner/resources/app_icon.ico | Bin .../windows/runner/runner.exe.manifest | 0 .../windows/runner/utils.cpp | 0 .../windows/runner/utils.h | 0 .../windows/runner/win32_window.cpp | 0 .../windows/runner/win32_window.h | 0 93 files changed, 693 insertions(+), 1 deletion(-) create mode 100644 backend/desktop-app/lib/features/workflows/presentation/club_workflows_page.dart rename {backend/desktop-app => desktop-app}/.github/workflows/quality.yml (100%) rename {backend/desktop-app => desktop-app}/.gitignore (100%) rename {backend/desktop-app => desktop-app}/.metadata (100%) rename {backend/desktop-app => desktop-app}/README.md (100%) rename {backend/desktop-app => desktop-app}/analysis_options.yaml (100%) rename {backend/desktop-app => desktop-app}/lib/core/config/app_environment.dart (100%) rename {backend/desktop-app => desktop-app}/lib/core/logging/app_logger.dart (100%) rename {backend/desktop-app => desktop-app}/lib/core/navigation/app_router.dart (100%) rename {backend/desktop-app => desktop-app}/lib/core/network/api_client.dart (75%) rename {backend/desktop-app => desktop-app}/lib/core/network/api_exception.dart (100%) rename {backend/desktop-app => desktop-app}/lib/core/session/app_session.dart (100%) rename {backend/desktop-app => desktop-app}/lib/core/theme/app_theme.dart (100%) rename {backend/desktop-app => desktop-app}/lib/core/widgets/async_content.dart (100%) rename {backend/desktop-app => desktop-app}/lib/core/widgets/product_mark.dart (100%) rename {backend/desktop-app => desktop-app}/lib/data/README.md (100%) rename {backend/desktop-app => desktop-app}/lib/data/auth/auth_repository.dart (100%) rename {backend/desktop-app => desktop-app}/lib/data/auth/token_storage.dart (100%) rename {backend/desktop-app => desktop-app}/lib/data/club_overview/club_overview_repository.dart (100%) create mode 100644 desktop-app/lib/data/club_workflows/club_workflows_repository.dart rename {backend/desktop-app => desktop-app}/lib/data/clubs/club_repository.dart (100%) create mode 100644 desktop-app/lib/data/training/training_repository.dart rename {backend/desktop-app => desktop-app}/lib/domain/README.md (100%) rename {backend/desktop-app => desktop-app}/lib/domain/models/club.dart (100%) rename {backend/desktop-app => desktop-app}/lib/domain/models/club_permissions.dart (100%) rename {backend/desktop-app => desktop-app}/lib/features/auth/presentation/login_page.dart (100%) rename {backend/desktop-app => desktop-app}/lib/features/auth/presentation/password_reset_pages.dart (100%) rename {backend/desktop-app => desktop-app}/lib/features/clubs/presentation/club_selection_page.dart (100%) rename {backend/desktop-app => desktop-app}/lib/features/overview/presentation/club_overview_page.dart (100%) rename {backend/desktop-app => desktop-app}/lib/features/shell/presentation/app_shell.dart (98%) create mode 100644 desktop-app/lib/features/training/presentation/training_workbench_page.dart rename {backend/desktop-app => desktop-app}/lib/main.dart (100%) rename {backend/desktop-app => desktop-app}/lib/presentation/README.md (100%) rename {backend/desktop-app => desktop-app}/linux/.gitignore (100%) rename {backend/desktop-app => desktop-app}/linux/CMakeLists.txt (90%) rename {backend/desktop-app => desktop-app}/linux/flutter/CMakeLists.txt (100%) rename {backend/desktop-app => desktop-app}/linux/flutter/generated_plugin_registrant.cc (100%) rename {backend/desktop-app => desktop-app}/linux/flutter/generated_plugin_registrant.h (100%) rename {backend/desktop-app => desktop-app}/linux/flutter/generated_plugins.cmake (100%) rename {backend/desktop-app => desktop-app}/linux/runner/CMakeLists.txt (100%) rename {backend/desktop-app => desktop-app}/linux/runner/main.cc (100%) rename {backend/desktop-app => desktop-app}/linux/runner/my_application.cc (100%) rename {backend/desktop-app => desktop-app}/linux/runner/my_application.h (100%) rename {backend/desktop-app => desktop-app}/macos/.gitignore (100%) rename {backend/desktop-app => desktop-app}/macos/Flutter/Flutter-Debug.xcconfig (100%) rename {backend/desktop-app => desktop-app}/macos/Flutter/Flutter-Release.xcconfig (100%) rename {backend/desktop-app => desktop-app}/macos/Flutter/GeneratedPluginRegistrant.swift (100%) rename {backend/desktop-app => desktop-app}/macos/Runner.xcodeproj/project.pbxproj (100%) rename {backend/desktop-app => desktop-app}/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {backend/desktop-app => desktop-app}/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {backend/desktop-app => desktop-app}/macos/Runner.xcworkspace/contents.xcworkspacedata (100%) rename {backend/desktop-app => desktop-app}/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/AppDelegate.swift (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Base.lproj/MainMenu.xib (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Configs/AppInfo.xcconfig (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Configs/Debug.xcconfig (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Configs/Release.xcconfig (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Configs/Warnings.xcconfig (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/DebugProfile.entitlements (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Info.plist (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/MainFlutterWindow.swift (100%) rename {backend/desktop-app => desktop-app}/macos/Runner/Release.entitlements (100%) rename {backend/desktop-app => desktop-app}/macos/RunnerTests/RunnerTests.swift (100%) rename {backend/desktop-app => desktop-app}/pubspec.lock (100%) rename {backend/desktop-app => desktop-app}/pubspec.yaml (100%) rename {backend/desktop-app => desktop-app}/test/widget_test.dart (100%) rename {backend/desktop-app => desktop-app}/windows/.gitignore (100%) rename {backend/desktop-app => desktop-app}/windows/CMakeLists.txt (100%) rename {backend/desktop-app => desktop-app}/windows/flutter/CMakeLists.txt (100%) rename {backend/desktop-app => desktop-app}/windows/flutter/generated_plugin_registrant.cc (100%) rename {backend/desktop-app => desktop-app}/windows/flutter/generated_plugin_registrant.h (100%) rename {backend/desktop-app => desktop-app}/windows/flutter/generated_plugins.cmake (100%) rename {backend/desktop-app => desktop-app}/windows/runner/CMakeLists.txt (100%) rename {backend/desktop-app => desktop-app}/windows/runner/Runner.rc (100%) rename {backend/desktop-app => desktop-app}/windows/runner/flutter_window.cpp (100%) rename {backend/desktop-app => desktop-app}/windows/runner/flutter_window.h (100%) rename {backend/desktop-app => desktop-app}/windows/runner/main.cpp (100%) rename {backend/desktop-app => desktop-app}/windows/runner/resource.h (100%) rename {backend/desktop-app => desktop-app}/windows/runner/resources/app_icon.ico (100%) rename {backend/desktop-app => desktop-app}/windows/runner/runner.exe.manifest (100%) rename {backend/desktop-app => desktop-app}/windows/runner/utils.cpp (100%) rename {backend/desktop-app => desktop-app}/windows/runner/utils.h (100%) rename {backend/desktop-app => desktop-app}/windows/runner/win32_window.cpp (100%) rename {backend/desktop-app => desktop-app}/windows/runner/win32_window.h (100%) diff --git a/backend/desktop-app/lib/features/workflows/presentation/club_workflows_page.dart b/backend/desktop-app/lib/features/workflows/presentation/club_workflows_page.dart new file mode 100644 index 00000000..5bc461cd --- /dev/null +++ b/backend/desktop-app/lib/features/workflows/presentation/club_workflows_page.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; + +import '../../../core/network/api_client.dart'; +import '../../../core/theme/app_theme.dart'; +import '../../../data/club_workflows/club_workflows_repository.dart'; + +enum ClubWorkflow { training, members, orders, teams, tournaments } + +/// Phase 4: focused desktop write workflows. The forms deliberately expose the +/// safe, common fields while leaving specialised administration in the web app. +class ClubWorkflowsPage extends StatefulWidget { + const ClubWorkflowsPage({ + super.key, + required this.workflow, + required this.clubId, + required this.api, + }); + + final ClubWorkflow workflow; + final int clubId; + final ApiClient api; + + @override + State createState() => _ClubWorkflowsPageState(); +} + +class _ClubWorkflowsPageState extends State { + late ClubWorkflowsRepository _repository; + late Future>> _items; + bool _saving = false; + String? _message; + bool _isError = false; + + @override + void initState() { + super.initState(); + _repository = ClubWorkflowsRepository(widget.api); + _items = _load(); + } + + @override + void didUpdateWidget(covariant ClubWorkflowsPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.workflow != widget.workflow || oldWidget.clubId != widget.clubId) { + _repository = ClubWorkflowsRepository(widget.api); + _items = _load(); + } + } + + Future>> _load() async { + Object? response = switch (widget.workflow) { + ClubWorkflow.training => await _repository.training(widget.clubId), + ClubWorkflow.members => await _repository.members(widget.clubId), + ClubWorkflow.orders => await _repository.members(widget.clubId), + ClubWorkflow.teams => await _repository.teams(widget.clubId), + ClubWorkflow.tournaments => await _repository.tournaments(widget.clubId), + }; + return _asRecords(response); + } + + void _refresh() => setState(() => _items = _load()); + + Future _save(Future Function() request, String success) async { + setState(() { + _saving = true; + _message = null; + }); + try { + await request(); + if (!mounted) return; + setState(() { + _message = success; + _isError = false; + }); + _refresh(); + } catch (error) { + if (!mounted) return; + setState(() { + _message = error.toString().replaceFirst('ApiException: ', ''); + _isError = true; + }); + } finally { + if (mounted) setState(() => _saving = false); + } + } + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Editor( + workflow: widget.workflow, + items: _items, + saving: _saving, + onSave: _save, + repository: _repository, + clubId: widget.clubId, + onSaved: _refresh, + ), + if (_message != null) ...[ + const SizedBox(height: 16), + _Notice(message: _message!, error: _isError), + ], + const SizedBox(height: 24), + Row( + children: [ + Text('Vorhandene Einträge', style: Theme.of(context).textTheme.titleMedium), + const Spacer(), + TextButton.icon(onPressed: _saving ? null : _refresh, icon: const Icon(Icons.refresh), label: const Text('Aktualisieren')), + ], + ), + const SizedBox(height: 8), + FutureBuilder>>( + future: _items, + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) return const Padding(padding: EdgeInsets.all(24), child: Center(child: CircularProgressIndicator())); + if (snapshot.hasError) return _Notice(message: snapshot.error.toString(), error: true); + final items = snapshot.data ?? const []; + if (items.isEmpty) return const Padding(padding: EdgeInsets.all(16), child: Text('Noch keine Einträge vorhanden.')); + return _ItemList(items: items, workflow: widget.workflow, onEditMember: (member) => _showMemberDialog(context, member)); + }, + ), + ], + ); + + Future _showMemberDialog(BuildContext context, Map member) async { + final first = TextEditingController(text: _value(member, 'firstName', 'firstname')); + final last = TextEditingController(text: _value(member, 'lastName', 'lastname')); + final email = TextEditingController(text: _value(member, 'email')); + await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: const Text('Mitglied bearbeiten'), + content: SizedBox(width: 440, child: Column(mainAxisSize: MainAxisSize.min, children: [ + _Field(label: 'Vorname', controller: first), _Field(label: 'Nachname', controller: last), _Field(label: 'E-Mail', controller: email), + ])), + actions: [ + TextButton(onPressed: () => Navigator.pop(dialogContext), child: const Text('Abbrechen')), + FilledButton(onPressed: () async { + Navigator.pop(dialogContext); + await _save(() => _repository.saveMember(widget.clubId, { + 'id': member['id'], 'firstname': first.text.trim(), 'lastname': last.text.trim(), 'email': email.text.trim(), + }), 'Mitglied gespeichert.'); + }, child: const Text('Speichern')), + ], + ), + ); + first.dispose(); last.dispose(); email.dispose(); + } +} + +class _Editor extends StatefulWidget { + const _Editor({required this.workflow, required this.items, required this.saving, required this.onSave, required this.repository, required this.clubId, required this.onSaved}); + final ClubWorkflow workflow; + final Future>> items; + final bool saving; + final Future Function(Future Function(), String) onSave; + final ClubWorkflowsRepository repository; + final int clubId; + final VoidCallback onSaved; + @override State<_Editor> createState() => _EditorState(); +} + +class _EditorState extends State<_Editor> { + final _form = GlobalKey(); + final _a = TextEditingController(); final _b = TextEditingController(); final _c = TextEditingController(); + String _status = 'requested'; int? _memberId; + @override void dispose() { _a.dispose(); _b.dispose(); _c.dispose(); super.dispose(); } + void _clear() { _a.clear(); _b.clear(); _c.clear(); setState(() { _memberId = null; _status = 'requested'; }); } + @override + Widget build(BuildContext context) { + final labels = switch (widget.workflow) { + ClubWorkflow.training => ('Training anlegen', 'Datum (YYYY-MM-DD)', 'Beginn (HH:MM)', 'Ende (HH:MM)'), + ClubWorkflow.members => ('Mitglied anlegen', 'Vorname', 'Nachname', 'E-Mail'), + ClubWorkflow.orders => ('Sammelbestellung erfassen', 'Artikel', 'Betrag in €', 'Budget in €'), + ClubWorkflow.teams => ('Mannschaft anlegen', 'Name', 'Geplante Liga (optional)', ''), + ClubWorkflow.tournaments => ('Turnier anlegen', 'Name', 'Datum (YYYY-MM-DD)', 'Gewinnsätze'), + }; + return Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(20), + child: Form(key: _form, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(labels.$1, style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 14), + if (widget.workflow == ClubWorkflow.orders) FutureBuilder>( + future: widget.items, + builder: (context, snapshot) => DropdownButtonFormField( + value: _memberId, isExpanded: true, decoration: const InputDecoration(labelText: 'Mitglied'), + items: (snapshot.data ?? const []).map((m) => DropdownMenuItem(value: _int(m['id']), child: Text(_memberName(m)))).toList(), + onChanged: widget.saving ? null : (value) => setState(() => _memberId = value), + validator: (value) => value == null ? 'Bitte Mitglied wählen.' : null, + ), + ), + if (widget.workflow == ClubWorkflow.orders) const SizedBox(height: 12), + Wrap(spacing: 12, runSpacing: 12, children: [ + SizedBox(width: 250, child: _Field(label: labels.$2, controller: _a, required: true)), + SizedBox(width: 220, child: _Field(label: labels.$3, controller: _b, required: widget.workflow != ClubWorkflow.teams)), + if (labels.$4.isNotEmpty) SizedBox(width: 180, child: _Field(label: labels.$4, controller: _c, required: widget.workflow == ClubWorkflow.tournaments)), + if (widget.workflow == ClubWorkflow.orders) SizedBox(width: 180, child: DropdownButtonFormField(value: _status, decoration: const InputDecoration(labelText: 'Status'), items: const [DropdownMenuItem(value: 'requested', child: Text('Angefragt')), DropdownMenuItem(value: 'ordered', child: Text('Bestellt')), DropdownMenuItem(value: 'received', child: Text('Eingetroffen'))], onChanged: (value) => setState(() => _status = value ?? _status))), + ]), + const SizedBox(height: 16), + Row(children: [ + FilledButton.icon(onPressed: widget.saving ? null : _submit, icon: widget.saving ? const SizedBox.square(dimension: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.save_outlined), label: const Text('Speichern')), + const SizedBox(width: 8), TextButton(onPressed: widget.saving ? null : _clear, child: const Text('Leeren')), + ]), + ])), + ), + ); + } + void _submit() { + if (!(_form.currentState?.validate() ?? false)) return; + final a = _a.text.trim(), b = _b.text.trim(), c = _c.text.trim(); + switch (widget.workflow) { + case ClubWorkflow.training: widget.onSave(() => widget.repository.createTraining(widget.clubId, {'date': a, 'trainingStart': b, 'trainingEnd': c}), 'Training angelegt.'); + case ClubWorkflow.members: widget.onSave(() => widget.repository.saveMember(widget.clubId, {'firstname': a, 'lastname': b, 'email': c, 'active': true}), 'Mitglied angelegt.'); + case ClubWorkflow.orders: widget.onSave(() => widget.repository.createOrder(widget.clubId, _memberId!, {'item': a, 'cost': _number(b), 'budget': _number(c), 'status': _status}), 'Bestellung gespeichert.'); + case ClubWorkflow.teams: widget.onSave(() => widget.repository.createTeam(widget.clubId, {'name': a, 'plannedLeagueName': b}), 'Mannschaft angelegt.'); + case ClubWorkflow.tournaments: widget.onSave(() => widget.repository.createTournament({'clubId': widget.clubId, 'tournamentName': a, 'date': b, 'winningSets': _int(c) ?? 3}), 'Turnier angelegt.'); + } + _clear(); + } +} + +class _ItemList extends StatelessWidget { + const _ItemList({required this.items, required this.workflow, required this.onEditMember}); final List> items; final ClubWorkflow workflow; final ValueChanged> onEditMember; + @override Widget build(BuildContext context) => Column(children: items.take(20).map((item) { + final editable = workflow == ClubWorkflow.members; + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + leading: Icon(_icon(workflow), color: AppColors.green), title: Text(_title(item, workflow)), subtitle: _subtitle(item) == null ? null : Text(_subtitle(item)!), + trailing: editable ? IconButton(icon: const Icon(Icons.edit_outlined), tooltip: 'Bearbeiten', onPressed: () => onEditMember(item)) : null, + ); + }).toList()); +} + +class _Field extends StatelessWidget { const _Field({required this.label, required this.controller, this.required = false}); final String label; final TextEditingController controller; final bool required; @override Widget build(BuildContext context) => TextFormField(controller: controller, decoration: InputDecoration(labelText: label), validator: required ? (value) => value == null || value.trim().isEmpty ? 'Pflichtfeld' : null : null); } +class _Notice extends StatelessWidget { const _Notice({required this.message, required this.error}); final String message; final bool error; @override Widget build(BuildContext context) => Container(width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: error ? const Color(0xFFFFECEB) : const Color(0xFFE7F4EC), borderRadius: BorderRadius.circular(8)), child: Text(message, style: TextStyle(color: error ? Colors.red.shade800 : AppColors.greenDark))); } +List> _asRecords(Object? response) { if (response is List) return response.whereType().map((e) => Map.from(e)).toList(); if (response is Map) { final map = Map.from(response); for (final key in const ['items', 'members', 'orders', 'data']) { if (map[key] is List) return _asRecords(map[key]); } return [map]; } return const []; } +String _value(Map map, String first, [String? second]) => (map[first] ?? (second == null ? null : map[second]) ?? '').toString(); +int? _int(Object? value) => value is int ? value : int.tryParse('$value'); double? _number(String value) => double.tryParse(value.replaceAll(',', '.')); +String _memberName(Map item) => '${_value(item, 'firstName', 'firstname')} ${_value(item, 'lastName', 'lastname')}'.trim().isEmpty ? 'Mitglied #${item['id']}' : '${_value(item, 'firstName', 'firstname')} ${_value(item, 'lastName', 'lastname')}'.trim(); +String _title(Map item, ClubWorkflow workflow) => switch (workflow) { ClubWorkflow.members || ClubWorkflow.orders => workflow == ClubWorkflow.members ? _memberName(item) : _value(item, 'item'), ClubWorkflow.training => _value(item, 'date'), ClubWorkflow.teams => _value(item, 'name'), ClubWorkflow.tournaments => _value(item, 'name') }; +String? _subtitle(Map item) { for (final key in const ['email', 'trainingStart', 'startTime', 'date', 'status', 'plannedLeagueName']) { final value = item[key]?.toString(); if (value != null && value.isNotEmpty) return value; } return null; } +IconData _icon(ClubWorkflow workflow) => switch (workflow) { ClubWorkflow.training => Icons.sports_tennis_outlined, ClubWorkflow.members => Icons.person_outline, ClubWorkflow.orders => Icons.shopping_bag_outlined, ClubWorkflow.teams => Icons.groups_outlined, ClubWorkflow.tournaments => Icons.emoji_events_outlined }; diff --git a/backend/services/pdfParserService.js b/backend/services/pdfParserService.js index 883dec69..c1c03a3c 100755 --- a/backend/services/pdfParserService.js +++ b/backend/services/pdfParserService.js @@ -806,7 +806,9 @@ class PDFParserService { } static async extractPdfTextWithLayout(filePath) { - const { default: pdfjsLib } = await import('pdfjs-dist/legacy/build/pdf.js'); + // pdfjs-dist v5 publishes the legacy build as an ESM module (.mjs) and + // exposes getDocument as a named export (there is no default export). + const pdfjsLib = await import('pdfjs-dist/legacy/build/pdf.mjs'); const pdfData = new Uint8Array(fs.readFileSync(filePath)); const loadingTask = pdfjsLib.getDocument({ data: pdfData, disableWorker: true }); const pdf = await loadingTask.promise; diff --git a/backend/desktop-app/.github/workflows/quality.yml b/desktop-app/.github/workflows/quality.yml similarity index 100% rename from backend/desktop-app/.github/workflows/quality.yml rename to desktop-app/.github/workflows/quality.yml diff --git a/backend/desktop-app/.gitignore b/desktop-app/.gitignore similarity index 100% rename from backend/desktop-app/.gitignore rename to desktop-app/.gitignore diff --git a/backend/desktop-app/.metadata b/desktop-app/.metadata similarity index 100% rename from backend/desktop-app/.metadata rename to desktop-app/.metadata diff --git a/backend/desktop-app/README.md b/desktop-app/README.md similarity index 100% rename from backend/desktop-app/README.md rename to desktop-app/README.md diff --git a/backend/desktop-app/analysis_options.yaml b/desktop-app/analysis_options.yaml similarity index 100% rename from backend/desktop-app/analysis_options.yaml rename to desktop-app/analysis_options.yaml diff --git a/backend/desktop-app/lib/core/config/app_environment.dart b/desktop-app/lib/core/config/app_environment.dart similarity index 100% rename from backend/desktop-app/lib/core/config/app_environment.dart rename to desktop-app/lib/core/config/app_environment.dart diff --git a/backend/desktop-app/lib/core/logging/app_logger.dart b/desktop-app/lib/core/logging/app_logger.dart similarity index 100% rename from backend/desktop-app/lib/core/logging/app_logger.dart rename to desktop-app/lib/core/logging/app_logger.dart diff --git a/backend/desktop-app/lib/core/navigation/app_router.dart b/desktop-app/lib/core/navigation/app_router.dart similarity index 100% rename from backend/desktop-app/lib/core/navigation/app_router.dart rename to desktop-app/lib/core/navigation/app_router.dart diff --git a/backend/desktop-app/lib/core/network/api_client.dart b/desktop-app/lib/core/network/api_client.dart similarity index 75% rename from backend/desktop-app/lib/core/network/api_client.dart rename to desktop-app/lib/core/network/api_client.dart index 8fffaa74..11ec07a6 100644 --- a/backend/desktop-app/lib/core/network/api_client.dart +++ b/desktop-app/lib/core/network/api_client.dart @@ -70,6 +70,33 @@ class ApiClient { } } + /// Sends a mutation request where the server response is not part of the UI. + Future post(String path, {Object? data}) async { + try { + await _dio.post(path, data: data); + } on DioException catch (error) { + throw _toApiException(error); + } + } + + /// Sends a JSON PUT request to one of the existing write APIs. + Future putData(String path, {Object? data}) async { + try { + return (await _dio.put(path, data: data)).data; + } on DioException catch (error) { + throw _toApiException(error); + } + } + + /// Sends a JSON PATCH request to one of the existing write APIs. + Future patchData(String path, {Object? data}) async { + try { + return (await _dio.patch(path, data: data)).data; + } on DioException catch (error) { + throw _toApiException(error); + } + } + ApiException _toApiException(DioException error) { final data = error.response?.data; final message = data is Map diff --git a/backend/desktop-app/lib/core/network/api_exception.dart b/desktop-app/lib/core/network/api_exception.dart similarity index 100% rename from backend/desktop-app/lib/core/network/api_exception.dart rename to desktop-app/lib/core/network/api_exception.dart diff --git a/backend/desktop-app/lib/core/session/app_session.dart b/desktop-app/lib/core/session/app_session.dart similarity index 100% rename from backend/desktop-app/lib/core/session/app_session.dart rename to desktop-app/lib/core/session/app_session.dart diff --git a/backend/desktop-app/lib/core/theme/app_theme.dart b/desktop-app/lib/core/theme/app_theme.dart similarity index 100% rename from backend/desktop-app/lib/core/theme/app_theme.dart rename to desktop-app/lib/core/theme/app_theme.dart diff --git a/backend/desktop-app/lib/core/widgets/async_content.dart b/desktop-app/lib/core/widgets/async_content.dart similarity index 100% rename from backend/desktop-app/lib/core/widgets/async_content.dart rename to desktop-app/lib/core/widgets/async_content.dart diff --git a/backend/desktop-app/lib/core/widgets/product_mark.dart b/desktop-app/lib/core/widgets/product_mark.dart similarity index 100% rename from backend/desktop-app/lib/core/widgets/product_mark.dart rename to desktop-app/lib/core/widgets/product_mark.dart diff --git a/backend/desktop-app/lib/data/README.md b/desktop-app/lib/data/README.md similarity index 100% rename from backend/desktop-app/lib/data/README.md rename to desktop-app/lib/data/README.md diff --git a/backend/desktop-app/lib/data/auth/auth_repository.dart b/desktop-app/lib/data/auth/auth_repository.dart similarity index 100% rename from backend/desktop-app/lib/data/auth/auth_repository.dart rename to desktop-app/lib/data/auth/auth_repository.dart diff --git a/backend/desktop-app/lib/data/auth/token_storage.dart b/desktop-app/lib/data/auth/token_storage.dart similarity index 100% rename from backend/desktop-app/lib/data/auth/token_storage.dart rename to desktop-app/lib/data/auth/token_storage.dart diff --git a/backend/desktop-app/lib/data/club_overview/club_overview_repository.dart b/desktop-app/lib/data/club_overview/club_overview_repository.dart similarity index 100% rename from backend/desktop-app/lib/data/club_overview/club_overview_repository.dart rename to desktop-app/lib/data/club_overview/club_overview_repository.dart diff --git a/desktop-app/lib/data/club_workflows/club_workflows_repository.dart b/desktop-app/lib/data/club_workflows/club_workflows_repository.dart new file mode 100644 index 00000000..0933fa85 --- /dev/null +++ b/desktop-app/lib/data/club_workflows/club_workflows_repository.dart @@ -0,0 +1,35 @@ +import '../../core/network/api_client.dart'; + +/// Thin adapter for the established write APIs used by the desktop MVP. +class ClubWorkflowsRepository { + const ClubWorkflowsRepository(this._api); + final ApiClient _api; + + Future members(int clubId) => + _api.getData('/clubmembers/get/$clubId/false'); + Future training(int clubId) => _api.getData('/diary/$clubId'); + Future teams(int clubId) => _api.getData('/club-teams/club/$clubId'); + Future tournaments(int clubId) => _api.getData('/tournament/$clubId'); + Future orders(int clubId, int memberId) => + _api.getData('/member-orders/$clubId/$memberId'); + + Future createTraining(int clubId, Map body) => + _api.postObject('/diary/$clubId', data: body); + Future saveMember(int clubId, Map body) => + _api.postObject('/clubmembers/set/$clubId', data: body); + Future createOrder( + int clubId, + int memberId, + Map body, + ) => _api.postObject('/member-orders/$clubId/$memberId', data: body); + Future updateOrder( + int clubId, + int memberId, + int orderId, + Map body, + ) => _api.patchData('/member-orders/$clubId/$memberId/$orderId', data: body); + Future createTeam(int clubId, Map body) => + _api.postObject('/club-teams/club/$clubId', data: body); + Future createTournament(Map body) => + _api.postObject('/tournament', data: body); +} diff --git a/backend/desktop-app/lib/data/clubs/club_repository.dart b/desktop-app/lib/data/clubs/club_repository.dart similarity index 100% rename from backend/desktop-app/lib/data/clubs/club_repository.dart rename to desktop-app/lib/data/clubs/club_repository.dart diff --git a/desktop-app/lib/data/training/training_repository.dart b/desktop-app/lib/data/training/training_repository.dart new file mode 100644 index 00000000..dadbca47 --- /dev/null +++ b/desktop-app/lib/data/training/training_repository.dart @@ -0,0 +1,31 @@ +import '../../core/network/api_client.dart'; + +/// Narrow client for the existing diary and participant endpoints. +class TrainingRepository { + const TrainingRepository(this._api); + + final ApiClient _api; + + Future> diaryDates(int clubId) => + _api.getList('/diary/$clubId'); + Future> members(int clubId) => + _api.getList('/clubmembers/get/$clubId/false'); + Future> participants(int diaryDateId) => + _api.getList('/participants/$diaryDateId'); + + Future addAttendance({ + required int diaryDateId, + required int memberId, + }) => _api.post( + '/participants/add', + data: {'diaryDateId': diaryDateId, 'memberId': memberId}, + ); + + Future removeAttendance({ + required int diaryDateId, + required int memberId, + }) => _api.post( + '/participants/remove', + data: {'diaryDateId': diaryDateId, 'memberId': memberId}, + ); +} diff --git a/backend/desktop-app/lib/domain/README.md b/desktop-app/lib/domain/README.md similarity index 100% rename from backend/desktop-app/lib/domain/README.md rename to desktop-app/lib/domain/README.md diff --git a/backend/desktop-app/lib/domain/models/club.dart b/desktop-app/lib/domain/models/club.dart similarity index 100% rename from backend/desktop-app/lib/domain/models/club.dart rename to desktop-app/lib/domain/models/club.dart diff --git a/backend/desktop-app/lib/domain/models/club_permissions.dart b/desktop-app/lib/domain/models/club_permissions.dart similarity index 100% rename from backend/desktop-app/lib/domain/models/club_permissions.dart rename to desktop-app/lib/domain/models/club_permissions.dart diff --git a/backend/desktop-app/lib/features/auth/presentation/login_page.dart b/desktop-app/lib/features/auth/presentation/login_page.dart similarity index 100% rename from backend/desktop-app/lib/features/auth/presentation/login_page.dart rename to desktop-app/lib/features/auth/presentation/login_page.dart diff --git a/backend/desktop-app/lib/features/auth/presentation/password_reset_pages.dart b/desktop-app/lib/features/auth/presentation/password_reset_pages.dart similarity index 100% rename from backend/desktop-app/lib/features/auth/presentation/password_reset_pages.dart rename to desktop-app/lib/features/auth/presentation/password_reset_pages.dart diff --git a/backend/desktop-app/lib/features/clubs/presentation/club_selection_page.dart b/desktop-app/lib/features/clubs/presentation/club_selection_page.dart similarity index 100% rename from backend/desktop-app/lib/features/clubs/presentation/club_selection_page.dart rename to desktop-app/lib/features/clubs/presentation/club_selection_page.dart diff --git a/backend/desktop-app/lib/features/overview/presentation/club_overview_page.dart b/desktop-app/lib/features/overview/presentation/club_overview_page.dart similarity index 100% rename from backend/desktop-app/lib/features/overview/presentation/club_overview_page.dart rename to desktop-app/lib/features/overview/presentation/club_overview_page.dart diff --git a/backend/desktop-app/lib/features/shell/presentation/app_shell.dart b/desktop-app/lib/features/shell/presentation/app_shell.dart similarity index 98% rename from backend/desktop-app/lib/features/shell/presentation/app_shell.dart rename to desktop-app/lib/features/shell/presentation/app_shell.dart index 4dfaa681..11e12190 100644 --- a/backend/desktop-app/lib/features/shell/presentation/app_shell.dart +++ b/desktop-app/lib/features/shell/presentation/app_shell.dart @@ -4,6 +4,7 @@ import '../../../core/network/api_client.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/widgets/product_mark.dart'; import '../../overview/presentation/club_overview_page.dart'; +import '../../training/presentation/training_workbench_page.dart'; class AppShell extends StatefulWidget { const AppShell({super.key, this.onLogout, this.api, this.clubId}); @@ -597,6 +598,16 @@ class _Content extends StatelessWidget { ); Widget _pageBody(BuildContext context) { + if (page.title == 'Training' && + product.product == Product.trainer && + api != null && + clubId != null) { + return TrainingWorkbenchPage( + key: ValueKey('training-workbench-$clubId'), + clubId: clubId!, + api: api!, + ); + } final kind = _overviewKind(); if (kind != null && api != null && clubId != null) { return Container( diff --git a/desktop-app/lib/features/training/presentation/training_workbench_page.dart b/desktop-app/lib/features/training/presentation/training_workbench_page.dart new file mode 100644 index 00000000..b385b2b1 --- /dev/null +++ b/desktop-app/lib/features/training/presentation/training_workbench_page.dart @@ -0,0 +1,330 @@ +import 'package:flutter/material.dart'; + +import '../../../core/network/api_client.dart'; +import '../../../core/theme/app_theme.dart'; +import '../../../core/widgets/async_content.dart'; +import '../../../data/training/training_repository.dart'; + +/// Native workbench for reviewing a diary day and maintaining attendance. +class TrainingWorkbenchPage extends StatefulWidget { + const TrainingWorkbenchPage({ + super.key, + required this.clubId, + required this.api, + }); + + final int clubId; + final ApiClient api; + + @override + State createState() => _TrainingWorkbenchPageState(); +} + +class _TrainingWorkbenchPageState extends State { + late final TrainingRepository _repository; + late Future<_InitialTrainingData> _initialRequest; + Future>? _participantsRequest; + int? _selectedDateId; + int? _pendingMemberId; + + @override + void initState() { + super.initState(); + _repository = TrainingRepository(widget.api); + _initialRequest = _loadInitial(); + } + + Future<_InitialTrainingData> _loadInitial() async { + final values = await Future.wait([ + _repository.diaryDates(widget.clubId), + _repository.members(widget.clubId), + ]); + return _InitialTrainingData( + dates: _maps(values[0]), + members: _maps(values[1]), + ); + } + + void _selectDate(int dateId) { + setState(() { + _selectedDateId = dateId; + _participantsRequest = _repository.participants(dateId); + }); + } + + void _retry() => setState(() { + _selectedDateId = null; + _participantsRequest = null; + _initialRequest = _loadInitial(); + }); + + Future _changeAttendance(int dateId, int memberId, bool present) async { + setState(() => _pendingMemberId = memberId); + try { + if (present) { + await _repository.removeAttendance( + diaryDateId: dateId, + memberId: memberId, + ); + } else { + await _repository.addAttendance( + diaryDateId: dateId, + memberId: memberId, + ); + } + if (!mounted) return; + setState(() => _participantsRequest = _repository.participants(dateId)); + } catch (error) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Anwesenheit konnte nicht geändert werden: $error'), + ), + ); + } finally { + if (mounted) setState(() => _pendingMemberId = null); + } + } + + @override + Widget build(BuildContext context) => FutureBuilder<_InitialTrainingData>( + future: _initialRequest, + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const SizedBox(height: 300, child: AppLoadingView()); + } + if (snapshot.hasError) { + return SizedBox( + height: 300, + child: AppErrorView( + message: snapshot.error.toString(), + onRetry: _retry, + ), + ); + } + final data = snapshot.requireData; + if (data.dates.isEmpty) return const _NoTrainingDates(); + final currentDateId = _selectedDateId ?? _id(data.dates.first); + if (currentDateId == null) return const _NoTrainingDates(); + _participantsRequest ??= _repository.participants(currentDateId); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _DateSelector( + dates: data.dates, + selectedDateId: currentDateId, + onSelected: _selectDate, + ), + const SizedBox(height: 24), + FutureBuilder>( + future: _participantsRequest, + builder: (context, participantsSnapshot) { + if (participantsSnapshot.connectionState != + ConnectionState.done) { + return const SizedBox(height: 220, child: AppLoadingView()); + } + if (participantsSnapshot.hasError) { + return SizedBox( + height: 220, + child: AppErrorView( + message: participantsSnapshot.error.toString(), + onRetry: () => _selectDate(currentDateId), + ), + ); + } + return _AttendanceList( + dateId: currentDateId, + members: data.members, + participants: _maps(participantsSnapshot.data), + pendingMemberId: _pendingMemberId, + onChanged: _changeAttendance, + ); + }, + ), + ], + ); + }, + ); +} + +class _InitialTrainingData { + const _InitialTrainingData({required this.dates, required this.members}); + final List> dates; + final List> members; +} + +class _DateSelector extends StatelessWidget { + const _DateSelector({ + required this.dates, + required this.selectedDateId, + required this.onSelected, + }); + final List> dates; + final int selectedDateId; + final ValueChanged onSelected; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Trainingstag', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: dates.map((date) { + final id = _id(date); + if (id == null) return const SizedBox.shrink(); + return ChoiceChip( + label: Text(_dateLabel(date)), + selected: id == selectedDateId, + onSelected: (_) => onSelected(id), + ); + }).toList(), + ), + ], + ); +} + +class _AttendanceList extends StatelessWidget { + const _AttendanceList({ + required this.dateId, + required this.members, + required this.participants, + required this.pendingMemberId, + required this.onChanged, + }); + final int dateId; + final List> members; + final List> participants; + final int? pendingMemberId; + final Future Function(int, int, bool) onChanged; + + @override + Widget build(BuildContext context) { + final presentIds = participants + .where((participant) => participant['attendanceStatus'] == 'present') + .map(_id) + .whereType() + .toSet(); + final sortedMembers = [...members] + ..sort((a, b) => _memberName(a).compareTo(_memberName(b))); + if (sortedMembers.isEmpty) { + return const _EmptyAttendance( + message: 'Für diesen Verein sind keine Mitglieder vorhanden.', + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Anwesenheit', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 4), + Text( + '${presentIds.length} anwesend', + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: AppColors.muted), + ), + const SizedBox(height: 12), + DecoratedBox( + decoration: BoxDecoration( + color: AppColors.surface, + border: Border.all(color: AppColors.line), + borderRadius: BorderRadius.circular(10), + ), + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: sortedMembers.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, index) { + final member = sortedMembers[index]; + final memberId = _id(member); + if (memberId == null) return const SizedBox.shrink(); + final present = presentIds.contains(memberId); + final pending = pendingMemberId == memberId; + return ListTile( + leading: CircleAvatar( + child: Text(_initials(_memberName(member))), + ), + title: Text(_memberName(member)), + subtitle: Text(present ? 'Anwesend' : 'Nicht eingetragen'), + trailing: pending + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Switch( + value: present, + onChanged: (_) => onChanged(dateId, memberId, present), + ), + ); + }, + ), + ), + ], + ); + } +} + +class _NoTrainingDates extends StatelessWidget { + const _NoTrainingDates(); + @override + Widget build(BuildContext context) => const _EmptyAttendance( + message: 'Es sind noch keine Trainingstage im Tagebuch vorhanden.', + ); +} + +class _EmptyAttendance extends StatelessWidget { + const _EmptyAttendance({required this.message}); + final String message; + @override + Widget build(BuildContext context) => Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Text(message, textAlign: TextAlign.center), + ), + ); +} + +List> _maps(Object? value) => value is List + ? value + .whereType() + .map((entry) => Map.from(entry)) + .toList() + : const []; + +int? _id(Map value) => int.tryParse('${value['id'] ?? ''}'); + +String _memberName(Map member) { + final name = + [ + member['firstName'] ?? member['firstname'], + member['lastName'] ?? member['lastname'], + ] + .where((part) => part != null && part.toString().trim().isNotEmpty) + .join(' ') + .trim(); + return name.isNotEmpty + ? name + : (member['fullName']?.toString() ?? 'Mitglied ${member['id']}'); +} + +String _initials(String name) => name + .split(RegExp(r'\s+')) + .where((part) => part.isNotEmpty) + .take(2) + .map((part) => part[0]) + .join() + .toUpperCase(); + +String _dateLabel(Map date) { + final raw = date['date']?.toString(); + if (raw == null || raw.length < 10) return 'Training'; + final day = raw.substring(8, 10); + final month = raw.substring(5, 7); + final year = raw.substring(0, 4); + final start = date['trainingStart']?.toString(); + return '$day.$month.$year${start == null || start.isEmpty ? '' : ' · ${start.substring(0, start.length.clamp(0, 5))}'}'; +} diff --git a/backend/desktop-app/lib/main.dart b/desktop-app/lib/main.dart similarity index 100% rename from backend/desktop-app/lib/main.dart rename to desktop-app/lib/main.dart diff --git a/backend/desktop-app/lib/presentation/README.md b/desktop-app/lib/presentation/README.md similarity index 100% rename from backend/desktop-app/lib/presentation/README.md rename to desktop-app/lib/presentation/README.md diff --git a/backend/desktop-app/linux/.gitignore b/desktop-app/linux/.gitignore similarity index 100% rename from backend/desktop-app/linux/.gitignore rename to desktop-app/linux/.gitignore diff --git a/backend/desktop-app/linux/CMakeLists.txt b/desktop-app/linux/CMakeLists.txt similarity index 90% rename from backend/desktop-app/linux/CMakeLists.txt rename to desktop-app/linux/CMakeLists.txt index 01445db8..9fc8fdf8 100644 --- a/backend/desktop-app/linux/CMakeLists.txt +++ b/desktop-app/linux/CMakeLists.txt @@ -1,5 +1,17 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.13) + +# Some Linux environments export CXX=clang++ although clang is not installed. +# Prefer the available GNU compiler in that situation before CMake enables C++. +find_program(TT_TAGEBUCH_GCC_EXECUTABLE gcc) +find_program(TT_TAGEBUCH_GXX_EXECUTABLE g++) +if(TT_TAGEBUCH_GCC_EXECUTABLE) + set(CMAKE_C_COMPILER "${TT_TAGEBUCH_GCC_EXECUTABLE}" CACHE FILEPATH "C compiler" FORCE) +endif() +if(TT_TAGEBUCH_GXX_EXECUTABLE) + set(CMAKE_CXX_COMPILER "${TT_TAGEBUCH_GXX_EXECUTABLE}" CACHE FILEPATH "C++ compiler" FORCE) +endif() + project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change diff --git a/backend/desktop-app/linux/flutter/CMakeLists.txt b/desktop-app/linux/flutter/CMakeLists.txt similarity index 100% rename from backend/desktop-app/linux/flutter/CMakeLists.txt rename to desktop-app/linux/flutter/CMakeLists.txt diff --git a/backend/desktop-app/linux/flutter/generated_plugin_registrant.cc b/desktop-app/linux/flutter/generated_plugin_registrant.cc similarity index 100% rename from backend/desktop-app/linux/flutter/generated_plugin_registrant.cc rename to desktop-app/linux/flutter/generated_plugin_registrant.cc diff --git a/backend/desktop-app/linux/flutter/generated_plugin_registrant.h b/desktop-app/linux/flutter/generated_plugin_registrant.h similarity index 100% rename from backend/desktop-app/linux/flutter/generated_plugin_registrant.h rename to desktop-app/linux/flutter/generated_plugin_registrant.h diff --git a/backend/desktop-app/linux/flutter/generated_plugins.cmake b/desktop-app/linux/flutter/generated_plugins.cmake similarity index 100% rename from backend/desktop-app/linux/flutter/generated_plugins.cmake rename to desktop-app/linux/flutter/generated_plugins.cmake diff --git a/backend/desktop-app/linux/runner/CMakeLists.txt b/desktop-app/linux/runner/CMakeLists.txt similarity index 100% rename from backend/desktop-app/linux/runner/CMakeLists.txt rename to desktop-app/linux/runner/CMakeLists.txt diff --git a/backend/desktop-app/linux/runner/main.cc b/desktop-app/linux/runner/main.cc similarity index 100% rename from backend/desktop-app/linux/runner/main.cc rename to desktop-app/linux/runner/main.cc diff --git a/backend/desktop-app/linux/runner/my_application.cc b/desktop-app/linux/runner/my_application.cc similarity index 100% rename from backend/desktop-app/linux/runner/my_application.cc rename to desktop-app/linux/runner/my_application.cc diff --git a/backend/desktop-app/linux/runner/my_application.h b/desktop-app/linux/runner/my_application.h similarity index 100% rename from backend/desktop-app/linux/runner/my_application.h rename to desktop-app/linux/runner/my_application.h diff --git a/backend/desktop-app/macos/.gitignore b/desktop-app/macos/.gitignore similarity index 100% rename from backend/desktop-app/macos/.gitignore rename to desktop-app/macos/.gitignore diff --git a/backend/desktop-app/macos/Flutter/Flutter-Debug.xcconfig b/desktop-app/macos/Flutter/Flutter-Debug.xcconfig similarity index 100% rename from backend/desktop-app/macos/Flutter/Flutter-Debug.xcconfig rename to desktop-app/macos/Flutter/Flutter-Debug.xcconfig diff --git a/backend/desktop-app/macos/Flutter/Flutter-Release.xcconfig b/desktop-app/macos/Flutter/Flutter-Release.xcconfig similarity index 100% rename from backend/desktop-app/macos/Flutter/Flutter-Release.xcconfig rename to desktop-app/macos/Flutter/Flutter-Release.xcconfig diff --git a/backend/desktop-app/macos/Flutter/GeneratedPluginRegistrant.swift b/desktop-app/macos/Flutter/GeneratedPluginRegistrant.swift similarity index 100% rename from backend/desktop-app/macos/Flutter/GeneratedPluginRegistrant.swift rename to desktop-app/macos/Flutter/GeneratedPluginRegistrant.swift diff --git a/backend/desktop-app/macos/Runner.xcodeproj/project.pbxproj b/desktop-app/macos/Runner.xcodeproj/project.pbxproj similarity index 100% rename from backend/desktop-app/macos/Runner.xcodeproj/project.pbxproj rename to desktop-app/macos/Runner.xcodeproj/project.pbxproj diff --git a/backend/desktop-app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/desktop-app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from backend/desktop-app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to desktop-app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/backend/desktop-app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/desktop-app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from backend/desktop-app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to desktop-app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/backend/desktop-app/macos/Runner.xcworkspace/contents.xcworkspacedata b/desktop-app/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from backend/desktop-app/macos/Runner.xcworkspace/contents.xcworkspacedata rename to desktop-app/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/backend/desktop-app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/desktop-app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from backend/desktop-app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to desktop-app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/backend/desktop-app/macos/Runner/AppDelegate.swift b/desktop-app/macos/Runner/AppDelegate.swift similarity index 100% rename from backend/desktop-app/macos/Runner/AppDelegate.swift rename to desktop-app/macos/Runner/AppDelegate.swift diff --git a/backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png similarity index 100% rename from backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png diff --git a/backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png similarity index 100% rename from backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png diff --git a/backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png similarity index 100% rename from backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png diff --git a/backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png similarity index 100% rename from backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png diff --git a/backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png similarity index 100% rename from backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png similarity index 100% rename from backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png diff --git a/backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png similarity index 100% rename from backend/desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to desktop-app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/backend/desktop-app/macos/Runner/Base.lproj/MainMenu.xib b/desktop-app/macos/Runner/Base.lproj/MainMenu.xib similarity index 100% rename from backend/desktop-app/macos/Runner/Base.lproj/MainMenu.xib rename to desktop-app/macos/Runner/Base.lproj/MainMenu.xib diff --git a/backend/desktop-app/macos/Runner/Configs/AppInfo.xcconfig b/desktop-app/macos/Runner/Configs/AppInfo.xcconfig similarity index 100% rename from backend/desktop-app/macos/Runner/Configs/AppInfo.xcconfig rename to desktop-app/macos/Runner/Configs/AppInfo.xcconfig diff --git a/backend/desktop-app/macos/Runner/Configs/Debug.xcconfig b/desktop-app/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from backend/desktop-app/macos/Runner/Configs/Debug.xcconfig rename to desktop-app/macos/Runner/Configs/Debug.xcconfig diff --git a/backend/desktop-app/macos/Runner/Configs/Release.xcconfig b/desktop-app/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from backend/desktop-app/macos/Runner/Configs/Release.xcconfig rename to desktop-app/macos/Runner/Configs/Release.xcconfig diff --git a/backend/desktop-app/macos/Runner/Configs/Warnings.xcconfig b/desktop-app/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from backend/desktop-app/macos/Runner/Configs/Warnings.xcconfig rename to desktop-app/macos/Runner/Configs/Warnings.xcconfig diff --git a/backend/desktop-app/macos/Runner/DebugProfile.entitlements b/desktop-app/macos/Runner/DebugProfile.entitlements similarity index 100% rename from backend/desktop-app/macos/Runner/DebugProfile.entitlements rename to desktop-app/macos/Runner/DebugProfile.entitlements diff --git a/backend/desktop-app/macos/Runner/Info.plist b/desktop-app/macos/Runner/Info.plist similarity index 100% rename from backend/desktop-app/macos/Runner/Info.plist rename to desktop-app/macos/Runner/Info.plist diff --git a/backend/desktop-app/macos/Runner/MainFlutterWindow.swift b/desktop-app/macos/Runner/MainFlutterWindow.swift similarity index 100% rename from backend/desktop-app/macos/Runner/MainFlutterWindow.swift rename to desktop-app/macos/Runner/MainFlutterWindow.swift diff --git a/backend/desktop-app/macos/Runner/Release.entitlements b/desktop-app/macos/Runner/Release.entitlements similarity index 100% rename from backend/desktop-app/macos/Runner/Release.entitlements rename to desktop-app/macos/Runner/Release.entitlements diff --git a/backend/desktop-app/macos/RunnerTests/RunnerTests.swift b/desktop-app/macos/RunnerTests/RunnerTests.swift similarity index 100% rename from backend/desktop-app/macos/RunnerTests/RunnerTests.swift rename to desktop-app/macos/RunnerTests/RunnerTests.swift diff --git a/backend/desktop-app/pubspec.lock b/desktop-app/pubspec.lock similarity index 100% rename from backend/desktop-app/pubspec.lock rename to desktop-app/pubspec.lock diff --git a/backend/desktop-app/pubspec.yaml b/desktop-app/pubspec.yaml similarity index 100% rename from backend/desktop-app/pubspec.yaml rename to desktop-app/pubspec.yaml diff --git a/backend/desktop-app/test/widget_test.dart b/desktop-app/test/widget_test.dart similarity index 100% rename from backend/desktop-app/test/widget_test.dart rename to desktop-app/test/widget_test.dart diff --git a/backend/desktop-app/windows/.gitignore b/desktop-app/windows/.gitignore similarity index 100% rename from backend/desktop-app/windows/.gitignore rename to desktop-app/windows/.gitignore diff --git a/backend/desktop-app/windows/CMakeLists.txt b/desktop-app/windows/CMakeLists.txt similarity index 100% rename from backend/desktop-app/windows/CMakeLists.txt rename to desktop-app/windows/CMakeLists.txt diff --git a/backend/desktop-app/windows/flutter/CMakeLists.txt b/desktop-app/windows/flutter/CMakeLists.txt similarity index 100% rename from backend/desktop-app/windows/flutter/CMakeLists.txt rename to desktop-app/windows/flutter/CMakeLists.txt diff --git a/backend/desktop-app/windows/flutter/generated_plugin_registrant.cc b/desktop-app/windows/flutter/generated_plugin_registrant.cc similarity index 100% rename from backend/desktop-app/windows/flutter/generated_plugin_registrant.cc rename to desktop-app/windows/flutter/generated_plugin_registrant.cc diff --git a/backend/desktop-app/windows/flutter/generated_plugin_registrant.h b/desktop-app/windows/flutter/generated_plugin_registrant.h similarity index 100% rename from backend/desktop-app/windows/flutter/generated_plugin_registrant.h rename to desktop-app/windows/flutter/generated_plugin_registrant.h diff --git a/backend/desktop-app/windows/flutter/generated_plugins.cmake b/desktop-app/windows/flutter/generated_plugins.cmake similarity index 100% rename from backend/desktop-app/windows/flutter/generated_plugins.cmake rename to desktop-app/windows/flutter/generated_plugins.cmake diff --git a/backend/desktop-app/windows/runner/CMakeLists.txt b/desktop-app/windows/runner/CMakeLists.txt similarity index 100% rename from backend/desktop-app/windows/runner/CMakeLists.txt rename to desktop-app/windows/runner/CMakeLists.txt diff --git a/backend/desktop-app/windows/runner/Runner.rc b/desktop-app/windows/runner/Runner.rc similarity index 100% rename from backend/desktop-app/windows/runner/Runner.rc rename to desktop-app/windows/runner/Runner.rc diff --git a/backend/desktop-app/windows/runner/flutter_window.cpp b/desktop-app/windows/runner/flutter_window.cpp similarity index 100% rename from backend/desktop-app/windows/runner/flutter_window.cpp rename to desktop-app/windows/runner/flutter_window.cpp diff --git a/backend/desktop-app/windows/runner/flutter_window.h b/desktop-app/windows/runner/flutter_window.h similarity index 100% rename from backend/desktop-app/windows/runner/flutter_window.h rename to desktop-app/windows/runner/flutter_window.h diff --git a/backend/desktop-app/windows/runner/main.cpp b/desktop-app/windows/runner/main.cpp similarity index 100% rename from backend/desktop-app/windows/runner/main.cpp rename to desktop-app/windows/runner/main.cpp diff --git a/backend/desktop-app/windows/runner/resource.h b/desktop-app/windows/runner/resource.h similarity index 100% rename from backend/desktop-app/windows/runner/resource.h rename to desktop-app/windows/runner/resource.h diff --git a/backend/desktop-app/windows/runner/resources/app_icon.ico b/desktop-app/windows/runner/resources/app_icon.ico similarity index 100% rename from backend/desktop-app/windows/runner/resources/app_icon.ico rename to desktop-app/windows/runner/resources/app_icon.ico diff --git a/backend/desktop-app/windows/runner/runner.exe.manifest b/desktop-app/windows/runner/runner.exe.manifest similarity index 100% rename from backend/desktop-app/windows/runner/runner.exe.manifest rename to desktop-app/windows/runner/runner.exe.manifest diff --git a/backend/desktop-app/windows/runner/utils.cpp b/desktop-app/windows/runner/utils.cpp similarity index 100% rename from backend/desktop-app/windows/runner/utils.cpp rename to desktop-app/windows/runner/utils.cpp diff --git a/backend/desktop-app/windows/runner/utils.h b/desktop-app/windows/runner/utils.h similarity index 100% rename from backend/desktop-app/windows/runner/utils.h rename to desktop-app/windows/runner/utils.h diff --git a/backend/desktop-app/windows/runner/win32_window.cpp b/desktop-app/windows/runner/win32_window.cpp similarity index 100% rename from backend/desktop-app/windows/runner/win32_window.cpp rename to desktop-app/windows/runner/win32_window.cpp diff --git a/backend/desktop-app/windows/runner/win32_window.h b/desktop-app/windows/runner/win32_window.h similarity index 100% rename from backend/desktop-app/windows/runner/win32_window.h rename to desktop-app/windows/runner/win32_window.h