import 'package:cdb_ui/api.dart'; import 'package:cdb_ui/pages/consume.dart'; import 'package:cdb_ui/pages/supply.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:qr_bar_code/qr/qr.dart'; class TransactionPage extends StatefulWidget { final Transaction transaction; final Function? refresh; const TransactionPage(this.transaction, {this.refresh, super.key}); @override State createState() => _TransactionPageState(); } class _TransactionPageState extends State { late Transaction transaction; @override void initState() { super.initState(); transaction = widget.transaction; } Future reload() async { if (widget.refresh != null) { widget.refresh!(); } var updateTransaction = await API().getTransaction(transaction.uuid); setState(() { transaction = updateTransaction; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(API().getItem(transaction.item).name), actions: [ IconButton( onPressed: () { var locations = API().getLocations(); List locationList = locations.keys.toList(); String? selectedLocationID; showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Move Transaction'), content: Row( mainAxisSize: MainAxisSize.min, children: [ DropdownButton( value: selectedLocationID, onChanged: (value) { selectedLocationID = value!; API() .moveTransaction(widget.transaction.uuid, selectedLocationID!) .then((x) { Navigator.of(context).pop(); }); setState(() {}); }, items: locationList .map>((locationID) { return DropdownMenuItem( value: locationID, child: Text(locations[locationID]!.name), ); }).toList(), ), IconButton( onPressed: () async { var locations = API().getLocations(); var code = await scanQRCode(context, title: "Scan Location Code"); if (!locations.containsKey(code)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'The location $code does not exist.')), ); return; } API() .moveTransaction(widget.transaction.uuid, selectedLocationID!) .then( (x) { Navigator.of(context).pop(); }, ); setState(() {}); }, icon: const Icon(Icons.qr_code)) ], ), ); }, ); }, icon: const Icon(Icons.move_up)) ], ), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 18.0), child: Column( children: [ Row( children: [ QRCode( data: transaction.uuid, size: 128, eyeStyle: const QREyeStyle(color: Colors.white), dataModuleStyle: const QRDataModuleStyle(color: Colors.white), semanticsLabel: "Transaction UUID", ), const SizedBox( width: 16.0, ), Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // todo : human names Text( API().getItem(transaction.item).name, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 28), ), Text( API() .getItem(transaction.item) .variants[transaction.variant]! .name, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 20, color: Colors.grey), ), const SizedBox( height: 8.0, ), Text("Added: ${tsFormat(transaction.timestamp)}"), ], ) ], ), const Divider(), const SizedBox( height: 12.0, ), if (transaction.expired) const Text("Transaction is Expired!"), IconText(Icons.money, transaction.price.toStringAsFixed(2), color: Colors.green), if (transaction.origin != null) IconText(Icons.store, transaction.origin!, color: Colors.blue), if (transaction.location != null) IconText(Icons.location_city, transaction.location!.name), if (transaction.note != null) Text(transaction.note!), if (transaction.consumed != null) ...[ const Divider(), Text("Consumed on: ${tsFormat(transaction.consumed!.timestamp)}"), IconText(Icons.store, transaction.consumed!.destination, color: Colors.blue), IconText( Icons.money, transaction.consumed!.price.toStringAsFixed(2), color: Colors.green), ] // todo : chart with price history ], ), ), floatingActionButton: transaction.consumed == null ? FloatingActionButton( onPressed: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) { return ConsumePage(transaction, reload); }, )); }, child: const Icon(Icons.receipt_long)) : null, ); } } class TransactionCard extends StatelessWidget { final Transaction t; final Function refresh; final Function(Transaction)? onTap; final Function(Transaction)? onLongPress; const TransactionCard(this.t, this.refresh, {this.onTap, this.onLongPress, super.key}); @override Widget build(BuildContext context) { return InkWell( onTap: () { if (onTap != null) { onTap!(t); return; } Navigator.of(context).push(MaterialPageRoute( builder: (context) { return ConsumePage(t, refresh); }, )); }, onLongPress: () { if (onLongPress != null) { onLongPress!(t); return; } Navigator.of(context).push(MaterialPageRoute( builder: (context) { return TransactionPage(t); }, )); }, child: Card( color: t.expired ? Colors.red[100] : Colors.black, margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), elevation: 4, child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Text( API().getItem(t.item).name, style: const TextStyle(fontSize: 16), ), const SizedBox( width: 8, ), Text( API().getItem(t.item).variants[t.variant]!.name, style: TextStyle(fontSize: 14, color: Colors.grey[400]), ), ], ), if (t.timestamp != 0) Text( tsFormat(t.timestamp), style: TextStyle(fontSize: 14, color: Colors.grey[700]), ), ], ), if ((t.note ?? "").isNotEmpty) ...[ const SizedBox( height: 4, ), Text(t.note!) ], const SizedBox( height: 10, ), IconText(Icons.money, "${t.price.toStringAsFixed(2)} €", color: Colors.green), if (t.origin != null) ...[ const SizedBox(height: 8), IconText(Icons.store, t.origin!, color: Colors.blue) ], if (t.location != null) ...[ const SizedBox( height: 8, ), IconText(Icons.location_city, t.location!.name) ] ], ), ), ), ); } } String tsFormat(int ts) { DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(ts * 1000); return DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime); } class IconText extends StatelessWidget { final IconData icon; final String text; final Color? color; const IconText(this.icon, this.text, {super.key, this.color}); @override Widget build(BuildContext context) { return Row( children: [ Icon(icon, size: 18, color: color), const SizedBox(width: 6), Text( text, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), overflow: TextOverflow.fade, ), ], ); } } class TransactionSelectPage extends StatelessWidget { final Function(Transaction) onSelect; final List selections; final List? exclude; const TransactionSelectPage(this.selections, {super.key, required this.onSelect, this.exclude}); @override Widget build(BuildContext context) { var selectionList = []; for (var s in selections) { if (exclude?.any((x) => x.uuid == s.uuid) ?? false) { continue; } selectionList.add(s); } return Scaffold( appBar: AppBar( title: const Text("Select a Transaction"), ), body: ListView( children: selectionList.isEmpty ? [ const ListTile( title: Center(child: Text("No Transactions available")), ) ] : selectionList .map((x) => TransactionCard( x, () {}, onLongPress: (x) {}, onTap: (t) { onSelect(t); Navigator.of(context).pop(); }, )) .toList()), ); } }