receipt/all and receipt/one for frontend

This commit is contained in:
Mikkel Troels Kongsted 2025-03-19 13:47:02 +01:00
parent c729175a28
commit fd288dafee
12 changed files with 285 additions and 141 deletions

View File

@ -1,85 +1,16 @@
import 'package:flutter/material.dart';
import 'package:mobile/models/cart_item.dart';
import 'package:mobile/models/product.dart';
import 'package:mobile/controllers/session.dart';
import 'package:mobile/models/receipt.dart';
import 'package:mobile/results.dart';
import 'package:mobile/server/server.dart';
class ReceiptController extends ChangeNotifier {
int nextId = 0;
final List<Receipt> receipts = [];
class ReceiptController {
final Server server;
final SessionController sessionController;
List<Receipt> allReceipts() {
return receipts;
}
ReceiptController({required this.server, required this.sessionController});
List<Receipt> sortedReceiptsByDate() {
List<Receipt> clonedReceipts = [];
for (var i = 0; i < receipts.length; i++) {
clonedReceipts.add(receipts[i]);
}
clonedReceipts.sort((a, b) => b.date.compareTo(a.date));
return clonedReceipts;
}
Receipt? receiptWithId(int id) {
for (var i = 0; i < receipts.length; i++) {
if (receipts[i].id == id) {
return receipts[i];
}
}
return null;
}
void createReceipt(List<CartItem> cartItems) {
List<ReceiptItem> receiptItems = [];
for (var i = 0; i < cartItems.length; i++) {
final ReceiptItem receiptItem = ReceiptItem(
amount: cartItems[i].amount, product: cartItems[i].product);
receiptItems.add(receiptItem);
}
receipts.add(Receipt(
date: DateTime.now(), receiptItems: receiptItems, id: nextId++));
notifyListeners();
}
}
class Receipt {
final int id;
final DateTime date;
final List<ReceiptItem> receiptItems;
Receipt({required this.date, required this.receiptItems, required this.id});
List<ReceiptItem> allReceiptItems() {
return receiptItems;
}
ReceiptItem? withProductId(int productId) {
for (var i = 0; i < receiptItems.length; i++) {
if (receiptItems[i].product.id == productId) {
return receiptItems[i];
}
}
return null;
}
int totalPrice() {
var result = 0;
for (var i = 0; i < receiptItems.length; i++) {
result += receiptItems[i].totalPrice();
}
return result;
}
String dateFormatted() {
return "${date.day}-${date.month}-${date.year}";
}
}
class ReceiptItem {
final Product product;
final int amount;
ReceiptItem({required this.product, required this.amount});
int totalPrice() {
return product.priceDkkCent * amount;
Future<Result<Receipt, String>> receiptWithId(int id) async {
return await sessionController.requestWithSession(
(Server server, String token) => server.oneReceipt(token, id));
}
}

View File

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:mobile/controllers/session.dart';
import 'package:mobile/models/receipt.dart';
import 'package:mobile/results.dart';
import 'package:mobile/server/server.dart';
class ReceiptHeaderController extends ChangeNotifier {
List<ReceiptHeader> _receiptHeaders = [];
final Server server;
final SessionController sessionController;
ReceiptHeaderController(
{required this.server, required this.sessionController}) {
fetchReceiptsFromServer();
}
Future<void> fetchReceiptsFromServer() async {
final res = await sessionController.requestWithSession(
(Server server, String token) => server.allReceipts(token));
switch (res) {
case Ok<List<ReceiptHeader>, String>(value: final receiptHeaders):
_receiptHeaders = receiptHeaders;
case Err<List<ReceiptHeader>, String>():
break;
}
notifyListeners();
}
List<ReceiptHeader> receiptHeadersSortedByDate() {
List<ReceiptHeader> clonedReceiptHeaders = [];
for (var i = 0; i < _receiptHeaders.length; i++) {
clonedReceiptHeaders.add(_receiptHeaders[i]);
}
clonedReceiptHeaders.sort((a, b) => b.timestamp.compareTo(a.timestamp));
return clonedReceiptHeaders;
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:mobile/controllers/receipt_header.dart';
import 'package:mobile/controllers/session.dart';
import 'package:mobile/pages/dashboard.dart';
import 'package:mobile/pages/log_in_page.dart';
@ -56,7 +57,12 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(
create: (_) => CartControllerCache(
server: server, sessionController: sessionController)),
ChangeNotifierProvider(create: (_) => ReceiptController()),
Provider(
create: (_) => ReceiptController(
server: server, sessionController: sessionController)),
ChangeNotifierProvider(
create: (_) => ReceiptHeaderController(
server: server, sessionController: sessionController)),
ChangeNotifierProvider(create: (_) => PayingStateController()),
ChangeNotifierProvider(create: (_) => AddToCartStateController()),
ChangeNotifierProvider(create: (_) => LocationImageController()),

View File

@ -0,0 +1,72 @@
class Receipt {
final int id;
final DateTime timestamp;
final List<ReceiptItem> receiptItems;
Receipt(
{required this.timestamp, required this.receiptItems, required this.id});
List<ReceiptItem> allReceiptItems() {
return receiptItems;
}
ReceiptItem? withProductId(int productId) {
for (var i = 0; i < receiptItems.length; i++) {
if (receiptItems[i].productId == productId) {
return receiptItems[i];
}
}
return null;
}
int totalPrice() {
var result = 0;
for (var i = 0; i < receiptItems.length; i++) {
result += receiptItems[i].totalPrice();
}
return result;
}
Receipt.fromJson(Map<String, dynamic> json)
: id = json["receipt_id"],
timestamp = DateTime.parse(json["timestamp"]),
receiptItems = (json["products"] as List<dynamic>)
.map(((receiptItem) => ReceiptItem.fromJson(receiptItem)))
.toList();
}
class ReceiptItem {
final int productId;
final String name;
final int priceDkkCent;
final int amount;
ReceiptItem(
{required this.productId,
required this.name,
required this.priceDkkCent,
required this.amount});
int totalPrice() {
return priceDkkCent * amount;
}
ReceiptItem.fromJson(Map<String, dynamic> json)
: productId = json["product_id"],
name = json["name"],
priceDkkCent = json["price_dkk_cent"],
amount = json["amount"];
}
class ReceiptHeader {
final int id;
final DateTime timestamp;
final int totalDkkCent;
ReceiptHeader(
{required this.timestamp, required this.id, required this.totalDkkCent});
ReceiptHeader.fromJson(Map<String, dynamic> json)
: id = json["id"],
totalDkkCent = json["total_dkk_cent"],
timestamp = DateTime.parse(json["timestamp"]);
}

View File

@ -1,18 +1,14 @@
import 'package:flutter/material.dart';
import 'package:mobile/controllers/receipt_header.dart';
import 'package:mobile/models/receipt.dart';
import 'package:mobile/pages/receipt_page.dart';
import 'package:mobile/controllers/receipt.dart';
import 'package:mobile/utils/date.dart';
import 'package:mobile/utils/price.dart';
import 'package:provider/provider.dart';
class ReceiptsListItem extends StatelessWidget {
final String dateFormatted;
final int totalPrice;
final ReceiptPage receiptPage;
const ReceiptsListItem(
{super.key,
required this.dateFormatted,
required this.totalPrice,
required this.receiptPage});
final ReceiptHeader receiptHeader;
const ReceiptsListItem({super.key, required this.receiptHeader});
@override
Widget build(BuildContext context) {
@ -21,14 +17,19 @@ class ReceiptsListItem extends StatelessWidget {
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(10)),
onTap: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => receiptPage));
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ReceiptPage(
receiptId: receiptHeader.id,
)));
},
child: Container(
padding: const EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text(dateFormatted), Text(formatDkkCents(totalPrice))],
children: [
Text(dateFormatted(receiptHeader.timestamp)),
Text(formatDkkCents(receiptHeader.totalDkkCent))
],
),
),
),
@ -41,20 +42,21 @@ class AllReceiptsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
context.read<ReceiptHeaderController>().fetchReceiptsFromServer();
return Column(
children: [
Expanded(child: Consumer<ReceiptController>(
builder: (_, receiptRepo, __) {
final allReceipts = receiptRepo.sortedReceiptsByDate();
Expanded(child: Consumer<ReceiptHeaderController>(
builder: (_, receiptHeaderController, __) {
final allReceiptHeaders =
receiptHeaderController.receiptHeadersSortedByDate();
return ListView.builder(
shrinkWrap: true,
itemBuilder: (_, idx) {
return ReceiptsListItem(
dateFormatted: allReceipts[idx].dateFormatted(),
totalPrice: allReceipts[idx].totalPrice(),
receiptPage: ReceiptPage(receipt: allReceipts[idx]));
receiptHeader: allReceiptHeaders[idx],
);
},
itemCount: allReceipts.length,
itemCount: allReceiptHeaders.length,
);
},
)),

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:mobile/controllers/routing.dart';
import 'package:mobile/controllers/cart.dart';
import 'package:mobile/controllers/paying_state.dart';
import 'package:mobile/controllers/receipt.dart';
import 'package:mobile/results.dart';
import 'package:mobile/utils/price.dart';
import 'package:mobile/widgets/primary_button.dart';
@ -16,7 +15,6 @@ class FinishShoppingPage extends StatelessWidget {
Widget build(BuildContext context) {
final CartControllerCache cartController =
context.read<CartControllerCache>();
final ReceiptController receiptRepo = context.read<ReceiptController>();
final PayingStateController payingStateRepo =
context.watch<PayingStateController>();
final cart = cartController.allCartItems();
@ -70,8 +68,8 @@ class FinishShoppingPage extends StatelessWidget {
actions: <Widget>[
TextButton(
onPressed: () =>
Navigator.pop(context, 'OK'),
child: const Text('OK'),
Navigator.pop(context, 'Ok'),
child: const Text('Ok'),
),
],
),
@ -80,7 +78,6 @@ class FinishShoppingPage extends StatelessWidget {
payingStateRepo.reset();
return;
}
receiptRepo.createReceipt(cart);
payingStateRepo.next();
await Future.delayed(const Duration(seconds: 1));
cartController.clearCart();

View File

@ -1,15 +1,19 @@
import 'package:flutter/material.dart';
import 'package:mobile/controllers/receipt.dart';
import 'package:mobile/models/receipt.dart';
import 'package:mobile/results.dart';
import 'package:mobile/utils/date.dart';
import 'package:mobile/utils/price.dart';
import 'package:mobile/widgets/receipt_item.dart';
import 'package:provider/provider.dart';
class ReceiptView extends StatelessWidget {
final Receipt receipt;
const ReceiptView({super.key, required this.receipt});
final int receiptId;
const ReceiptView({super.key, required this.receiptId});
@override
Widget build(BuildContext context) {
final receiptItems = receipt.allReceiptItems();
final receiptController = context.read<ReceiptController>();
return SafeArea(
child: Column(
@ -19,34 +23,65 @@ class ReceiptView extends StatelessWidget {
Expanded(
child: Container(
margin: const EdgeInsets.all(20),
child: Column(
children: [
Text(receipt.dateFormatted()),
Expanded(
child: Column(
children: [
ListView.builder(
shrinkWrap: true,
itemBuilder: (_, idx) => ReceiptItemView(
pricePerAmount:
receiptItems[idx].product.priceDkkCent,
name: receiptItems[idx].product.name,
amount: receiptItems[idx].amount),
itemCount: receiptItems.length),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Total:",
style: TextStyle(fontWeight: FontWeight.bold),
child: FutureBuilder(
future: receiptController.receiptWithId(receiptId),
builder: (context, snapshot) {
final error = snapshot.error;
if (error != null) {
throw error;
}
final receipt = snapshot.data;
switch (receipt) {
case null:
return const CircularProgressIndicator();
case Err<Receipt, String>(value: final message):
showDialog<String>(
context: context,
builder: (BuildContext context) => AlertDialog(
content: Text(message),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'Ok'),
child: const Text('Ok'),
),
],
),
Text(formatDkkCents(receipt.totalPrice())),
],
),
],
)),
],
),
);
return Container();
case Ok<Receipt, String>(value: final receipt):
return Column(
children: [
Text(dateFormatted(receipt.timestamp)),
Expanded(
child: Column(
children: [
ListView.builder(
shrinkWrap: true,
itemBuilder: (_, idx) => ReceiptItemView(
pricePerAmount: receipt
.receiptItems[idx].priceDkkCent,
name: receipt.receiptItems[idx].name,
amount:
receipt.receiptItems[idx].amount),
itemCount: receipt.receiptItems.length),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text(
"Total:",
style: TextStyle(
fontWeight: FontWeight.bold),
),
Text(formatDkkCents(receipt.totalPrice())),
],
),
],
)),
],
);
}
}),
),
),
],
@ -56,12 +91,12 @@ class ReceiptView extends StatelessWidget {
}
class ReceiptPage extends StatelessWidget {
final Receipt receipt;
final int receiptId;
const ReceiptPage({super.key, required this.receipt});
const ReceiptPage({super.key, required this.receiptId});
@override
Widget build(BuildContext context) {
return Scaffold(body: ReceiptView(receipt: receipt));
return Scaffold(body: ReceiptView(receiptId: receiptId));
}
}

View File

@ -42,8 +42,8 @@ class SaldoSettingsPage extends StatelessWidget {
content: Text('Serverfejl: $message'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
onPressed: () => Navigator.pop(context, 'Ok'),
child: const Text('Ok'),
),
],
));

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:mobile/models/cart_item.dart';
import 'package:mobile/models/product.dart';
import 'package:mobile/models/receipt.dart';
import 'package:mobile/models/user.dart';
import 'package:mobile/results.dart';
import 'package:mobile/server/server.dart';
@ -30,7 +31,7 @@ class BackendServer implements Server {
.then((res) => json.decode(res.body));
if (res["ok"]) {
return Ok((res["products"] as List<dynamic>)
.map(((product) => Product.fromJson(product)))
.map(((productJson) => Product.fromJson(productJson)))
.toList());
} else {
return Err(res["msg"]);
@ -149,4 +150,43 @@ class BackendServer implements Server {
return Err(res["msg"]);
}
}
@override
Future<Result<List<ReceiptHeader>, String>> allReceipts(String token) async {
final res = await http.get(
Uri.parse("$_apiUrl/receipts/all"),
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Session-Token": token
},
).then((res) => json.decode(res.body));
if (res["ok"]) {
return Ok((res["receipts"] as List<dynamic>)
.map(((receiptHeaderJson) =>
ReceiptHeader.fromJson(receiptHeaderJson)))
.toList());
} else {
return Err(res["msg"]);
}
}
@override
Future<Result<Receipt, String>> oneReceipt(String token, int id) async {
final res = await http.get(
Uri.parse("$_apiUrl/receipts/one?receipt_id=$id"),
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Session-Token": token
},
).then((res) => json.decode(res.body));
if (res["ok"]) {
return Ok((Receipt.fromJson(res["receipt"] as Map<String, dynamic>)));
} else {
return Err(res["msg"]);
}
}
}

View File

@ -1,6 +1,7 @@
import 'package:mobile/models/cart_item.dart';
import 'package:mobile/models/coordinate.dart';
import 'package:mobile/models/product.dart';
import 'package:mobile/models/receipt.dart';
import 'package:mobile/models/user.dart';
import 'package:mobile/results.dart';
import 'package:mobile/server/server.dart';
@ -132,4 +133,17 @@ class MockServer implements Server {
Future<Result<Null, String>> addBalance(String token) async {
return const Ok(null);
}
@override
Future<Result<List<ReceiptHeader>, String>> allReceipts(String token) async {
return Ok([
ReceiptHeader(timestamp: DateTime.now(), id: 0, totalDkkCent: 1242431)
]);
}
@override
Future<Result<Receipt, String>> oneReceipt(String token, int id) async {
return Ok(Receipt(
timestamp: DateTime.now(), id: id, receiptItems: <ReceiptItem>[]));
}
}

View File

@ -1,5 +1,6 @@
import 'package:mobile/models/cart_item.dart';
import 'package:mobile/models/product.dart';
import 'package:mobile/models/receipt.dart';
import 'package:mobile/models/user.dart';
import 'package:mobile/results.dart';
@ -24,4 +25,7 @@ abstract class Server {
String token, List<CartItem> cartItems);
Future<Result<Null, String>> addBalance(String token);
Future<Result<List<ReceiptHeader>, String>> allReceipts(String token);
Future<Result<Receipt, String>> oneReceipt(String token, int id);
}

View File

@ -0,0 +1,3 @@
String dateFormatted(DateTime date) {
return "${date.day}-${date.month}-${date.year} ${date.hour}:${date.minute}";
}