add some new features
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../controller/abfall_controller.dart';
|
||||
import '../helper/input_validators.dart';
|
||||
import 'entry_panel.dart';
|
||||
|
||||
class AbfallForm extends StatelessWidget {
|
||||
const AbfallForm({super.key, required this.controller});
|
||||
final AbfallController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() {
|
||||
final date = controller.datumZeit.value;
|
||||
final busy = controller.isSaving.value;
|
||||
final editing = controller.editingId.value != null;
|
||||
return EntryPanel(
|
||||
title: editing ? 'Abfall bearbeiten' : 'Abfall erfassen',
|
||||
subtitle: 'Erfasse zusätzlichen Abfall. Abfall aus gespeicherten Bauteilen erscheint automatisch in der Übersicht.',
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
EntryFieldGrid(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: busy
|
||||
? null
|
||||
: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: date,
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime(2100, 12, 31),
|
||||
helpText: 'Datum auswählen',
|
||||
cancelText: 'Abbrechen',
|
||||
confirmText: 'Übernehmen',
|
||||
);
|
||||
if (picked != null && context.mounted) {
|
||||
controller.setDatum(picked);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.calendar_today_outlined),
|
||||
label: Text(
|
||||
'Datum: ${DateFormat('dd.MM.yyyy').format(date)}',
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: busy
|
||||
? null
|
||||
: () async {
|
||||
final picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(date),
|
||||
helpText: 'Uhrzeit auswählen',
|
||||
cancelText: 'Abbrechen',
|
||||
confirmText: 'Übernehmen',
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(context)
|
||||
.copyWith(alwaysUse24HourFormat: true),
|
||||
child: child!,
|
||||
),
|
||||
);
|
||||
if (picked != null && context.mounted) {
|
||||
controller.setUhrzeit(picked);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.schedule),
|
||||
label: Text('Uhrzeit: ${DateFormat('HH:mm').format(date)}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Durchschnittlicher Preis pro kg',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
EntryNumberField(
|
||||
controller: controller.kilopreisController,
|
||||
label: 'Kilopreis (optional)',
|
||||
unit: '€/kg',
|
||||
validator: (value) =>
|
||||
InputValidators.nonNegative(value, optional: true),
|
||||
onChanged: controller.berechneKosten,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
EntryFieldGrid(
|
||||
children: [
|
||||
EntryNumberField(
|
||||
controller: controller.gewichtController,
|
||||
onChanged: controller.berechneKosten,
|
||||
label: 'Abfallgewicht',
|
||||
unit: 'g',
|
||||
validator: InputValidators.positive,
|
||||
),
|
||||
EntryNumberField(
|
||||
controller: controller.kostenController,
|
||||
label: 'Abfallkosten',
|
||||
unit: '€',
|
||||
validator: InputValidators.nonNegative,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Abfallkosten sind manuell änderbar. Bei Änderung von Gewicht oder Kilopreis werden sie neu berechnet.',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: busy ? null : controller.saveAbfall,
|
||||
icon: Icon(editing ? Icons.check : Icons.add),
|
||||
label: Text(
|
||||
busy
|
||||
? 'Wird gespeichert …'
|
||||
: editing
|
||||
? 'Änderungen speichern'
|
||||
: 'Abfall speichern',
|
||||
),
|
||||
),
|
||||
if (editing)
|
||||
TextButton.icon(
|
||||
onPressed: busy ? null : controller.resetForm,
|
||||
icon: const Icon(Icons.close),
|
||||
label: const Text('Bearbeitung abbrechen'),
|
||||
),
|
||||
EntryMessage(
|
||||
message: controller.message.value,
|
||||
hasError: controller.hasError.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controller/abfall_controller.dart';
|
||||
import '../helper/display_formats.dart';
|
||||
import '../models/abfall_model.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import 'entry_panel.dart';
|
||||
|
||||
class AbfallInventory extends StatelessWidget {
|
||||
const AbfallInventory({super.key, required this.controller});
|
||||
final AbfallController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => DecoratedSliver(
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.inventorySurface,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
sliver: SliverPadding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
sliver: Obx(() {
|
||||
final entries = controller.abfaelle.toList();
|
||||
final busy = controller.isSaving.value;
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Abfallübersicht',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 25,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${entries.length} Einträge · die neuesten zuerst.',
|
||||
style: const TextStyle(color: Color(0xFFB9CCD9)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
_AbfallStat(
|
||||
label: 'Summe Abfall',
|
||||
value:
|
||||
'${DisplayFormats.number(controller.summeAbfall)} g',
|
||||
),
|
||||
_AbfallStat(
|
||||
label: 'Summe Abfallkosten',
|
||||
value: DisplayFormats.currency(controller.summeKosten),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (entries.any((entry) => entry.kostenUnbekannt)) ...[
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Bei einigen älteren Bauteilen fehlen Filamentdaten. Ihr Gewicht ist enthalten; ihre unbekannten Kosten fehlen in der Kostensumme.',
|
||||
style: TextStyle(color: Color(0xFFB9CCD9), height: 1.5),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (entries.isEmpty)
|
||||
const SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
child: Text(
|
||||
'Noch kein Abfall gespeichert. Erfasse deinen ersten Eintrag.',
|
||||
style: TextStyle(color: Color(0xFFB9CCD9), height: 1.5),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverList.separated(
|
||||
itemCount: entries.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final entry = entries[index];
|
||||
return SavedEntryCard(
|
||||
key: ValueKey(entry.abfallId),
|
||||
title:
|
||||
'${DisplayFormats.number(entry.gewichtGramm)} g Abfall',
|
||||
subtitle: DisplayFormats.date(entry.datumZeit),
|
||||
notes: entry.ausBauteil
|
||||
? 'Aus Bauteil „${entry.bauteilName}“. Änderungen und Löschen über die Bauteilverwaltung.${entry.kostenNachtraeglich ? ' Kosten nachträglich aus den aktuellen Rollenpreisen und Startgewichten berechnet.' : ''}'
|
||||
: '',
|
||||
details: [
|
||||
entry.kostenUnbekannt
|
||||
? 'Abfallkosten: nicht ermittelbar'
|
||||
: 'Abfallkosten: ${DisplayFormats.currency(entry.kosten)}',
|
||||
],
|
||||
trailing: entry.ausBauteil
|
||||
? null
|
||||
: PopupMenuButton<String>(
|
||||
tooltip: 'Abfalleintrag verwalten',
|
||||
enabled: !busy,
|
||||
iconColor: AppTheme.ink,
|
||||
onSelected: (action) {
|
||||
if (action == 'edit') {
|
||||
controller.editAbfall(entry);
|
||||
} else if (action == 'delete') {
|
||||
_confirmDelete(context, entry);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => const [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Text('Bearbeiten'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text('Löschen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> _confirmDelete(BuildContext context, AbfallModel entry) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Abfall löschen?'),
|
||||
content: Text(
|
||||
'${DisplayFormats.number(entry.gewichtGramm)} g Abfall vom ${DisplayFormats.date(entry.datumZeit)} löschen?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Löschen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) await controller.deleteAbfall(entry.abfallId);
|
||||
}
|
||||
}
|
||||
|
||||
class _AbfallStat extends StatelessWidget {
|
||||
const _AbfallStat({required this.label, required this.value});
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF243E50),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(color: Color(0xFFB9CCD9), fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -110,6 +110,18 @@ class BauteilCard extends StatelessWidget {
|
||||
'Bauteil ${DisplayFormats.number(bauteil.gewichtGramm)} g',
|
||||
),
|
||||
_detail('Abfall ${DisplayFormats.number(bauteil.abfallGramm)} g'),
|
||||
if (bauteil.materialKosten != null)
|
||||
_detail(
|
||||
'Filamentkosten ${DisplayFormats.currency(bauteil.materialKosten!)}',
|
||||
),
|
||||
if (bauteil.aufschlagProzent != null)
|
||||
_detail(
|
||||
'Aufschlag ${DisplayFormats.number(bauteil.aufschlagProzent!)} %',
|
||||
),
|
||||
if (bauteil.abfallGramm > 0 && bauteil.abfallKosten != null)
|
||||
_detail(
|
||||
'Abfallkosten ${DisplayFormats.currency(bauteil.abfallKosten!)}',
|
||||
),
|
||||
_detail(
|
||||
'Gesamt ${DisplayFormats.number(bauteil.gesamtverbrauchGramm)} g',
|
||||
),
|
||||
|
||||
@@ -11,6 +11,29 @@ class BauteilFilamentAuswahl extends StatelessWidget {
|
||||
const BauteilFilamentAuswahl({super.key, required this.controller});
|
||||
final BauteilController controller;
|
||||
|
||||
Future<void> _waehleFilament(BuildContext context, String id) async {
|
||||
controller.toggleFilament(id, true);
|
||||
final warnung = controller.pruefeFilamentVorrat();
|
||||
if (warnung == null || !context.mounted) return;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => PopScope(
|
||||
canPop: false,
|
||||
child: AlertDialog(
|
||||
title: const Text('Achtung: zu wenig Filament'),
|
||||
content: Text(warnung),
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() {
|
||||
final ids = controller.selectedFilamente.keys.toList();
|
||||
@@ -36,21 +59,41 @@ class BauteilFilamentAuswahl extends StatelessWidget {
|
||||
.toString(),
|
||||
),
|
||||
isExpanded: true,
|
||||
itemHeight: 64,
|
||||
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,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
controller.filamentLabel(item.filamentId),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
controller.restgewichtGramm(item.filamentId) == null
|
||||
? 'Restgewicht unbekannt'
|
||||
: 'Restgewicht: ${DisplayFormats.number(controller.restgewichtGramm(item.filamentId)!)} g',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.teal,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (id) {
|
||||
if (id != null) controller.toggleFilament(id, true);
|
||||
if (id != null) _waehleFilament(context, id);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../helper/input_validators.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import 'bauteil_filament_auswahl.dart';
|
||||
import 'bauteil_photo_picker.dart';
|
||||
import 'bauteil_preis_uebersicht.dart';
|
||||
import 'entry_panel.dart';
|
||||
|
||||
class BauteilForm extends StatelessWidget {
|
||||
@@ -65,14 +66,18 @@ class BauteilForm extends StatelessWidget {
|
||||
validator: InputValidators.nonNegative,
|
||||
),
|
||||
EntryNumberField(
|
||||
controller: controller.gesamtpreisController,
|
||||
label: 'Gesamtpreis (optional)',
|
||||
unit: '€',
|
||||
validator: (value) =>
|
||||
InputValidators.nonNegative(value, optional: true),
|
||||
controller: controller.aufschlagController,
|
||||
label: 'Aufschlag',
|
||||
unit: '%',
|
||||
validator: InputValidators.nonNegative,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Abfall wird automatisch in die Abfallverwaltung übernommen. Seine Kosten ergeben sich aus Einkaufspreis und Startgewicht der verwendeten Rollen. Bei mehreren Filamenten wird der Abfall nach ihrem Verbrauch aufgeteilt.',
|
||||
style: TextStyle(color: AppTheme.muted, height: 1.5),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
BauteilPhotoPicker(controller: controller),
|
||||
const SizedBox(height: 24),
|
||||
@@ -87,7 +92,9 @@ class BauteilForm extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
BauteilFilamentAuswahl(controller: controller),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 16),
|
||||
BauteilPreisUebersicht(controller: controller),
|
||||
const SizedBox(height: 16),
|
||||
Obx(
|
||||
() => FilledButton.icon(
|
||||
onPressed:
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controller/bauteil_controller.dart';
|
||||
import '../helper/display_formats.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class BauteilPreisUebersicht extends StatelessWidget {
|
||||
const BauteilPreisUebersicht({super.key, required this.controller});
|
||||
final BauteilController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() {
|
||||
final preis = controller.preisDetails.value;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Preiskalkulation',
|
||||
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (preis != null) ...[
|
||||
Text(
|
||||
'Filamentkosten (Bauteil): ${DisplayFormats.currency(preis.materialKosten)}',
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Abfallkosten: ${DisplayFormats.currency(preis.abfallKosten)}',
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Aufschlag (${DisplayFormats.number(preis.aufschlagProzent)} %): ${DisplayFormats.currency(preis.aufschlagKosten)}',
|
||||
),
|
||||
] else
|
||||
const Text(
|
||||
'Für die Berechnung Gewicht, Abfallmenge und Filament auswählen. Bei mehreren Rollen zusätzlich den Verbrauch je Rolle passend zum Gesamtgewicht eingeben.',
|
||||
style: TextStyle(color: AppTheme.muted, height: 1.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: controller.gesamtpreisController,
|
||||
readOnly: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Gesamtpreis (automatisch)',
|
||||
suffixText: '€',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -75,11 +75,13 @@ class EntryNumberField extends StatelessWidget {
|
||||
required this.label,
|
||||
required this.unit,
|
||||
required this.validator,
|
||||
this.onChanged,
|
||||
});
|
||||
final TextEditingController controller;
|
||||
final String label;
|
||||
final String unit;
|
||||
final FormFieldValidator<String> validator;
|
||||
final ValueChanged<String>? onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => TextFormField(
|
||||
@@ -87,6 +89,7 @@ class EntryNumberField extends StatelessWidget {
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: InputDecoration(labelText: label, suffixText: unit),
|
||||
validator: validator,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -176,11 +179,13 @@ class SavedEntryCard extends StatelessWidget {
|
||||
required this.subtitle,
|
||||
required this.details,
|
||||
this.notes = '',
|
||||
this.trailing,
|
||||
});
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final List<String> details;
|
||||
final String notes;
|
||||
final Widget? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Card(
|
||||
@@ -194,9 +199,20 @@ class SavedEntryCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 18),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[const SizedBox(width: 8), trailing!],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
|
||||
@@ -137,6 +137,11 @@ class FilamentCard extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
value: (rest! / start).clamp(0.0, 1.0),
|
||||
color: rest < 100
|
||||
? Colors.red
|
||||
: rest <= start / 2
|
||||
? Colors.blue
|
||||
: null,
|
||||
minHeight: 6,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
backgroundColor: AppTheme.inputSurface,
|
||||
@@ -156,7 +161,16 @@ class FilamentCard extends StatelessWidget {
|
||||
label: filament.material,
|
||||
icon: Icons.layers_outlined,
|
||||
),
|
||||
_DetailPill(label: weight, icon: Icons.scale_outlined),
|
||||
_DetailPill(
|
||||
label: 'Netto: $weight',
|
||||
icon: Icons.scale_outlined,
|
||||
),
|
||||
if (filament.bruttoGewichtGramm != null)
|
||||
_DetailPill(
|
||||
label:
|
||||
'Brutto mit Spule: ${DisplayFormats.number(filament.bruttoGewichtGramm!)} g',
|
||||
icon: Icons.scale_outlined,
|
||||
),
|
||||
if (hatProfil)
|
||||
const _DetailPill(
|
||||
label: 'Profil erstellt',
|
||||
|
||||
@@ -145,57 +145,44 @@ class FilamentForm extends StatelessWidget {
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final fields = [
|
||||
TextFormField(
|
||||
controller: controller.preisController,
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Preis',
|
||||
suffixText: '€',
|
||||
),
|
||||
validator: (value) {
|
||||
final price = double.tryParse(
|
||||
(value ?? '').replaceAll(',', '.'),
|
||||
);
|
||||
return price == null || !price.isFinite || price < 0
|
||||
? 'Preis ungültig'
|
||||
: null;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
controller: controller.gewichtController,
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Startgewicht',
|
||||
suffixText: 'g',
|
||||
),
|
||||
validator: InputValidators.positive,
|
||||
),
|
||||
];
|
||||
if (constraints.maxWidth < 280) {
|
||||
return Column(
|
||||
children: [
|
||||
fields[0],
|
||||
const SizedBox(height: 16),
|
||||
fields[1],
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: fields[0]),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: fields[1]),
|
||||
],
|
||||
);
|
||||
},
|
||||
TextFormField(
|
||||
controller: controller.preisController,
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Preis',
|
||||
suffixText: '€',
|
||||
),
|
||||
validator: InputValidators.nonNegative,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: controller.gewichtController,
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nettogewicht',
|
||||
suffixText: 'g',
|
||||
helperText: 'Startgewicht ohne Spule, nur Material.',
|
||||
helperMaxLines: 2,
|
||||
),
|
||||
validator: InputValidators.positive,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: controller.bruttoGewichtController,
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Bruttogewicht (optional)',
|
||||
suffixText: 'g',
|
||||
helperText: 'Material und Spule zusammen.',
|
||||
helperMaxLines: 2,
|
||||
),
|
||||
validator: controller.validateBruttogewicht,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Obx(
|
||||
|
||||
Reference in New Issue
Block a user