add some props an features

This commit is contained in:
atseirjo
2026-10-28 14:07:40 +01:00
parent 77fbe16867
commit c876405a8f
7 changed files with 231 additions and 96 deletions
+12
View File
@@ -17,6 +17,8 @@ class AnwendungController extends GetxController {
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
final druckbettController = TextEditingController(); final druckbettController = TextEditingController();
final druckController = TextEditingController(); final druckController = TextEditingController();
final empfohleneDrucktemperaturController = TextEditingController();
final empfohlenerHeizbettbereichController = TextEditingController();
final bauraumController = TextEditingController(); final bauraumController = TextEditingController();
final sonstigesController = TextEditingController(); final sonstigesController = TextEditingController();
final bauraumGeschlossen = false.obs; final bauraumGeschlossen = false.obs;
@@ -48,6 +50,10 @@ class AnwendungController extends GetxController {
anwendungen.assignAll([profile]); anwendungen.assignAll([profile]);
druckbettController.text = profile.druckbettTemperatur.toString(); druckbettController.text = profile.druckbettTemperatur.toString();
druckController.text = profile.druckTemperatur.toString(); druckController.text = profile.druckTemperatur.toString();
empfohleneDrucktemperaturController.text =
profile.empfohleneDrucktemperatur;
empfohlenerHeizbettbereichController.text =
profile.empfohlenerHeizbettbereich;
bauraumController.text = profile.bauraumTemperatur?.toString() ?? ''; bauraumController.text = profile.bauraumTemperatur?.toString() ?? '';
sonstigesController.text = profile.sonstiges; sonstigesController.text = profile.sonstiges;
bauraumGeschlossen.value = profile.bauraumGeschlossen; bauraumGeschlossen.value = profile.bauraumGeschlossen;
@@ -67,6 +73,10 @@ class AnwendungController extends GetxController {
erstelltAm: existing?.erstelltAm ?? DateTime.now(), erstelltAm: existing?.erstelltAm ?? DateTime.now(),
druckbettTemperatur: InputValidators.number(druckbettController.text)!, druckbettTemperatur: InputValidators.number(druckbettController.text)!,
druckTemperatur: InputValidators.number(druckController.text)!, druckTemperatur: InputValidators.number(druckController.text)!,
empfohleneDrucktemperatur: empfohleneDrucktemperaturController.text
.trim(),
empfohlenerHeizbettbereich: empfohlenerHeizbettbereichController.text
.trim(),
bauraumGeschlossen: bauraumGeschlossen.value, bauraumGeschlossen: bauraumGeschlossen.value,
bauraumTemperatur: InputValidators.number(bauraumController.text), bauraumTemperatur: InputValidators.number(bauraumController.text),
sonstiges: sonstigesController.text.trim(), sonstiges: sonstigesController.text.trim(),
@@ -94,6 +104,8 @@ class AnwendungController extends GetxController {
void onClose() { void onClose() {
druckbettController.dispose(); druckbettController.dispose();
druckController.dispose(); druckController.dispose();
empfohleneDrucktemperaturController.dispose();
empfohlenerHeizbettbereichController.dispose();
bauraumController.dispose(); bauraumController.dispose();
sonstigesController.dispose(); sonstigesController.dispose();
super.onClose(); super.onClose();
+10
View File
@@ -4,6 +4,8 @@ class AnwendungModel {
final DateTime erstelltAm; final DateTime erstelltAm;
final double druckbettTemperatur; final double druckbettTemperatur;
final double druckTemperatur; final double druckTemperatur;
final String empfohleneDrucktemperatur;
final String empfohlenerHeizbettbereich;
final bool bauraumGeschlossen; final bool bauraumGeschlossen;
final double? bauraumTemperatur; final double? bauraumTemperatur;
final String sonstiges; final String sonstiges;
@@ -15,6 +17,8 @@ class AnwendungModel {
required this.druckbettTemperatur, required this.druckbettTemperatur,
required this.druckTemperatur, required this.druckTemperatur,
required this.bauraumGeschlossen, required this.bauraumGeschlossen,
this.empfohleneDrucktemperatur = '',
this.empfohlenerHeizbettbereich = '',
this.bauraumTemperatur, this.bauraumTemperatur,
this.sonstiges = '', this.sonstiges = '',
}); });
@@ -25,6 +29,8 @@ class AnwendungModel {
'erstelltAm': erstelltAm.toIso8601String(), 'erstelltAm': erstelltAm.toIso8601String(),
'druckbettTemperatur': druckbettTemperatur, 'druckbettTemperatur': druckbettTemperatur,
'druckTemperatur': druckTemperatur, 'druckTemperatur': druckTemperatur,
'empfohleneDrucktemperatur': empfohleneDrucktemperatur,
'empfohlenerHeizbettbereich': empfohlenerHeizbettbereich,
'bauraumGeschlossen': bauraumGeschlossen, 'bauraumGeschlossen': bauraumGeschlossen,
'bauraumTemperatur': bauraumTemperatur, 'bauraumTemperatur': bauraumTemperatur,
'sonstiges': sonstiges, 'sonstiges': sonstiges,
@@ -36,6 +42,10 @@ class AnwendungModel {
erstelltAm: DateTime.parse(json['erstelltAm'] as String), erstelltAm: DateTime.parse(json['erstelltAm'] as String),
druckbettTemperatur: (json['druckbettTemperatur'] as num).toDouble(), druckbettTemperatur: (json['druckbettTemperatur'] as num).toDouble(),
druckTemperatur: (json['druckTemperatur'] as num).toDouble(), druckTemperatur: (json['druckTemperatur'] as num).toDouble(),
empfohleneDrucktemperatur:
json['empfohleneDrucktemperatur'] as String? ?? '',
empfohlenerHeizbettbereich:
json['empfohlenerHeizbettbereich'] as String? ?? '',
bauraumGeschlossen: json['bauraumGeschlossen'] as bool, bauraumGeschlossen: json['bauraumGeschlossen'] as bool,
bauraumTemperatur: (json['bauraumTemperatur'] as num?)?.toDouble(), bauraumTemperatur: (json['bauraumTemperatur'] as num?)?.toDouble(),
sonstiges: json['sonstiges'] as String? ?? '', sonstiges: json['sonstiges'] as String? ?? '',
+20 -1
View File
@@ -22,11 +22,30 @@ class AnwendungForm extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ 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( EntryFieldGrid(
children: [ children: [
EntryNumberField( EntryNumberField(
controller: controller.druckController, controller: controller.druckController,
label: 'Drucktemperatur', label: 'Nozzletemperatur',
unit: '°C', unit: '°C',
validator: InputValidators.positive, validator: InputValidators.positive,
), ),
+134 -94
View File
@@ -13,115 +13,155 @@ class BauteilFilamentAuswahl extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Obx(() { Widget build(BuildContext context) => Obx(() {
final ids = { final ids = controller.selectedFilamente.keys.toList();
...controller.filamente.map((item) => item.filamentId), final availableFilamente = controller.filamente
...controller.selectedFilamente.keys, .where(
}; (item) => !controller.selectedFilamente.containsKey(item.filamentId),
if (ids.isEmpty) { )
.toList();
if (controller.filamente.isEmpty && ids.isEmpty) {
return const Text( return const Text(
'Erfasse zuerst ein Filament in der Einkaufsverwaltung.', 'Erfasse zuerst ein Filament in der Einkaufsverwaltung.',
); );
} }
return Column( return Column(
children: ids.map((id) { crossAxisAlignment: CrossAxisAlignment.stretch,
final selected = controller.selectedFilamente.containsKey(id); children: [
final profiles = controller.profilesFor(id); if (availableFilamente.isNotEmpty) ...[
final available = controller.verfuegbarGramm(id); DropdownButtonFormField<String>(
final checkbox = CheckboxListTile( key: ValueKey(
key: ValueKey('filament-$id'), availableFilamente
title: Text(controller.filamentLabel(id)), .map((item) => item.filamentId)
subtitle: Text( .toList()
available == null .toString(),
? 'Startgewicht unbekannt' ),
: '${DisplayFormats.number(available)} g verfügbar', 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, const SizedBox(height: 12),
controlAffinity: ListTileControlAffinity.leading, ] else if (controller.filamente.isNotEmpty) ...[
onChanged: (value) => controller.toggleFilament(id, value ?? false), const Text('Alle Filamente wurden hinzugefügt.'),
); const SizedBox(height: 12),
final consumption = selected ],
? Padding( if (ids.isEmpty)
padding: const EdgeInsets.all(16), const Text(
child: EntryNumberField( 'Noch keine Filamente hinzugefügt.',
controller: controller.verbrauchController(id), style: TextStyle(color: AppTheme.muted),
label: 'Verbrauch', ),
unit: 'g', ...ids.map((id) {
validator: InputValidators.positive, final profiles = controller.profilesFor(id);
), final available = controller.verfuegbarGramm(id);
) final filament = ListTile(
: const SizedBox.shrink(); key: ValueKey('filament-$id'),
return Padding( title: Text(controller.filamentLabel(id)),
padding: const EdgeInsets.only(bottom: 12), subtitle: Text(
child: Material( available == null
color: Colors.white, ? 'Startgewicht unbekannt'
borderRadius: BorderRadius.circular(12), : '${DisplayFormats.number(available)} g verfügbar',
clipBehavior: Clip.antiAlias, ),
child: Column( trailing: IconButton(
children: [ tooltip: 'Filament entfernen',
LayoutBuilder( icon: const Icon(Icons.close),
builder: (context, constraints) => onPressed: () => controller.toggleFilament(id, false),
constraints.maxWidth >= 500 && selected ),
? Row( );
children: [ final consumption = Padding(
Expanded(child: checkbox), padding: const EdgeInsets.all(16),
SizedBox(width: 190, child: consumption), child: EntryNumberField(
], controller: controller.verbrauchController(id),
) label: 'Verbrauch',
: Column(children: [checkbox, if (selected) consumption]), unit: 'g',
), validator: InputValidators.positive,
if (selected && profiles.isNotEmpty) ),
Padding( );
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), return Padding(
child: DropdownButtonFormField<String>( padding: const EdgeInsets.only(bottom: 12),
key: ValueKey( child: Material(
'profile-$id-${controller.selectedFilamente[id]}', color: Colors.white,
), borderRadius: BorderRadius.circular(12),
initialValue: clipBehavior: Clip.antiAlias,
profiles.any( child: Column(
(item) => children: [
item.anwendungId == LayoutBuilder(
controller.selectedFilamente[id], builder: (context, constraints) =>
constraints.maxWidth >= 500
? Row(
children: [
Expanded(child: filament),
SizedBox(width: 190, child: consumption),
],
) )
? controller.selectedFilamente[id] : Column(children: [filament, consumption]),
: '', ),
isExpanded: true, if (profiles.isNotEmpty)
decoration: const InputDecoration( Padding(
labelText: 'Verarbeitungsprofil', padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
), child: DropdownButtonFormField<String>(
items: [ key: ValueKey(
const DropdownMenuItem( 'profile-$id-${controller.selectedFilamente[id]}',
value: '',
child: Text('Ohne Profil'),
), ),
...profiles.map( initialValue:
(profile) => DropdownMenuItem( profiles.any(
value: profile.anwendungId, (item) =>
child: Text( item.anwendungId ==
'${DisplayFormats.number(profile.druckTemperatur)} °C · Bett ${DisplayFormats.number(profile.druckbettTemperatur)} °C · ${DisplayFormats.date(profile.erstelltAm)}', controller.selectedFilamente[id],
overflow: TextOverflow.ellipsis, )
? 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 (profiles.isEmpty)
if (selected && profiles.isEmpty) const Padding(
const Padding( padding: EdgeInsets.fromLTRB(16, 0, 16, 16),
padding: EdgeInsets.fromLTRB(16, 0, 16, 16), child: Text(
child: Text( 'Noch kein Profil. Einstellungen kannst du über das Filament in der Einkaufsübersicht erfassen.',
'Noch kein Profil. Einstellungen kannst du über das Filament in der Einkaufsübersicht erfassen.', style: TextStyle(color: AppTheme.muted),
style: TextStyle(color: AppTheme.muted), ),
), ),
), ],
], ),
), ),
), );
); }),
}).toList(), ],
); );
}); });
} }
+1 -1
View File
@@ -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 # 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 # 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. # 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: environment:
sdk: ^3.13.0 sdk: ^3.13.0
+6
View File
@@ -107,6 +107,8 @@ void main() {
final controller = Get.put(AnwendungController(filamentId: 'a')); final controller = Get.put(AnwendungController(filamentId: 'a'));
await show(tester, const AnwendungView(), narrow: true); await show(tester, const AnwendungView(), narrow: true);
controller.druckController.text = '220,5'; controller.druckController.text = '220,5';
controller.empfohleneDrucktemperaturController.text = '220–260 °C';
controller.empfohlenerHeizbettbereichController.text = '0–60 °C';
controller.druckbettController.text = '65'; controller.druckbettController.text = '65';
controller.bauraumController.text = '40'; controller.bauraumController.text = '40';
controller.bauraumGeschlossen.value = true; controller.bauraumGeschlossen.value = true;
@@ -120,6 +122,8 @@ void main() {
expect(saved.anwendungId, matches(uuidPattern)); expect(saved.anwendungId, matches(uuidPattern));
expect(saved.filamentId, 'a'); expect(saved.filamentId, 'a');
expect(saved.druckTemperatur, 220.5); expect(saved.druckTemperatur, 220.5);
expect(saved.empfohleneDrucktemperatur, '220–260 °C');
expect(saved.empfohlenerHeizbettbereich, '0–60 °C');
expect(saved.druckbettTemperatur, 65); expect(saved.druckbettTemperatur, 65);
expect(saved.bauraumGeschlossen, isTrue); expect(saved.bauraumGeschlossen, isTrue);
expect(saved.bauraumTemperatur, 40); expect(saved.bauraumTemperatur, 40);
@@ -131,6 +135,8 @@ void main() {
); );
expect(reloaded.anwendungen.single.anwendungId, saved.anwendungId); expect(reloaded.anwendungen.single.anwendungId, saved.anwendungId);
expect(reloaded.druckController.text, '220.5'); 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.druckbettController.text, '65.0');
expect(reloaded.bauraumController.text, '40.0'); expect(reloaded.bauraumController.text, '40.0');
expect(reloaded.bauraumGeschlossen.value, isTrue); expect(reloaded.bauraumGeschlossen.value, isTrue);
+48
View File
@@ -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/services/get_storage_services.dart';
import 'package:ertl_filament_verwaltung/theme/app_theme.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_card.dart';
import 'package:ertl_filament_verwaltung/widgets/bauteil_filament_auswahl.dart';
import 'package:ertl_filament_verwaltung/widgets/bauteil_photo.dart'; import 'package:ertl_filament_verwaltung/widgets/bauteil_photo.dart';
import 'helpers/memory_storage_service.dart'; import 'helpers/memory_storage_service.dart';
@@ -123,6 +124,53 @@ void main() {
home.filamente.firstWhere((item) => item.filamentId == id), 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<void> add(String id) async {
await tester.tap(find.byType(DropdownButtonFormField<String>));
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<String>), 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<String>), findsOneWidget);
expect(tester.takeException(), isNull);
});
test( test(
'Legacy records retain unknown usage and old weight units are supported', 'Legacy records retain unknown usage and old weight units are supported',
() { () {