add two views

This commit is contained in:
atseirjo
2026-10-27 15:00:16 +01:00
parent 864da57537
commit 07a1bb02fd
42 changed files with 3582 additions and 346 deletions
+86 -8
View File
@@ -1,17 +1,95 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:uuid/uuid.dart';
import '../helper/input_validators.dart';
import '../models/anwendung_model.dart';
import '../models/kauf_filament_model.dart';
import '../services/get_storage_services.dart';
import 'home_controller.dart';
class AnwendungController extends GetxController {
final count = 0.obs;
AnwendungController({required this.filamentId});
@override
void onInit() {
static const storageKey = 'filament_anwendungen';
final String filamentId;
final storage = Get.find<GetStorageService>();
final formKey = GlobalKey<FormState>();
final druckbettController = TextEditingController();
final druckController = TextEditingController();
final bauraumController = TextEditingController();
final sonstigesController = TextEditingController();
final bauraumGeschlossen = false.obs;
final isSaving = false.obs;
final message = RxnString();
final hasError = false.obs;
final filament = Rxn<KaufFilamentModel>();
final anwendungen = <AnwendungModel>[].obs;
@override
void onInit() {
super.onInit();
final filamente = storage
.getModelList(HomeController.storageKey)
.map(KaufFilamentModel.fromJson)
.toList();
filament.value = filamente.firstWhereOrNull(
(item) => item.filamentId == filamentId,
);
anwendungen.assignAll(
storage
.getModelList(storageKey)
.map(AnwendungModel.fromJson)
.where((item) => item.filamentId == filamentId),
);
anwendungen.sort((a, b) => b.erstelltAm.compareTo(a.erstelltAm));
}
Future<void> saveAnwendung() async {
if (isSaving.value || filament.value == null) return;
if (!(formKey.currentState?.validate() ?? false)) return;
isSaving.value = true;
message.value = null;
try {
final entry = AnwendungModel(
anwendungId: const Uuid().v4(),
filamentId: filamentId,
erstelltAm: DateTime.now(),
druckbettTemperatur: InputValidators.number(druckbettController.text)!,
druckTemperatur: InputValidators.number(druckController.text)!,
bauraumGeschlossen: bauraumGeschlossen.value,
bauraumTemperatur: InputValidators.number(bauraumController.text),
sonstiges: sonstigesController.text.trim(),
);
final entries = storage.getModelList(storageKey);
await storage.saveModelList(storageKey, [...entries, entry.toJson()]);
anwendungen.insert(0, entry);
resetForm();
hasError.value = false;
message.value = 'Verarbeitungsprofil gespeichert.';
} catch (_) {
hasError.value = true;
message.value = 'Speichern fehlgeschlagen. Bitte erneut versuchen.';
} finally {
isSaving.value = false;
}
}
@override
void onReady() {}
@override
void onClose() {}
void resetForm() {
formKey.currentState?.reset();
druckbettController.clear();
druckController.clear();
bauraumController.clear();
sonstigesController.clear();
bauraumGeschlossen.value = false;
}
@override
void onClose() {
druckbettController.dispose();
druckController.dispose();
bauraumController.dispose();
sonstigesController.dispose();
super.onClose();
}
}
+367
View File
@@ -0,0 +1,367 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:uuid/uuid.dart';
import '../helper/display_formats.dart';
import '../helper/filament_bestand.dart';
import '../helper/input_validators.dart';
import '../helper/storage_keys.dart';
import '../models/anwendung_model.dart';
import '../models/bauteil_model.dart';
import '../models/kauf_filament_model.dart';
import '../services/bauteil_photo_service.dart';
import '../services/get_storage_services.dart';
import 'home_controller.dart';
class BauteilController extends GetxController {
BauteilController({BauteilPhotoService? photoService})
: photoService = photoService ?? BauteilPhotoService();
static const storageKey = StorageKeys.bauteile;
final storage = Get.find<GetStorageService>();
final BauteilPhotoService photoService;
final formKey = GlobalKey<FormState>();
final scrollController = ScrollController();
final nameController = TextEditingController();
final verwendungController = TextEditingController();
final stundenController = TextEditingController(text: '0');
final minutenController = TextEditingController(text: '0');
final gewichtController = TextEditingController();
final abfallController = TextEditingController(text: '0');
final _verbrauchControllers = <String, TextEditingController>{};
final filamente = <KaufFilamentModel>[].obs;
final profile = <AnwendungModel>[].obs;
final bauteile = <BauteilModel>[].obs;
final selectedFilamente = <String, String?>{}.obs;
final editingId = RxnString();
final foto = Rxn<Uint8List>();
final isPickingPhoto = false.obs;
final isSaving = false.obs;
final message = RxnString();
final hasError = false.obs;
bool _closed = false;
@override
void onInit() {
super.onInit();
filamente.assignAll(
storage
.getModelList(StorageKeys.filamente)
.map(KaufFilamentModel.fromJson),
);
profile.assignAll(
storage
.getModelList(StorageKeys.anwendungen)
.map(AnwendungModel.fromJson),
);
bauteile.assignAll(_loadBauteile());
bauteile.sort((a, b) => b.erstelltAm.compareTo(a.erstelltAm));
}
List<BauteilModel> _loadBauteile() =>
storage.getModelList(storageKey).map(BauteilModel.fromJson).toList();
TextEditingController verbrauchController(String id) =>
_verbrauchControllers.putIfAbsent(id, () => TextEditingController());
List<AnwendungModel> profilesFor(String filamentId) =>
profile.where((item) => item.filamentId == filamentId).toList()
..sort((a, b) => b.erstelltAm.compareTo(a.erstelltAm));
String filamentLabel(String id) {
final item = filamente.firstWhereOrNull((item) => item.filamentId == id);
if (item == null) return 'Gelöschtes Filament ($id)';
return '${item.marke} · ${item.material} · ${item.farbe}';
}
double? verfuegbarGramm(String id) {
final start = filamente
.firstWhereOrNull((item) => item.filamentId == id)
?.gewichtGramm;
if (start == null) return null;
final used = FilamentBestand.verbrauch(
bauteile,
ohneBauteilId: editingId.value,
);
return start - (used[id] ?? 0);
}
void toggleFilament(String id, bool selected) {
if (selected) {
verbrauchController(id);
selectedFilamente[id] = null;
} else {
selectedFilamente.remove(id);
}
}
void selectProfile(String filamentId, String? anwendungId) {
if (!selectedFilamente.containsKey(filamentId)) return;
if (anwendungId != null &&
!profilesFor(filamentId)
.any((item) => item.anwendungId == anwendungId)) {
return;
}
selectedFilamente[filamentId] = anwendungId;
}
Future<void> pickPhoto() async {
if (isPickingPhoto.value || isSaving.value) return;
isPickingPhoto.value = true;
try {
final bytes = await photoService.pickPhoto();
if (!_closed && bytes != null) foto.value = bytes;
} on FormatException catch (error) {
if (!_closed) _error(error.message);
} catch (_) {
if (!_closed) {
_error(
'Das Foto konnte nicht geladen werden. Bitte ein JPG-, PNG- oder WebP-Bild auswählen.',
);
}
} finally {
if (!_closed) isPickingPhoto.value = false;
}
}
void removePhoto() => foto.value = null;
void _error(String text) {
hasError.value = true;
message.value = text;
}
String? _validateConsumption(
BauteilModel entry,
List<BauteilModel> existing,
) {
if (entry.filamente.isEmpty) {
return 'Bitte mindestens ein Filament auswählen.';
}
final sum = entry.filamente.fold<double>(
0,
(sum, item) => sum + item.verbrauchGramm,
);
if (entry.filamente.any(
(item) => !item.verbrauchGramm.isFinite || item.verbrauchGramm <= 0,
)) {
return 'Bitte für jedes verwendete Filament einen Verbrauch größer 0 eingeben.';
}
if (!sum.isFinite || (sum - entry.gesamtverbrauchGramm).abs() > 0.01) {
return 'Der Filamentverbrauch muss zusammen ${DisplayFormats.number(entry.gesamtverbrauchGramm)} g ergeben (Bauteilgewicht + Abfall).';
}
final used = FilamentBestand.verbrauch(
existing,
ohneBauteilId: entry.bauteilId,
);
final currentFilamente = storage
.getModelList(StorageKeys.filamente)
.map(KaufFilamentModel.fromJson)
.toList();
for (final item in entry.filamente) {
final filament = currentFilamente.firstWhereOrNull(
(filament) => filament.filamentId == item.filamentId,
);
final start = filament?.gewichtGramm;
if (start == null) {
return 'Filament fehlt oder sein Startgewicht ist ungültig. Bitte die Auswahl bzw. das Filament prüfen.';
}
final available = start - (used[item.filamentId] ?? 0);
if (item.verbrauchGramm - available > 0.000001) {
return '${filamentLabel(item.filamentId)}: nur ${DisplayFormats.number(available)} g verfügbar.';
}
}
return null;
}
/// One persisted ledger determines all remaining weights. Updating or deleting
/// an entry therefore cannot leave a second stock table out of sync.
Future<void> _commit(List<BauteilModel> entries) async {
await storage.saveModelList(
storageKey,
entries.map((item) => item.toJson()).toList(),
);
entries.sort((a, b) => b.erstelltAm.compareTo(a.erstelltAm));
bauteile.assignAll(entries);
if (Get.isRegistered<HomeController>()) {
Get.find<HomeController>().loadVerbrauch();
}
}
Future<void> saveBauteil() async {
if (isSaving.value || isPickingPhoto.value) return;
message.value = null;
if (!(formKey.currentState?.validate() ?? false)) return;
if (selectedFilamente.isEmpty) {
_error('Bitte mindestens ein Filament auswählen.');
return;
}
final duration =
int.parse(stundenController.text.trim()) * 60 +
int.parse(minutenController.text.trim());
if (duration <= 0) {
_error('Die Druckdauer muss mindestens eine Minute betragen.');
return;
}
isSaving.value = true;
try {
final existing = _loadBauteile();
final previous = existing.firstWhereOrNull(
(item) => item.bauteilId == editingId.value,
);
if (editingId.value != null && previous == null) {
_error('Das bearbeitete Bauteil wurde nicht gefunden.');
return;
}
final entry = BauteilModel(
bauteilId: editingId.value ?? const Uuid().v4(),
erstelltAm: previous?.erstelltAm ?? DateTime.now(),
name: nameController.text.trim(),
verwendung: verwendungController.text.trim(),
druckdauerMinuten: duration,
gewichtGramm: InputValidators.number(gewichtController.text)!,
abfallGramm: InputValidators.number(abfallController.text)!,
fotoBase64: foto.value == null ? null : base64Encode(foto.value!),
filamente: selectedFilamente.entries
.map(
(item) => BauteilFilamentModel(
filamentId: item.key,
anwendungId: item.value,
verbrauchGramm:
InputValidators.number(
verbrauchController(item.key).text,
) ??
double.nan,
),
)
.toList(),
);
final error = _validateConsumption(entry, existing);
if (error != null) {
_error(error);
return;
}
await _commit([
...existing.where((item) => item.bauteilId != entry.bauteilId),
entry,
]);
resetForm();
hasError.value = false;
message.value = 'Bauteil und Filamentverbrauch gespeichert.';
} catch (_) {
_error('Speichern fehlgeschlagen. Bitte erneut versuchen.');
} finally {
isSaving.value = false;
}
}
Future<void> editBauteil(BauteilModel entry) async {
if (isSaving.value || isPickingPhoto.value) return;
resetForm();
message.value = null;
nameController.text = entry.name;
verwendungController.text = entry.verwendung;
stundenController.text = '${entry.druckdauerMinuten ~/ 60}';
minutenController.text = '${entry.druckdauerMinuten % 60}';
gewichtController.text = DisplayFormats.number(entry.gewichtGramm);
abfallController.text = DisplayFormats.number(entry.abfallGramm);
foto.value = entry.fotoBytes;
for (final item in entry.filamente) {
verbrauchController(item.filamentId).text = item.verbrauchGramm > 0
? DisplayFormats.number(item.verbrauchGramm)
: '';
selectedFilamente[item.filamentId] = item.anwendungId;
}
editingId.value = entry.bauteilId;
await scrollToForm();
}
Future<void> duplicateBauteil(BauteilModel entry) async {
if (isSaving.value || isPickingPhoto.value) return;
isSaving.value = true;
try {
final existing = _loadBauteile();
final copy = entry.copyWith(
bauteilId: const Uuid().v4(),
erstelltAm: DateTime.now(),
);
final error = _validateConsumption(copy, existing);
if (error != null) {
_error(error);
await scrollToForm();
return;
}
await _commit([...existing, copy]);
hasError.value = false;
message.value = 'Bauteil dupliziert und Verbrauch erneut gebucht.';
} catch (_) {
_error('Duplizieren fehlgeschlagen. Bitte erneut versuchen.');
await scrollToForm();
} finally {
isSaving.value = false;
}
}
Future<void> deleteBauteil(String id) async {
if (isSaving.value || isPickingPhoto.value) return;
isSaving.value = true;
try {
await _commit(
_loadBauteile().where((item) => item.bauteilId != id).toList(),
);
if (editingId.value == id) resetForm();
hasError.value = false;
message.value = 'Bauteil gelöscht und Verbrauch zurückgebucht.';
} catch (_) {
_error('Löschen fehlgeschlagen. Bitte erneut versuchen.');
await scrollToForm();
} finally {
isSaving.value = false;
}
}
Future<void> scrollToForm() async {
if (scrollController.hasClients) {
await scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
);
}
}
void resetForm() {
formKey.currentState?.reset();
editingId.value = null;
nameController.clear();
verwendungController.clear();
stundenController.text = '0';
minutenController.text = '0';
gewichtController.clear();
abfallController.text = '0';
selectedFilamente.clear();
for (final controller in _verbrauchControllers.values) {
controller.clear();
}
foto.value = null;
}
@override
void onClose() {
_closed = true;
scrollController.dispose();
nameController.dispose();
verwendungController.dispose();
stundenController.dispose();
minutenController.dispose();
gewichtController.dispose();
abfallController.dispose();
for (final controller in _verbrauchControllers.values) {
controller.dispose();
}
super.onClose();
}
}
+64 -20
View File
@@ -3,17 +3,40 @@ import 'package:get/get.dart';
import 'package:uuid/uuid.dart';
import '../models/kauf_filament_model.dart';
import '../models/bauteil_model.dart';
import '../helper/storage_keys.dart';
import '../helper/filament_bestand.dart';
import '../helper/display_formats.dart';
import '../helper/input_validators.dart';
import '../services/get_storage_services.dart';
class HomeController extends GetxController {
static const storageKey = 'kauf_filamente';
static const storageKey = StorageKeys.filamente;
final storage = Get.find<GetStorageService>();
final uuid = const Uuid();
final filamente = <KaufFilamentModel>[].obs;
final verbrauch = <String, double>{}.obs;
void loadVerbrauch() {
verbrauch.assignAll(
FilamentBestand.verbrauch(
storage.getModelList(StorageKeys.bauteile).map(BauteilModel.fromJson),
),
);
}
double? restgewicht(KaufFilamentModel filament) {
final gewicht = filament.gewichtGramm;
return gewicht == null
? null
: gewicht - (verbrauch[filament.filamentId] ?? 0);
}
final formKey = GlobalKey<FormState>();
final pageScrollController = ScrollController();
final formScrollController = ScrollController();
final haendlerController = TextEditingController();
final farbeController = TextEditingController();
@@ -46,19 +69,16 @@ class HomeController extends GetxController {
}
void loadFilamente() {
loadVerbrauch();
final data = storage.getModelList(storageKey);
filamente.assignAll(
data.map(KaufFilamentModel.fromJson),
);
filamente.assignAll(data.map(KaufFilamentModel.fromJson));
sortFilamente();
}
void sortFilamente() {
filamente.sort(
(a, b) => b.kaufdatum.compareTo(a.kaufdatum),
);
filamente.sort((a, b) => b.kaufdatum.compareTo(a.kaufdatum));
}
Future<void> persist() async {
@@ -80,6 +100,22 @@ class HomeController extends GetxController {
return;
}
loadVerbrauch();
final gewicht = InputValidators.number(gewichtController.text);
final bereitsVerbraucht = editingId.value == null
? 0
: (verbrauch[editingId.value!] ?? 0);
if (gewicht == null ||
!gewicht.isFinite ||
gewicht <= 0 ||
gewicht < bereitsVerbraucht) {
Get.snackbar(
'Ungültiges Gewicht',
'Das Startgewicht muss positiv sein und mindestens dem bereits gebuchten Verbrauch entsprechen.',
);
return;
}
final filament = KaufFilamentModel(
filamentId: editingId.value ?? uuid.v4(),
marke: markeController.text.trim(),
@@ -88,9 +124,7 @@ class HomeController extends GetxController {
material: material.value,
preis: preis,
farbe: farbeController.text.trim(),
gewicht: gewichtController.text.trim().isEmpty
? '1kg'
: gewichtController.text.trim(),
gewicht: gewicht.toString(),
);
if (editingId.value == null) {
@@ -113,7 +147,7 @@ class HomeController extends GetxController {
resetForm();
}
void editFilament(KaufFilamentModel filament) {
Future<void> editFilament(KaufFilamentModel filament) async {
editingId.value = filament.filamentId;
kaufdatum.value = filament.kaufdatum;
markeController.text = filament.marke;
@@ -121,15 +155,24 @@ class HomeController extends GetxController {
material.value = filament.material;
preisController.text = filament.preis.toStringAsFixed(2);
farbeController.text = filament.farbe;
gewichtController.text = filament.gewicht;
gewichtController.text = filament.gewichtGramm == null
? filament.gewicht
: DisplayFormats.number(filament.gewichtGramm!);
final scroll = formScrollController.hasClients
? formScrollController
: pageScrollController;
if (scroll.hasClients) {
await scroll.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
);
}
}
Future<void> duplicateFilament(
KaufFilamentModel filament,
) async {
filamente.add(
filament.copyWith(filamentId: uuid.v4()),
);
Future<void> duplicateFilament(KaufFilamentModel filament) async {
filamente.add(filament.copyWith(filamentId: uuid.v4()));
sortFilamente();
await persist();
@@ -151,11 +194,12 @@ class HomeController extends GetxController {
material.value = 'PLA';
gewichtController.clear();
formKey.currentState?.reset();
}
@override
void onClose() {
pageScrollController.dispose();
formScrollController.dispose();
haendlerController.dispose();
markeController.dispose();
farbeController.dispose();
@@ -163,4 +207,4 @@ class HomeController extends GetxController {
gewichtController.dispose();
super.onClose();
}
}
}