diff --git a/lib/api.dart b/lib/api.dart index 5142b3f..1c403aa 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -96,6 +96,12 @@ class API { return resp["uuid"]; } + Future consumeItem( + String transaction, String destination, String price) async { + await postRequest("$instance/demand", + {"uuid": transaction, "destination": destination, "price": price}); + } + String getImageURL(String item) { return "$instance/$item/image"; } diff --git a/lib/pages/consume.dart b/lib/pages/consume.dart new file mode 100644 index 0000000..ed199c2 --- /dev/null +++ b/lib/pages/consume.dart @@ -0,0 +1,144 @@ +import 'package:cdb_ui/api.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'supply.dart'; + +class ConsumePage extends StatefulWidget { + final String item; + final String variant; + final String transaction; + Function refresh; + + ConsumePage(this.transaction, this.item, this.variant, this.refresh, + {super.key}); + + @override + State createState() => _ConsumePageState(); +} + +class _ConsumePageState extends State { + final _formKey = GlobalKey(); + String _selectedDestination = ""; + String _price = ""; + + @override + void initState() { + super.initState(); + } + + Future _consume() async { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + + await API() + .consumeItem(widget.transaction, _selectedDestination, "${_price} €"); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Item consumed successfully!')), + ); + Navigator.of(context).pop(); + widget.refresh(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Consume Item'), + ), + body: FutureBuilder( + future: + API().getUniqueField(widget.item, widget.variant, "destination"), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + + var destinations = snapshot.data!; + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Destination Field with Dropdown and Text Input + Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text.isEmpty) { + return destinations; + } + return destinations.where((String option) { + return option + .toLowerCase() + .contains(textEditingValue.text.toLowerCase()); + }); + }, + onSelected: (String selection) { + setState(() { + _selectedDestination = selection; + }); + }, + fieldViewBuilder: (context, textEditingController, + focusNode, onFieldSubmitted) { + textEditingController.text = _selectedDestination; + return TextFormField( + onChanged: (value) { + _selectedDestination = value; + }, + controller: textEditingController, + focusNode: focusNode, + decoration: const InputDecoration( + labelText: 'Destination', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value?.isEmpty ?? true) { + return "Please enter a destination"; + } + + return null; + }, + ); + }, + ), + + const SizedBox(height: 16), + + // Price Field + TextFormField( + decoration: const InputDecoration(labelText: 'Price'), + keyboardType: TextInputType.number, + initialValue: _price, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a price'; + } + if (double.tryParse(value) == null) { + return 'Please enter a valid number'; + } + return null; + }, + onSaved: (value) { + _price = value!; + }, + ), + + const SizedBox(height: 20), + + // Submit Button + ElevatedButton( + onPressed: _consume, + child: const Text('Consume Item'), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages/itemview.dart b/lib/pages/itemview.dart index d96e2af..bcf39bf 100644 --- a/lib/pages/itemview.dart +++ b/lib/pages/itemview.dart @@ -1,11 +1,23 @@ import 'package:cdb_ui/api.dart'; +import 'package:cdb_ui/pages/consume.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'supply.dart'; -class ItemView extends StatelessWidget { +class ItemView extends StatefulWidget { final Item item; + @override + State createState() => _ItemViewState(); + + const ItemView({super.key, required this.item}); +} + +class _ItemViewState extends State { + void refresh() { + setState(() {}); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -19,21 +31,21 @@ class ItemView extends StatelessWidget { Column( children: [ Text( - item.name, + widget.item.name, style: const TextStyle(fontWeight: FontWeight.bold), ), - Text(item.category) + Text(widget.item.category) ], ) ], ), const SizedBox(height: 10), Row( - children: item.variants.entries.map((entry) { + children: widget.item.variants.entries.map((entry) { return Text(entry.value.name); }).toList()), FutureBuilder( - future: API().getInventory(item.id), + future: API().getInventory(widget.item.id), builder: (context, snapshot) { if (!snapshot.hasData) { return const CircularProgressIndicator(); @@ -42,92 +54,101 @@ class ItemView extends StatelessWidget { return Expanded( child: ListView( - children: data.map((x) => TransactionCard(x)).toList())); + children: + data.map((x) => TransactionCard(x, refresh)).toList())); }, ) ]), floatingActionButton: FloatingActionButton( onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => SupplyPage(item))); + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => SupplyPage(widget.item, refresh))); }, child: const Icon(Icons.add)), ); } - - const ItemView({super.key, required this.item}); } class TransactionCard extends StatelessWidget { final Transaction t; + Function refresh; - const TransactionCard(this.t, {super.key}); + TransactionCard(this.t, this.refresh, {super.key}); @override Widget build(BuildContext context) { - return Card( - color: t.expired ? Colors.red[100] : Colors.white, - margin: EdgeInsets.symmetric(vertical: 8, horizontal: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - elevation: 4, - child: Padding( - padding: EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - t.item, - style: TextStyle(fontSize: 16), - ), - SizedBox( - width: 4, - ), - Text( - t.variant, - style: TextStyle(fontSize: 14, color: Colors.grey[600]), - ), - ], - ), - Text( - tsFormat(t.timestamp), - style: TextStyle(fontSize: 14, color: Colors.grey[700]), - ), - ], - ), - SizedBox( - height: 10, - ), - Row( - children: [ - Icon(Icons.money, size: 18, color: Colors.green), - SizedBox(width: 6), - Text( - "${t.price.value} ${t.price.currency}", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - ], - ), - if (t.origin != null) ...[ - SizedBox(height: 8), + return InkWell( + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) { + return ConsumePage(t.uuid, t.item, t.variant, this.refresh); + }, + )); + }, + child: Card( + color: t.expired ? Colors.red[100] : Colors.white, + margin: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 4, + child: Padding( + padding: EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Icon(Icons.store, size: 18, color: Colors.blue), - SizedBox(width: 6), + Row( + children: [ + Text( + t.item, + style: TextStyle(fontSize: 16), + ), + SizedBox( + width: 4, + ), + Text( + t.variant, + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + ], + ), Text( - t.origin!, + tsFormat(t.timestamp), style: TextStyle(fontSize: 14, color: Colors.grey[700]), ), ], ), + SizedBox( + height: 10, + ), + Row( + children: [ + Icon(Icons.money, size: 18, color: Colors.green), + SizedBox(width: 6), + Text( + "${t.price.value} ${t.price.currency}", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ), + if (t.origin != null) ...[ + SizedBox(height: 8), + Row( + children: [ + Icon(Icons.store, size: 18, color: Colors.blue), + SizedBox(width: 6), + Text( + t.origin!, + style: TextStyle(fontSize: 14, color: Colors.grey[700]), + ), + ], + ), + ], ], - ], + ), ), ), ); diff --git a/lib/pages/supply.dart b/lib/pages/supply.dart index 1025fb4..ff4dab5 100644 --- a/lib/pages/supply.dart +++ b/lib/pages/supply.dart @@ -4,8 +4,9 @@ import 'package:qr_bar_code_scanner_dialog/qr_bar_code_scanner_dialog.dart'; class SupplyPage extends StatefulWidget { final Item item; + Function refresh; - const SupplyPage(this.item, {super.key}); + SupplyPage(this.item, this.refresh, {super.key}); @override State createState() => _SupplyPageState(); @@ -35,6 +36,7 @@ class _SupplyPageState extends State { const SnackBar(content: Text('Item added successfully!')), ); Navigator.of(context).pop(); + widget.refresh(); } } @@ -85,7 +87,6 @@ class _SupplyPageState extends State { const SizedBox(height: 16), // Origin Field with Dropdown and Text Input - // todo : fix state Autocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text.isEmpty) { @@ -106,6 +107,9 @@ class _SupplyPageState extends State { focusNode, onFieldSubmitted) { textEditingController.text = _selectedOrigin; return TextFormField( + onChanged: (value) { + _selectedOrigin = value; + }, controller: textEditingController, focusNode: focusNode, decoration: const InputDecoration(