From c876405a8fde9d0d62018b71e2f732d480ff94d1 Mon Sep 17 00:00:00 2001 From: atseirjo Date: Wed, 28 Oct 2026 14:07:40 +0100 Subject: [PATCH] add some props an features --- lib/controller/anwendung_controller.dart | 12 ++ lib/models/anwendung_model.dart | 10 + lib/widgets/anwendung_form.dart | 21 +- lib/widgets/bauteil_filament_auswahl.dart | 228 +++++++++++++--------- pubspec.yaml | 2 +- test/anwendung_bauteil_test.dart | 6 + test/bauteil_bestand_test.dart | 48 +++++ 7 files changed, 231 insertions(+), 96 deletions(-) diff --git a/lib/controller/anwendung_controller.dart b/lib/controller/anwendung_controller.dart index 8061871..8b7c812 100644 --- a/lib/controller/anwendung_controller.dart +++ b/lib/controller/anwendung_controller.dart @@ -17,6 +17,8 @@ class AnwendungController extends GetxController { final formKey = GlobalKey(); final druckbettController = TextEditingController(); final druckController = TextEditingController(); + final empfohleneDrucktemperaturController = TextEditingController(); + final empfohlenerHeizbettbereichController = TextEditingController(); final bauraumController = TextEditingController(); final sonstigesController = TextEditingController(); final bauraumGeschlossen = false.obs; @@ -48,6 +50,10 @@ class AnwendungController extends GetxController { anwendungen.assignAll([profile]); druckbettController.text = profile.druckbettTemperatur.toString(); druckController.text = profile.druckTemperatur.toString(); + empfohleneDrucktemperaturController.text = + profile.empfohleneDrucktemperatur; + empfohlenerHeizbettbereichController.text = + profile.empfohlenerHeizbettbereich; bauraumController.text = profile.bauraumTemperatur?.toString() ?? ''; sonstigesController.text = profile.sonstiges; bauraumGeschlossen.value = profile.bauraumGeschlossen; @@ -67,6 +73,10 @@ class AnwendungController extends GetxController { erstelltAm: existing?.erstelltAm ?? DateTime.now(), druckbettTemperatur: InputValidators.number(druckbettController.text)!, druckTemperatur: InputValidators.number(druckController.text)!, + empfohleneDrucktemperatur: empfohleneDrucktemperaturController.text + .trim(), + empfohlenerHeizbettbereich: empfohlenerHeizbettbereichController.text + .trim(), bauraumGeschlossen: bauraumGeschlossen.value, bauraumTemperatur: InputValidators.number(bauraumController.text), sonstiges: sonstigesController.text.trim(), @@ -94,6 +104,8 @@ class AnwendungController extends GetxController { void onClose() { druckbettController.dispose(); druckController.dispose(); + empfohleneDrucktemperaturController.dispose(); + empfohlenerHeizbettbereichController.dispose(); bauraumController.dispose(); sonstigesController.dispose(); super.onClose(); diff --git a/lib/models/anwendung_model.dart b/lib/models/anwendung_model.dart index ebc4164..79782ca 100644 --- a/lib/models/anwendung_model.dart +++ b/lib/models/anwendung_model.dart @@ -4,6 +4,8 @@ class AnwendungModel { final DateTime erstelltAm; final double druckbettTemperatur; final double druckTemperatur; + final String empfohleneDrucktemperatur; + final String empfohlenerHeizbettbereich; final bool bauraumGeschlossen; final double? bauraumTemperatur; final String sonstiges; @@ -15,6 +17,8 @@ class AnwendungModel { required this.druckbettTemperatur, required this.druckTemperatur, required this.bauraumGeschlossen, + this.empfohleneDrucktemperatur = '', + this.empfohlenerHeizbettbereich = '', this.bauraumTemperatur, this.sonstiges = '', }); @@ -25,6 +29,8 @@ class AnwendungModel { 'erstelltAm': erstelltAm.toIso8601String(), 'druckbettTemperatur': druckbettTemperatur, 'druckTemperatur': druckTemperatur, + 'empfohleneDrucktemperatur': empfohleneDrucktemperatur, + 'empfohlenerHeizbettbereich': empfohlenerHeizbettbereich, 'bauraumGeschlossen': bauraumGeschlossen, 'bauraumTemperatur': bauraumTemperatur, 'sonstiges': sonstiges, @@ -36,6 +42,10 @@ class AnwendungModel { erstelltAm: DateTime.parse(json['erstelltAm'] as String), druckbettTemperatur: (json['druckbettTemperatur'] as num).toDouble(), druckTemperatur: (json['druckTemperatur'] as num).toDouble(), + empfohleneDrucktemperatur: + json['empfohleneDrucktemperatur'] as String? ?? '', + empfohlenerHeizbettbereich: + json['empfohlenerHeizbettbereich'] as String? ?? '', bauraumGeschlossen: json['bauraumGeschlossen'] as bool, bauraumTemperatur: (json['bauraumTemperatur'] as num?)?.toDouble(), sonstiges: json['sonstiges'] as String? ?? '', diff --git a/lib/widgets/anwendung_form.dart b/lib/widgets/anwendung_form.dart index b83d8bd..fd9211d 100644 --- a/lib/widgets/anwendung_form.dart +++ b/lib/widgets/anwendung_form.dart @@ -22,11 +22,30 @@ class AnwendungForm extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + EntryFieldGrid( + children: [ + TextFormField( + controller: controller.empfohleneDrucktemperaturController, + decoration: const InputDecoration( + labelText: 'Empfohlene Drucktemperatur von–bis', + hintText: 'z. B. 220–260 °C', + ), + ), + TextFormField( + controller: controller.empfohlenerHeizbettbereichController, + decoration: const InputDecoration( + labelText: 'Empfohlener Heizbettbereich von–bis', + hintText: 'z. B. 0–60 °C', + ), + ), + ], + ), + const SizedBox(height: 16), EntryFieldGrid( children: [ EntryNumberField( controller: controller.druckController, - label: 'Drucktemperatur', + label: 'Nozzletemperatur', unit: '°C', validator: InputValidators.positive, ), diff --git a/lib/widgets/bauteil_filament_auswahl.dart b/lib/widgets/bauteil_filament_auswahl.dart index 1df4c88..9eb276b 100644 --- a/lib/widgets/bauteil_filament_auswahl.dart +++ b/lib/widgets/bauteil_filament_auswahl.dart @@ -13,115 +13,155 @@ class BauteilFilamentAuswahl extends StatelessWidget { @override Widget build(BuildContext context) => Obx(() { - final ids = { - ...controller.filamente.map((item) => item.filamentId), - ...controller.selectedFilamente.keys, - }; - if (ids.isEmpty) { + final ids = controller.selectedFilamente.keys.toList(); + final availableFilamente = controller.filamente + .where( + (item) => !controller.selectedFilamente.containsKey(item.filamentId), + ) + .toList(); + if (controller.filamente.isEmpty && ids.isEmpty) { return const Text( 'Erfasse zuerst ein Filament in der Einkaufsverwaltung.', ); } return Column( - children: ids.map((id) { - final selected = controller.selectedFilamente.containsKey(id); - final profiles = controller.profilesFor(id); - final available = controller.verfuegbarGramm(id); - final checkbox = CheckboxListTile( - key: ValueKey('filament-$id'), - title: Text(controller.filamentLabel(id)), - subtitle: Text( - available == null - ? 'Startgewicht unbekannt' - : '${DisplayFormats.number(available)} g verfügbar', + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (availableFilamente.isNotEmpty) ...[ + DropdownButtonFormField( + key: ValueKey( + availableFilamente + .map((item) => item.filamentId) + .toList() + .toString(), + ), + isExpanded: true, + decoration: const InputDecoration(labelText: 'Filament hinzufügen'), + hint: const Text('Filament auswählen'), + items: availableFilamente + .map( + (item) => DropdownMenuItem( + value: item.filamentId, + child: Text( + controller.filamentLabel(item.filamentId), + overflow: TextOverflow.ellipsis, + ), + ), + ) + .toList(), + onChanged: (id) { + if (id != null) controller.toggleFilament(id, true); + }, ), - value: selected, - controlAffinity: ListTileControlAffinity.leading, - onChanged: (value) => controller.toggleFilament(id, value ?? false), - ); - final consumption = selected - ? Padding( - padding: const EdgeInsets.all(16), - child: EntryNumberField( - controller: controller.verbrauchController(id), - label: 'Verbrauch', - unit: 'g', - validator: InputValidators.positive, - ), - ) - : const SizedBox.shrink(); - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Material( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - clipBehavior: Clip.antiAlias, - child: Column( - children: [ - LayoutBuilder( - builder: (context, constraints) => - constraints.maxWidth >= 500 && selected - ? Row( - children: [ - Expanded(child: checkbox), - SizedBox(width: 190, child: consumption), - ], - ) - : Column(children: [checkbox, if (selected) consumption]), - ), - if (selected && profiles.isNotEmpty) - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: DropdownButtonFormField( - key: ValueKey( - 'profile-$id-${controller.selectedFilamente[id]}', - ), - initialValue: - profiles.any( - (item) => - item.anwendungId == - controller.selectedFilamente[id], + const SizedBox(height: 12), + ] else if (controller.filamente.isNotEmpty) ...[ + const Text('Alle Filamente wurden hinzugefügt.'), + const SizedBox(height: 12), + ], + if (ids.isEmpty) + const Text( + 'Noch keine Filamente hinzugefügt.', + style: TextStyle(color: AppTheme.muted), + ), + ...ids.map((id) { + final profiles = controller.profilesFor(id); + final available = controller.verfuegbarGramm(id); + final filament = ListTile( + key: ValueKey('filament-$id'), + title: Text(controller.filamentLabel(id)), + subtitle: Text( + available == null + ? 'Startgewicht unbekannt' + : '${DisplayFormats.number(available)} g verfügbar', + ), + trailing: IconButton( + tooltip: 'Filament entfernen', + icon: const Icon(Icons.close), + onPressed: () => controller.toggleFilament(id, false), + ), + ); + final consumption = Padding( + padding: const EdgeInsets.all(16), + child: EntryNumberField( + controller: controller.verbrauchController(id), + label: 'Verbrauch', + unit: 'g', + validator: InputValidators.positive, + ), + ); + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Material( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + LayoutBuilder( + builder: (context, constraints) => + constraints.maxWidth >= 500 + ? Row( + children: [ + Expanded(child: filament), + SizedBox(width: 190, child: consumption), + ], ) - ? controller.selectedFilamente[id] - : '', - isExpanded: true, - decoration: const InputDecoration( - labelText: 'Verarbeitungsprofil', - ), - items: [ - const DropdownMenuItem( - value: '', - child: Text('Ohne Profil'), + : Column(children: [filament, consumption]), + ), + if (profiles.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: DropdownButtonFormField( + key: ValueKey( + 'profile-$id-${controller.selectedFilamente[id]}', ), - ...profiles.map( - (profile) => DropdownMenuItem( - value: profile.anwendungId, - child: Text( - '${DisplayFormats.number(profile.druckTemperatur)} °C · Bett ${DisplayFormats.number(profile.druckbettTemperatur)} °C · ${DisplayFormats.date(profile.erstelltAm)}', - overflow: TextOverflow.ellipsis, + initialValue: + profiles.any( + (item) => + item.anwendungId == + controller.selectedFilamente[id], + ) + ? controller.selectedFilamente[id] + : '', + isExpanded: true, + decoration: const InputDecoration( + labelText: 'Verarbeitungsprofil', + ), + items: [ + const DropdownMenuItem( + value: '', + child: Text('Ohne Profil'), + ), + ...profiles.map( + (profile) => DropdownMenuItem( + value: profile.anwendungId, + child: Text( + '${DisplayFormats.number(profile.druckTemperatur)} °C · Bett ${DisplayFormats.number(profile.druckbettTemperatur)} °C · ${DisplayFormats.date(profile.erstelltAm)}', + overflow: TextOverflow.ellipsis, + ), ), ), + ], + onChanged: (value) => controller.selectProfile( + id, + value == '' ? null : value, ), - ], - onChanged: (value) => controller.selectProfile( - id, - value == '' ? null : value, ), ), - ), - if (selected && profiles.isEmpty) - const Padding( - padding: EdgeInsets.fromLTRB(16, 0, 16, 16), - child: Text( - 'Noch kein Profil. Einstellungen kannst du über das Filament in der Einkaufsübersicht erfassen.', - style: TextStyle(color: AppTheme.muted), + if (profiles.isEmpty) + const Padding( + padding: EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Text( + 'Noch kein Profil. Einstellungen kannst du über das Filament in der Einkaufsübersicht erfassen.', + style: TextStyle(color: AppTheme.muted), + ), ), - ), - ], + ], + ), ), - ), - ); - }).toList(), + ); + }), + ], ); }); } diff --git a/pubspec.yaml b/pubspec.yaml index 16978cc..e5af704 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.2+3 +version: 1.0.4+5 environment: sdk: ^3.13.0 diff --git a/test/anwendung_bauteil_test.dart b/test/anwendung_bauteil_test.dart index 30d9033..2df8296 100644 --- a/test/anwendung_bauteil_test.dart +++ b/test/anwendung_bauteil_test.dart @@ -107,6 +107,8 @@ void main() { final controller = Get.put(AnwendungController(filamentId: 'a')); await show(tester, const AnwendungView(), narrow: true); controller.druckController.text = '220,5'; + controller.empfohleneDrucktemperaturController.text = '220–260 °C'; + controller.empfohlenerHeizbettbereichController.text = '0–60 °C'; controller.druckbettController.text = '65'; controller.bauraumController.text = '40'; controller.bauraumGeschlossen.value = true; @@ -120,6 +122,8 @@ void main() { expect(saved.anwendungId, matches(uuidPattern)); expect(saved.filamentId, 'a'); expect(saved.druckTemperatur, 220.5); + expect(saved.empfohleneDrucktemperatur, '220–260 °C'); + expect(saved.empfohlenerHeizbettbereich, '0–60 °C'); expect(saved.druckbettTemperatur, 65); expect(saved.bauraumGeschlossen, isTrue); expect(saved.bauraumTemperatur, 40); @@ -131,6 +135,8 @@ void main() { ); expect(reloaded.anwendungen.single.anwendungId, saved.anwendungId); expect(reloaded.druckController.text, '220.5'); + expect(reloaded.empfohleneDrucktemperaturController.text, '220–260 °C'); + expect(reloaded.empfohlenerHeizbettbereichController.text, '0–60 °C'); expect(reloaded.druckbettController.text, '65.0'); expect(reloaded.bauraumController.text, '40.0'); expect(reloaded.bauraumGeschlossen.value, isTrue); diff --git a/test/bauteil_bestand_test.dart b/test/bauteil_bestand_test.dart index 7c5e777..9e13ea7 100644 --- a/test/bauteil_bestand_test.dart +++ b/test/bauteil_bestand_test.dart @@ -16,6 +16,7 @@ import 'package:ertl_filament_verwaltung/services/bauteil_photo_service.dart'; import 'package:ertl_filament_verwaltung/services/get_storage_services.dart'; import 'package:ertl_filament_verwaltung/theme/app_theme.dart'; import 'package:ertl_filament_verwaltung/widgets/bauteil_card.dart'; +import 'package:ertl_filament_verwaltung/widgets/bauteil_filament_auswahl.dart'; import 'package:ertl_filament_verwaltung/widgets/bauteil_photo.dart'; import 'helpers/memory_storage_service.dart'; @@ -123,6 +124,53 @@ void main() { home.filamente.firstWhere((item) => item.filamentId == id), )!; + testWidgets('Dropdown adds only selected filaments and supports removal', ( + tester, + ) async { + await tester.pumpWidget( + GetMaterialApp( + theme: AppTheme.light, + home: Scaffold( + body: SingleChildScrollView( + child: BauteilFilamentAuswahl(controller: controller), + ), + ), + ), + ); + expect(find.byKey(const ValueKey('filament-a')), findsNothing); + expect(find.byKey(const ValueKey('filament-b')), findsNothing); + + Future add(String id) async { + await tester.tap(find.byType(DropdownButtonFormField)); + await tester.pumpAndSettle(); + await tester.tap(find.text(controller.filamentLabel(id)).last); + await tester.pumpAndSettle(); + } + + await add('a'); + controller.verbrauchController('a').text = '25'; + expect(find.byKey(const ValueKey('filament-a')), findsOneWidget); + expect(find.byKey(const ValueKey('filament-b')), findsNothing); + expect(controller.selectedFilamente.keys, ['a']); + + await add('b'); + expect(find.byType(DropdownButtonFormField), findsNothing); + expect(find.byKey(const ValueKey('filament-b')), findsOneWidget); + await tester.tap(find.byTooltip('Filament entfernen').first); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('filament-a')), findsNothing); + expect(controller.selectedFilamente.keys, ['b']); + + await add('a'); + expect(controller.verbrauchController('a').text, '25'); + controller.resetForm(); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('filament-a')), findsNothing); + expect(find.byKey(const ValueKey('filament-b')), findsNothing); + expect(find.byType(DropdownButtonFormField), findsOneWidget); + expect(tester.takeException(), isNull); + }); + test( 'Legacy records retain unknown usage and old weight units are supported', () {