diff --git a/lib/api.dart b/lib/api.dart index 7a76f9c..5142b3f 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -2,7 +2,8 @@ import 'dart:convert'; import 'package:http/http.dart' as http; class API { - static String instance = "http://192.168.178.30:8080"; + // todo : rework with auth + static String instance = "http://192.168.2.20:8080"; Future getRequest(String url) async { var resp = await http.get(Uri.parse(url), headers: { @@ -16,8 +17,9 @@ class API { var resp = await http.post(Uri.parse(url), headers: { 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json' }, - body: data); + body: jsonEncode(data)); return resp.body; } @@ -28,14 +30,22 @@ class API { return lst as List; } - Future> getUniqueField( + Future> getUniqueField( String item, String variant, String field) async { - return jsonDecode(await getRequest( - "$instance/item/$item/$variant/unique?field=$field")) as List; + var resp = jsonDecode( + await getRequest("$instance/item/$item/$variant/unique?field=$field")); + List ret = []; + + for (var e in resp as List) { + ret.add(e); + } + + return ret; } - Future> getLocations() async { - return jsonDecode(await getRequest("$instance/locations")); + Future> getLocations() async { + var resp = jsonDecode(await getRequest("$instance/locations")); + return resp as List; } Future getItem(String item) async { @@ -73,13 +83,17 @@ class API { location = null; } - return jsonDecode(await postRequest("$instance/supply", { + var req = await postRequest("$instance/supply", { "item": item, "variant": variant, "price": price, "origin": origin, "location": location - }))["uuid"]; + }); + var resp = jsonDecode(req); + print(resp); + + return resp["uuid"]; } String getImageURL(String item) { @@ -138,7 +152,7 @@ class Transaction { late Price price; late String? origin; late int timestamp; - late ConsumeInfo consumed; + late ConsumeInfo? consumed; late bool expired; Transaction(Map json) { @@ -149,7 +163,7 @@ class Transaction { origin = json["origin"]; timestamp = json["timestamp"]; expired = json["expired"]; - consumed = ConsumeInfo(json["consumed"]); + consumed = json["consumed"] != null ? ConsumeInfo(json["consumed"]) : null; } } diff --git a/lib/itemview.dart b/lib/itemview.dart deleted file mode 100644 index 6815c9c..0000000 --- a/lib/itemview.dart +++ /dev/null @@ -1,276 +0,0 @@ -import 'package:cdb_ui/api.dart'; -import 'package:flutter/material.dart'; -import 'package:qr_bar_code_scanner_dialog/qr_bar_code_scanner_dialog.dart'; - -class ItemView extends StatelessWidget { - final Item item; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Column(children: [ - Row( - children: [ - const Align( - alignment: Alignment.centerLeft, - child: Placeholder(), - ), // todo - Column( - children: [ - Text( - item.name, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Text(item.category) - ], - ) - ], - ), - const SizedBox(height: 10), - Row( - children: item.variants.entries.map((entry) { - return Text(entry.value.name); - }).toList()), - FutureBuilder( - future: API().getInventory(item.id), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const CircularProgressIndicator(); - } - var data = snapshot.data!; - - return Expanded( - child: ListView( - children: data.map((x) => TransactionCard(x)).toList())); - }, - ) - ]), - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => SupplyPage(item))); - }, - child: const Icon(Icons.add)), - ); - } - - const ItemView({super.key, required this.item}); -} - -class TransactionCard extends StatelessWidget { - final Transaction t; - - const TransactionCard(this.t, {super.key}); - - @override - Widget build(BuildContext context) { - return Card( - child: Column( - children: [ - Row( - children: [Text(t.uuid), Text(t.item), Text(t.variant)], - ), - ], - ), - ); - } -} - -class SupplyPage extends StatefulWidget { - final Item item; - - const SupplyPage(this.item, {super.key}); - - @override - State createState() => _SupplyPageState(); -} - -class _SupplyPageState extends State { - late String variant; - - final _formKey = GlobalKey(); - - String _selectedOrigin = ""; - String _selectedLocation = ""; - String _price = ""; - - @override - void initState() { - super.initState(); - variant = widget.item.variants.keys.first; - } - - void _supply() async { - if (_formKey.currentState!.validate()) { - _formKey.currentState!.save(); - - await API().supplyItem(widget.item.name, variant, _price, _selectedOrigin, - _selectedLocation); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Item added successfully!')), - ); - Navigator.of(context).pop(); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Add New Item'), - ), - body: FutureBuilder(future: () async { - return ( - await API().getLocations(), - await API().getUniqueField(widget.item.id, variant, "origin") - ); - }(), builder: (context, snap) { - if (!snap.hasData) { - return const CircularProgressIndicator(); - } - - var (locationMap, origins) = snap.data!; - - // todo : fix locations - var locations = []; - - return Padding( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( - children: [ - // Variant selection - DropdownButtonFormField( - hint: const Text('Select Variant'), - value: variant, - onChanged: (value) { - setState(() { - variant = value!; - }); - }, - items: widget.item.variants.entries - .map>((variant) { - return DropdownMenuItem( - value: variant.key, - child: Text(variant.value.name), - ); - }).toList(), - validator: (value) { - if (value == null) { - return 'Please select a variant'; - } - return null; - }, - onSaved: (value) { - variant = value!; - }, - ), - - // Origin Field with Dropdown and Text Input - DropdownButtonFormField( - value: _selectedOrigin, - hint: const Text('Select or Enter Origin'), - onChanged: (value) { - setState(() { - _selectedOrigin = value ?? ""; - - if (_price.isNotEmpty) { - // todo : update price from latest - } - }); - }, - items: origins - .map>( - (origin) => DropdownMenuItem( - value: origin, - child: Text(origin), - )) - .toList(), - ), - TextFormField( - decoration: - const InputDecoration(labelText: 'Enter New Origin'), - onChanged: (value) { - setState(() { - _selectedOrigin = ""; // Clear dropdown selection - }); - }, - ), - - // Price Field - TextFormField( - decoration: const InputDecoration(labelText: 'Price'), - keyboardType: TextInputType.number, - 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!; - }, - ), - - // Location Dropdown - Row( - children: [ - DropdownButtonFormField( - hint: const Text('Select Location'), - value: _selectedLocation, - onChanged: (value) { - setState(() { - _selectedLocation = value!; - }); - }, - items: - locations.map>((location) { - return DropdownMenuItem( - value: location, - child: Text(location), - ); - }).toList(), - validator: (value) { - if (value == null) { - return 'Please select a location'; - } - return null; - }, - onSaved: (value) { - _selectedLocation = value!; - }, - ), - IconButton( - onPressed: () { - QrBarCodeScannerDialog().getScannedQrBarCode( - context: context, - onCode: (code) { - setState(() { - _selectedLocation = code!; - }); - }); - }, - icon: const Icon(Icons.qr_code)) - ], - ), - - const SizedBox(height: 20), - - // Submit Button - ElevatedButton( - onPressed: _supply, - child: const Text('Add Item'), - ), - ], - ), - ), - ); - }), - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index aaca2eb..ab8dc0b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,5 @@ import 'package:cdb_ui/api.dart'; -import 'package:cdb_ui/itemview.dart'; +import 'package:cdb_ui/pages/itemview.dart'; import 'package:flutter/material.dart'; void main() { @@ -23,16 +23,22 @@ class MyApp extends StatelessWidget { } } -// todo : homepage with tabs -// tab 1: home stats -// tab 2: item list -// tab 3: locations - class StatsPage extends StatelessWidget { const StatsPage({super.key}); @override Widget build(BuildContext context) { + // todo : add global statistics + return Scaffold(); + } +} + +class FlowsPage extends StatelessWidget { + const FlowsPage({super.key}); + + @override + Widget build(BuildContext context) { + // todo : add locations tree view return Scaffold(); } } @@ -42,6 +48,7 @@ class LocationsPage extends StatelessWidget { @override Widget build(BuildContext context) { + // todo : add locations tree view return Scaffold(); } } @@ -64,13 +71,12 @@ class ItemsPage extends StatelessWidget { var items = snapshot.data!; - return Expanded( - child: GridView.count( + return GridView.count( crossAxisCount: 2, children: items.map((x) { return ItemCard(x); }).toList(), - )); + ); }), ); } @@ -86,7 +92,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { int pageIndex = 0; - List pages = [StatsPage(), ItemsPage(), LocationsPage()]; + List pages = [StatsPage(), ItemsPage(), FlowsPage(), LocationsPage()]; @override Widget build(BuildContext context) { @@ -96,6 +102,7 @@ class _MyHomePageState extends State { BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"), BottomNavigationBarItem( icon: Icon(Icons.data_object), label: "Items"), + BottomNavigationBarItem(icon: Icon(Icons.receipt), label: "Flows"), BottomNavigationBarItem( icon: Icon(Icons.location_city), label: "Locations"), ], @@ -126,11 +133,11 @@ class ItemCard extends StatelessWidget { MaterialPageRoute(builder: (context) => ItemView(item: itemInfo))); }, child: Row(children: [ - Image.network( + /*Image.network( API().getImageURL(item), width: 128, height: 128, - ), + ),*/ Text(item) ]), ); diff --git a/lib/pages/itemview.dart b/lib/pages/itemview.dart new file mode 100644 index 0000000..d96e2af --- /dev/null +++ b/lib/pages/itemview.dart @@ -0,0 +1,140 @@ +import 'package:cdb_ui/api.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'supply.dart'; + +class ItemView extends StatelessWidget { + final Item item; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column(children: [ + Row( + children: [ + const Align( + alignment: Alignment.centerLeft, + child: Placeholder(), + ), // todo + Column( + children: [ + Text( + item.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text(item.category) + ], + ) + ], + ), + const SizedBox(height: 10), + Row( + children: item.variants.entries.map((entry) { + return Text(entry.value.name); + }).toList()), + FutureBuilder( + future: API().getInventory(item.id), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const CircularProgressIndicator(); + } + var data = snapshot.data!; + + return Expanded( + child: ListView( + children: data.map((x) => TransactionCard(x)).toList())); + }, + ) + ]), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => SupplyPage(item))); + }, + child: const Icon(Icons.add)), + ); + } + + const ItemView({super.key, required this.item}); +} + +class TransactionCard extends StatelessWidget { + final Transaction t; + + const TransactionCard(this.t, {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), + Row( + children: [ + Icon(Icons.store, size: 18, color: Colors.blue), + SizedBox(width: 6), + Text( + t.origin!, + style: TextStyle(fontSize: 14, color: Colors.grey[700]), + ), + ], + ), + ], + ], + ), + ), + ); + } +} + +String tsFormat(int ts) { + DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(ts * 1000); + return DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime); +} diff --git a/lib/pages/supply.dart b/lib/pages/supply.dart new file mode 100644 index 0000000..1025fb4 --- /dev/null +++ b/lib/pages/supply.dart @@ -0,0 +1,199 @@ +import 'package:cdb_ui/api.dart'; +import 'package:flutter/material.dart'; +import 'package:qr_bar_code_scanner_dialog/qr_bar_code_scanner_dialog.dart'; + +class SupplyPage extends StatefulWidget { + final Item item; + + const SupplyPage(this.item, {super.key}); + + @override + State createState() => _SupplyPageState(); +} + +class _SupplyPageState extends State { + late String variant; + final _formKey = GlobalKey(); + String _selectedOrigin = ""; + String _selectedLocation = ""; + String _price = ""; + + @override + void initState() { + super.initState(); + variant = widget.item.variants.keys.first; + } + + Future _supply() async { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + + await API().supplyItem(widget.item.name, variant, "${_price} €", + _selectedOrigin, _selectedLocation); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Item added successfully!')), + ); + Navigator.of(context).pop(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Add New Item'), + ), + body: FutureBuilder( + future: _fetchData(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + + var data = snapshot.data as Map>; + var locations = data['locations']!; + var origins = data['origins']! as List; + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Variant Selection + DropdownButtonFormField( + hint: const Text('Select Variant'), + value: variant, + onChanged: (value) { + setState(() { + variant = value!; + }); + }, + items: widget.item.variants.entries.map((entry) { + return DropdownMenuItem( + value: entry.key, + child: Text(entry.value.name), + ); + }).toList(), + onSaved: (value) { + variant = value!; + }, + ), + + const SizedBox(height: 16), + + // Origin Field with Dropdown and Text Input + // todo : fix state + Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text.isEmpty) { + return origins; + } + return origins.where((String option) { + return option + .toLowerCase() + .contains(textEditingValue.text.toLowerCase()); + }); + }, + onSelected: (String selection) { + setState(() { + _selectedOrigin = selection; + }); + }, + fieldViewBuilder: (context, textEditingController, + focusNode, onFieldSubmitted) { + textEditingController.text = _selectedOrigin; + return TextFormField( + controller: textEditingController, + focusNode: focusNode, + decoration: const InputDecoration( + labelText: 'Origin', + border: OutlineInputBorder(), + ), + ); + }, + ), + + 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: 16), + + // Location Dropdown + DropdownButtonFormField( + hint: const Text('Select Location'), + value: _selectedLocation, + onChanged: (value) { + setState(() { + _selectedLocation = value!; + }); + }, + items: locations.map>((location) { + return DropdownMenuItem( + value: location, + child: Text(location), + ); + }).toList(), + onSaved: (value) { + _selectedLocation = value!; + }, + ), + + IconButton( + onPressed: () { + QrBarCodeScannerDialog().getScannedQrBarCode( + context: context, + onCode: (code) { + setState(() { + _selectedLocation = code!; + }); + }, + ); + }, + icon: const Icon(Icons.qr_code), + ), + + const SizedBox(height: 20), + + // Submit Button + ElevatedButton( + onPressed: _supply, + child: const Text('Add Item'), + ), + ], + ), + ), + ); + }, + ), + ); + } + + Future>> _fetchData() async { + var locations = await API().getLocations(); + var origins = await API().getUniqueField(widget.item.id, variant, "origin"); + origins.insert(0, ""); + locations.insert(0, ""); + return {'locations': locations, 'origins': origins}; + } +} diff --git a/pubspec.lock b/pubspec.lock index 9a360ed..2af11b0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -128,6 +128,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" js: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0c6ca50..8ff6771 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: animated_tree_view: ^2.3.0 qr_bar_code: ^1.3.0 qr_bar_code_scanner_dialog: ^0.0.5 + intl: ^0.18.0 # The following adds the Cupertino Icons font to your application.