add two views
This commit is contained in:
@@ -1,25 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controller/anwendung_controller.dart';
|
||||
import '../helper/display_formats.dart';
|
||||
import '../widgets/anwendung_form.dart';
|
||||
import '../widgets/entry_panel.dart';
|
||||
|
||||
class AnwendungView extends GetView<AnwendungController> {
|
||||
static const routeName = '/anwendung-view';
|
||||
const AnwendungView({super.key, required String filamentId});
|
||||
static const routeName = '/anwendung-view';
|
||||
const AnwendungView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Anwendung'),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
)
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Filament verarbeiten')),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1000),
|
||||
child: Obx(() {
|
||||
if (controller.filament.value == null) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Text(
|
||||
'Das ausgewählte Filament wurde nicht gefunden. Bitte wähle ein gespeichertes Filament aus.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return CustomScrollView(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: AnwendungForm(controller: controller),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 24),
|
||||
sliver: Obx(() {
|
||||
final entries = controller.anwendungen.toList();
|
||||
return SavedEntries(
|
||||
title: 'Gespeicherte Profile',
|
||||
count: entries.length,
|
||||
emptyText: 'Noch keine Verarbeitungsprofile für dieses Filament gespeichert.',
|
||||
itemBuilder: (context, index) {
|
||||
final entry = entries[index];
|
||||
return SavedEntryCard(
|
||||
key: ValueKey(entry.anwendungId),
|
||||
title:
|
||||
'Druck ${DisplayFormats.number(entry.druckTemperatur)} °C · Bett ${DisplayFormats.number(entry.druckbettTemperatur)} °C',
|
||||
subtitle: DisplayFormats.date(entry.erstelltAm),
|
||||
details: [
|
||||
'Bauraum geschlossen: ${entry.bauraumGeschlossen ? "Ja" : "Nein"}',
|
||||
if (entry.bauraumTemperatur != null)
|
||||
'Bauraum ${DisplayFormats.number(entry.bauraumTemperatur!)} °C',
|
||||
],
|
||||
notes: entry.sonstiges,
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
body: Container(),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controller/bauteil_controller.dart';
|
||||
import '../helper/display_formats.dart';
|
||||
import '../models/bauteil_model.dart';
|
||||
import '../widgets/bauteil_card.dart';
|
||||
import '../widgets/bauteil_delete_dialog.dart';
|
||||
import '../widgets/bauteil_form.dart';
|
||||
import '../widgets/entry_panel.dart';
|
||||
|
||||
class BauteilView extends GetView<BauteilController> {
|
||||
static const routeName = '/bauteile-view';
|
||||
const BauteilView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Gedruckte Bauteile')),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1000),
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: BauteilForm(controller: controller),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 24),
|
||||
sliver: Obx(() {
|
||||
final entries = controller.bauteile.toList();
|
||||
return SavedEntries(
|
||||
title: 'Deine Bauteile',
|
||||
count: entries.length,
|
||||
emptyText: 'Noch keine Bauteile gespeichert. Erfasse deinen ersten Druck.',
|
||||
itemBuilder: (context, index) {
|
||||
final entry = entries[index];
|
||||
return BauteilCard(
|
||||
key: ValueKey(entry.bauteilId),
|
||||
bauteil: entry,
|
||||
onAction: (action) =>
|
||||
_handleAction(context, action, entry),
|
||||
filamentDetails: entry.filamente
|
||||
.map((item) {
|
||||
final profile = controller.profile
|
||||
.firstWhereOrNull(
|
||||
(profile) =>
|
||||
profile.anwendungId == item.anwendungId,
|
||||
);
|
||||
final suffix = profile == null
|
||||
? ''
|
||||
: ' · Profil ${DisplayFormats.number(profile.druckTemperatur)} °C / Bett ${DisplayFormats.number(profile.druckbettTemperatur)} °C';
|
||||
final usage = item.verbrauchGramm > 0
|
||||
? '${DisplayFormats.number(item.verbrauchGramm)} g'
|
||||
: 'Verbrauch noch nicht erfasst';
|
||||
return '${controller.filamentLabel(item.filamentId)} · $usage$suffix';
|
||||
})
|
||||
.join('\n'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> _handleAction(
|
||||
BuildContext context,
|
||||
String action,
|
||||
BauteilModel entry,
|
||||
) async {
|
||||
switch (action) {
|
||||
case 'edit':
|
||||
await controller.editBauteil(entry);
|
||||
break;
|
||||
case 'duplicate':
|
||||
await controller.duplicateBauteil(entry);
|
||||
break;
|
||||
case 'delete':
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => BauteilDeleteDialog(name: entry.name),
|
||||
);
|
||||
if (confirmed == true) await controller.deleteBauteil(entry.bauteilId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
-264
@@ -1,10 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../controller/home_controller.dart';
|
||||
import '../models/kauf_filament_model.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../widgets/filament_form.dart';
|
||||
import '../widgets/filament_inventory.dart';
|
||||
import 'anwendung_view.dart';
|
||||
import 'bauteil_view.dart';
|
||||
|
||||
class HomeView extends GetView<HomeController> {
|
||||
static const routeName = '/home-view';
|
||||
@@ -12,271 +15,91 @@ class HomeView extends GetView<HomeController> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFormat = DateFormat('dd.MM.yyyy');
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Filament Einkaufsverwaltung'),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.blue.shade300,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// EINGABEBEREICH
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Obx(() => ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
side: const BorderSide(
|
||||
color: Colors.grey,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
title: const Text('Kaufdatum'),
|
||||
subtitle: Text(
|
||||
dateFormat.format(controller.kaufdatum.value),
|
||||
),
|
||||
trailing: const Icon(Icons.calendar_month),
|
||||
onTap: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: controller.kaufdatum.value,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
|
||||
if (date != null) {
|
||||
controller.kaufdatum.value = date;
|
||||
}
|
||||
},
|
||||
)),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
TextFormField(
|
||||
controller: controller.haendlerController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Wo gekauft / Händler',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) =>
|
||||
value == null || value.trim().isEmpty
|
||||
? 'Händler eingeben'
|
||||
: null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
TextFormField(
|
||||
controller: controller.markeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Marke',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) =>
|
||||
value == null || value.trim().isEmpty
|
||||
? 'Marke eingeben'
|
||||
: null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Obx(() => DropdownButtonFormField<String>(
|
||||
key: ValueKey(
|
||||
'${controller.editingId.value ?? "new"}'
|
||||
'_${controller.material.value}',
|
||||
),
|
||||
initialValue: controller.material.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Material',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: controller.materialien.map((item) {
|
||||
return DropdownMenuItem(
|
||||
value: item,
|
||||
child: Text(item),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
controller.material.value = value;
|
||||
}
|
||||
},
|
||||
)),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller.farbeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Farbe',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) =>
|
||||
value == null || value.trim().isEmpty
|
||||
? 'Farbe eingeben'
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller.preisController,
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Preis (€)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
final price = double.tryParse(
|
||||
(value ?? '').replaceAll(',', '.'),
|
||||
);
|
||||
if (price == null ||
|
||||
!price.isFinite ||
|
||||
price < 0) {
|
||||
return 'Preis ungültig';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
TextFormField(
|
||||
controller: controller.gewichtController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Gewicht in Gramm (g)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) =>
|
||||
value == null || value.trim().isEmpty
|
||||
? 'Gewicht eingeben g'
|
||||
: null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: controller.saveFilament,
|
||||
icon: Icon(
|
||||
controller.editingId.value == null
|
||||
? Icons.add
|
||||
: Icons.save,
|
||||
),
|
||||
label: Text(
|
||||
controller.editingId.value == null
|
||||
? 'Filament speichern'
|
||||
: 'Änderungen speichern',
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.editingId.value != null)
|
||||
IconButton(
|
||||
tooltip: 'Bearbeitung abbrechen',
|
||||
onPressed: controller.resetForm,
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
toolbarHeight: 80,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Bauteile verwalten',
|
||||
icon: const Icon(Icons.view_in_ar_outlined),
|
||||
onPressed: () => Get.toNamed(BauteilView.routeName),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.all_inbox_rounded, color: AppTheme.teal, size: 28),
|
||||
SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Filamentverwaltung',
|
||||
style: TextStyle(fontSize: 21, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
||||
// LISTENBEREICH
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
final filamente = controller.filamente;
|
||||
|
||||
if (filamente.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('Noch keine Filamente gespeichert'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1320),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final wide = constraints.maxWidth >= 900;
|
||||
final form = FilamentForm(controller: controller);
|
||||
final inventory = FilamentInventory(
|
||||
controller: controller,
|
||||
onOpen: (filament) => Get.toNamed(
|
||||
'${AnwendungView.routeName}/${Uri.encodeComponent(filament.filamentId)}',
|
||||
),
|
||||
onAction: (action, filament) =>
|
||||
_handleAction(context, action, filament),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: filamente.length,
|
||||
itemBuilder: (context, index) {
|
||||
final filament = filamente[index];
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Icons.inventory_2_outlined),
|
||||
),
|
||||
title: Text(
|
||||
'${filament.material} – ${filament.farbe}',
|
||||
),
|
||||
subtitle: Text(
|
||||
'${filament.haendler}\n'
|
||||
'${dateFormat.format(filament.kaufdatum)}'
|
||||
' | ${filament.preis.toStringAsFixed(2)} €',
|
||||
),
|
||||
isThreeLine: true,
|
||||
onTap: () => Get.to(
|
||||
() => AnwendungView(
|
||||
filamentId: filament.filamentId,
|
||||
if (wide) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 8, 24, 24),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 390,
|
||||
child: SingleChildScrollView(
|
||||
controller: controller.formScrollController,
|
||||
child: form,
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: PopupMenuButton<String>(
|
||||
onSelected: (action) =>
|
||||
_handleAction(context, action, filament),
|
||||
itemBuilder: (context) => const [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.edit),
|
||||
title: Text('Bearbeiten'),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
controller: controller.pageScrollController,
|
||||
slivers: [inventory],
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'duplicate',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.copy),
|
||||
title: Text('Duplizieren'),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete_outline),
|
||||
title: Text('Löschen'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
}
|
||||
return CustomScrollView(
|
||||
controller: controller.pageScrollController,
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
|
||||
sliver: SliverToBoxAdapter(child: form),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
|
||||
sliver: inventory,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -288,21 +111,17 @@ class HomeView extends GetView<HomeController> {
|
||||
) async {
|
||||
switch (action) {
|
||||
case 'edit':
|
||||
controller.editFilament(filament);
|
||||
await controller.editFilament(filament);
|
||||
break;
|
||||
|
||||
case 'duplicate':
|
||||
await controller.duplicateFilament(filament);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Filament löschen?'),
|
||||
content: Text(
|
||||
'${filament.material} – ${filament.farbe} löschen?',
|
||||
),
|
||||
content: Text('${filament.material} – ${filament.farbe} löschen?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
@@ -315,11 +134,10 @@ class HomeView extends GetView<HomeController> {
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
await controller.deleteFilament(filament.filamentId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user