feat: add feature cart integrate eith API and life cycle
This commit is contained in:
parent
0d129218de
commit
83e65714ad
|
@ -17,3 +17,5 @@ export 'package:rijig_mobile/features/home/service/about_service.dart';
|
||||||
export 'package:rijig_mobile/globaldata/article/article_repository.dart';
|
export 'package:rijig_mobile/globaldata/article/article_repository.dart';
|
||||||
export 'package:rijig_mobile/globaldata/article/article_service.dart';
|
export 'package:rijig_mobile/globaldata/article/article_service.dart';
|
||||||
export 'package:rijig_mobile/globaldata/article/article_vmod.dart';
|
export 'package:rijig_mobile/globaldata/article/article_vmod.dart';
|
||||||
|
export 'package:rijig_mobile/features/cart/presentation/viewmodel/cartitem_vmod.dart';
|
||||||
|
export 'package:rijig_mobile/features/cart/repositories/cartitem_repo.dart';
|
||||||
|
|
|
@ -13,4 +13,5 @@ void init() {
|
||||||
sl.registerFactory(() => AboutViewModel(AboutService(AboutRepository())));
|
sl.registerFactory(() => AboutViewModel(AboutService(AboutRepository())));
|
||||||
sl.registerFactory(() => AboutDetailViewModel(AboutService(AboutRepository())));
|
sl.registerFactory(() => AboutDetailViewModel(AboutService(AboutRepository())));
|
||||||
sl.registerFactory(() => ArticleViewModel(ArticleService(ArticleRepository())));
|
sl.registerFactory(() => ArticleViewModel(ArticleService(ArticleRepository())));
|
||||||
|
sl.registerFactory(() => CartViewModel(CartRepository()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export 'package:go_router/go_router.dart';
|
export 'package:go_router/go_router.dart';
|
||||||
export 'package:rijig_mobile/core/utils/navigation.dart';
|
export 'package:rijig_mobile/core/utils/navigation.dart';
|
||||||
export 'package:rijig_mobile/features/activity/presentation/screen/activity_screen.dart';
|
export 'package:rijig_mobile/features/activity/presentation/screen/activity_screen.dart';
|
||||||
export 'package:rijig_mobile/features/cart/presentation/cart_screen.dart';
|
export 'package:rijig_mobile/features/cart/presentation/screens/cart_screen.dart';
|
||||||
export 'package:rijig_mobile/features/home/presentation/screen/home_screen.dart';
|
export 'package:rijig_mobile/features/home/presentation/screen/home_screen.dart';
|
||||||
export 'package:rijig_mobile/features/profil/presentation/screen/profil_screen.dart';
|
export 'package:rijig_mobile/features/profil/presentation/screen/profil_screen.dart';
|
||||||
export 'package:rijig_mobile/features/requestpick/presentation/screen/requestpickup_screen.dart';
|
export 'package:rijig_mobile/features/requestpick/presentation/screen/requestpickup_screen.dart';
|
||||||
|
|
|
@ -40,9 +40,9 @@ class Tulisan {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static TextStyle subheading({Color? color}) {
|
static TextStyle subheading({Color? color, double? fontsize}) {
|
||||||
return GoogleFonts.spaceGrotesk(
|
return GoogleFonts.spaceGrotesk(
|
||||||
fontSize: 18.sp,
|
fontSize: fontsize?.sp ?? 18.sp,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: color ?? blackNavyColor,
|
color: color ?? blackNavyColor,
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:iconsax_flutter/iconsax_flutter.dart';
|
||||||
import 'package:rijig_mobile/core/utils/guide.dart';
|
import 'package:rijig_mobile/core/utils/guide.dart';
|
||||||
import 'package:rijig_mobile/core/router.dart';
|
import 'package:rijig_mobile/core/router.dart';
|
||||||
import 'package:rijig_mobile/features/activity/presentation/screen/activity_screen.dart';
|
import 'package:rijig_mobile/features/activity/presentation/screen/activity_screen.dart';
|
||||||
import 'package:rijig_mobile/features/cart/presentation/cart_screen.dart';
|
import 'package:rijig_mobile/features/cart/presentation/screens/cart_screen.dart';
|
||||||
import 'package:rijig_mobile/features/home/presentation/screen/home_screen.dart';
|
import 'package:rijig_mobile/features/home/presentation/screen/home_screen.dart';
|
||||||
import 'package:rijig_mobile/features/profil/presentation/screen/profil_screen.dart';
|
import 'package:rijig_mobile/features/profil/presentation/screen/profil_screen.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class CartItem {
|
||||||
|
final String trashId;
|
||||||
|
final double amount;
|
||||||
|
|
||||||
|
CartItem({required this.trashId, required this.amount});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {'trashid': trashId, 'amount': amount};
|
||||||
|
|
||||||
|
factory CartItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
return CartItem(
|
||||||
|
trashId: json['trashid'],
|
||||||
|
amount: (json['amount'] as num).toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String encodeList(List<CartItem> items) =>
|
||||||
|
jsonEncode(items.map((e) => e.toJson()).toList());
|
||||||
|
|
||||||
|
static List<CartItem> decodeList(String source) =>
|
||||||
|
(jsonDecode(source) as List<dynamic>)
|
||||||
|
.map((e) => CartItem.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
class CartItemResponse {
|
||||||
|
final String trashId;
|
||||||
|
final String trashIcon;
|
||||||
|
final String trashName;
|
||||||
|
final double amount;
|
||||||
|
final double estimatedSubTotalPrice;
|
||||||
|
|
||||||
|
CartItemResponse({
|
||||||
|
required this.trashId,
|
||||||
|
required this.trashIcon,
|
||||||
|
required this.trashName,
|
||||||
|
required this.amount,
|
||||||
|
required this.estimatedSubTotalPrice,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CartItemResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return CartItemResponse(
|
||||||
|
trashId: json['trashid'],
|
||||||
|
trashIcon: json['trashicon'] ?? '',
|
||||||
|
trashName: json['trashname'] ?? '',
|
||||||
|
amount: (json['amount'] as num).toDouble(),
|
||||||
|
estimatedSubTotalPrice:
|
||||||
|
(json['estimated_subtotalprice'] as num).toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CartResponse {
|
||||||
|
final String id;
|
||||||
|
final String userId;
|
||||||
|
final double totalAmount;
|
||||||
|
final double estimatedTotalPrice;
|
||||||
|
final String createdAt;
|
||||||
|
final String updatedAt;
|
||||||
|
final List<CartItemResponse> cartItems;
|
||||||
|
|
||||||
|
CartResponse({
|
||||||
|
required this.id,
|
||||||
|
required this.userId,
|
||||||
|
required this.totalAmount,
|
||||||
|
required this.estimatedTotalPrice,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.cartItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CartResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
var items =
|
||||||
|
(json['cartitems'] as List<dynamic>)
|
||||||
|
.map((e) => CartItemResponse.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return CartResponse(
|
||||||
|
id: json['id'],
|
||||||
|
userId: json['userid'],
|
||||||
|
totalAmount: (json['totalamount'] as num).toDouble(),
|
||||||
|
estimatedTotalPrice: (json['estimated_totalprice'] as num).toDouble(),
|
||||||
|
createdAt: json['createdAt'],
|
||||||
|
updatedAt: json['updatedAt'],
|
||||||
|
cartItems: items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,236 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:rijig_mobile/core/utils/guide.dart';
|
|
||||||
import 'package:rijig_mobile/widget/buttoncard.dart';
|
|
||||||
import 'package:rijig_mobile/widget/counter_dialog.dart';
|
|
||||||
|
|
||||||
class CartScreen extends StatefulWidget {
|
|
||||||
const CartScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CartScreen> createState() => _CartScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CartScreenState extends State<CartScreen> {
|
|
||||||
double totalWeight = 1.0;
|
|
||||||
double pricePerKg = 700;
|
|
||||||
|
|
||||||
void _openEditAmountDialog(String itemName) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return EditAmountDialog(
|
|
||||||
initialAmount: totalWeight,
|
|
||||||
itemName: itemName,
|
|
||||||
pricePerKg: pricePerKg,
|
|
||||||
onSave: (newAmount) {
|
|
||||||
setState(() {
|
|
||||||
totalWeight = newAmount;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _increment() {
|
|
||||||
setState(() {
|
|
||||||
totalWeight += 0.25;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _decrement() {
|
|
||||||
if (totalWeight > 0.25) {
|
|
||||||
setState(() {
|
|
||||||
totalWeight -= 0.25;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String formatAmount(double value) {
|
|
||||||
String formattedValue = value.toStringAsFixed(2);
|
|
||||||
|
|
||||||
if (formattedValue.endsWith('.00')) {
|
|
||||||
return formattedValue.split('.').first;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: whiteColor,
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(
|
|
||||||
'Keranjang Sampah',
|
|
||||||
style: Tulisan.subheading(color: blackNavyColor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: PaddingCustom().paddingHorizontal(20),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(20),
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withValues(alpha: 0.1),
|
|
||||||
spreadRadius: 1,
|
|
||||||
blurRadius: 8,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Lokasi Penjemputan',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
Text(
|
|
||||||
'Lokasi harus di Jember',
|
|
||||||
style: TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
|
|
||||||
Gap(15),
|
|
||||||
CardButtonOne(
|
|
||||||
textButton: "Pilih Alamat",
|
|
||||||
fontSized: 16.sp,
|
|
||||||
color: primaryColor,
|
|
||||||
colorText: whiteColor,
|
|
||||||
borderRadius: 9,
|
|
||||||
horizontal: double.infinity,
|
|
||||||
vertical: 40,
|
|
||||||
onTap: () {
|
|
||||||
debugPrint("pilih alamat tapped");
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withValues(alpha: 0.1),
|
|
||||||
spreadRadius: 1,
|
|
||||||
blurRadius: 8,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Jenis Sampah',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.delete, color: Colors.blue),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Kertas Campur'),
|
|
||||||
Spacer(),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
_openEditAmountDialog('Kertas Campur');
|
|
||||||
},
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text("${formatAmount(totalWeight)} kg"),
|
|
||||||
Icon(Icons.edit, color: Colors.blue),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: _decrement,
|
|
||||||
icon: Icon(Icons.remove),
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
IconButton(
|
|
||||||
onPressed: _increment,
|
|
||||||
icon: Icon(Icons.add),
|
|
||||||
color: Colors.green,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 20),
|
|
||||||
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withValues(alpha: 0.1),
|
|
||||||
spreadRadius: 1,
|
|
||||||
blurRadius: 8,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Estimasi Total Berat',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${formatAmount(totalWeight)} kg",
|
|
||||||
style: TextStyle(fontSize: 18),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 20),
|
|
||||||
|
|
||||||
Center(
|
|
||||||
child: CardButtonOne(
|
|
||||||
textButton: "Lanjutkan",
|
|
||||||
fontSized: 16.sp,
|
|
||||||
color: primaryColor,
|
|
||||||
colorText: whiteColor,
|
|
||||||
borderRadius: 9,
|
|
||||||
horizontal: double.infinity,
|
|
||||||
vertical: 60,
|
|
||||||
onTap: () {
|
|
||||||
debugPrint("lanjutkan tapped");
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,460 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:iconsax_flutter/iconsax_flutter.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:rijig_mobile/core/utils/guide.dart';
|
||||||
|
import 'package:rijig_mobile/features/cart/model/cartitem_model.dart';
|
||||||
|
import 'package:rijig_mobile/features/cart/presentation/viewmodel/cartitem_vmod.dart';
|
||||||
|
import 'package:rijig_mobile/widget/buttoncard.dart';
|
||||||
|
import 'package:rijig_mobile/widget/skeletonize.dart';
|
||||||
|
|
||||||
|
class CartScreen extends StatefulWidget {
|
||||||
|
const CartScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CartScreen> createState() => _CartScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CartScreenState extends State<CartScreen> with WidgetsBindingObserver {
|
||||||
|
CartResponse? cart;
|
||||||
|
bool isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
fetchCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
debugPrint("App resumed, flushing cart...");
|
||||||
|
final vmod = Provider.of<CartViewModel>(context, listen: false);
|
||||||
|
vmod.flushCartToServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchCart() async {
|
||||||
|
final vmod = Provider.of<CartViewModel>(context, listen: false);
|
||||||
|
final result = await vmod.fetchCartFromServer();
|
||||||
|
setState(() {
|
||||||
|
cart = result;
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
double get totalAmount => cart?.totalAmount ?? 0;
|
||||||
|
double get totalPrice => cart?.estimatedTotalPrice ?? 0;
|
||||||
|
|
||||||
|
String formatAmount(double value) {
|
||||||
|
String formattedValue = value.toStringAsFixed(2);
|
||||||
|
return formattedValue.endsWith('.00')
|
||||||
|
? formattedValue.split('.').first
|
||||||
|
: formattedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showEditDialog(String trashId, double currentAmount, int index) {
|
||||||
|
double editedAmount = currentAmount;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Edit Jumlah'),
|
||||||
|
content: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
if (editedAmount > 0.25) editedAmount -= 0.25;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text('${formatAmount(editedAmount)} kg'),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
editedAmount += 0.25;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Batal'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
final vmod = Provider.of<CartViewModel>(context, listen: false);
|
||||||
|
vmod.addOrUpdateItem(
|
||||||
|
CartItem(trashId: trashId, amount: editedAmount),
|
||||||
|
);
|
||||||
|
|
||||||
|
final item = cart!.cartItems[index];
|
||||||
|
setState(() {
|
||||||
|
cart!.cartItems[index] = CartItemResponse(
|
||||||
|
trashIcon: item.trashIcon,
|
||||||
|
trashName: item.trashName,
|
||||||
|
amount: editedAmount,
|
||||||
|
estimatedSubTotalPrice:
|
||||||
|
editedAmount *
|
||||||
|
(item.estimatedSubTotalPrice / item.amount),
|
||||||
|
trashId: item.trashId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Simpan'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void recalculateCartSummary() {
|
||||||
|
double totalWeight = 0;
|
||||||
|
double totalPrice = 0;
|
||||||
|
|
||||||
|
for (final item in cart!.cartItems) {
|
||||||
|
totalWeight += item.amount;
|
||||||
|
totalPrice += item.estimatedSubTotalPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
cart = CartResponse(
|
||||||
|
id: cart!.id,
|
||||||
|
userId: cart!.userId,
|
||||||
|
totalAmount: totalWeight,
|
||||||
|
estimatedTotalPrice: totalPrice,
|
||||||
|
createdAt: cart!.createdAt,
|
||||||
|
updatedAt: cart!.updatedAt,
|
||||||
|
cartItems: cart!.cartItems,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final String? baseUrl = dotenv.env["BASE_URL"];
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: whiteColor,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text("Keranjang Item", style: Tulisan.subheading()),
|
||||||
|
backgroundColor: whiteColor,
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child:
|
||||||
|
isLoading
|
||||||
|
? ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: 3,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return SkeletonCard();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: PaddingCustom().paddingOnly(
|
||||||
|
left: 10,
|
||||||
|
right: 10,
|
||||||
|
bottom: 40,
|
||||||
|
top: 10,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Gap(20),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: cart?.cartItems.length ?? 0,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = cart!.cartItems[index];
|
||||||
|
final perKgPrice =
|
||||||
|
item.estimatedSubTotalPrice / item.amount;
|
||||||
|
final currentAmount = item.amount;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
|
padding: PaddingCustom().paddingAll(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: whiteColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: greyColor.withValues(alpha: 0.1),
|
||||||
|
spreadRadius: 1,
|
||||||
|
blurRadius: 8,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Image.network(
|
||||||
|
"$baseUrl${item.trashIcon}",
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
),
|
||||||
|
const Gap(10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.trashName,
|
||||||
|
style: Tulisan.customText(),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Rp${perKgPrice.toStringAsFixed(0)} / kg",
|
||||||
|
style: Tulisan.body(fontsize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
final vmod =
|
||||||
|
Provider.of<CartViewModel>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
vmod.removeItem(item.trashId);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
cart!.cartItems.removeAt(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
recalculateCartSummary();
|
||||||
|
},
|
||||||
|
|
||||||
|
icon: Icon(
|
||||||
|
Iconsax.trash,
|
||||||
|
color: redColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(10),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"berat",
|
||||||
|
style: Tulisan.body(fontsize: 12),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: greyAbsolutColor,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
final newAmount =
|
||||||
|
(currentAmount - 0.25)
|
||||||
|
.clamp(
|
||||||
|
0.25,
|
||||||
|
double.infinity,
|
||||||
|
);
|
||||||
|
Provider.of<CartViewModel>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
).addOrUpdateItem(
|
||||||
|
CartItem(
|
||||||
|
trashId: item.trashId,
|
||||||
|
amount: newAmount,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
cart!.cartItems[index] =
|
||||||
|
CartItemResponse(
|
||||||
|
trashIcon:
|
||||||
|
item.trashIcon,
|
||||||
|
trashName:
|
||||||
|
item.trashName,
|
||||||
|
amount: newAmount,
|
||||||
|
estimatedSubTotalPrice:
|
||||||
|
newAmount *
|
||||||
|
(item.estimatedSubTotalPrice /
|
||||||
|
item.amount),
|
||||||
|
trashId: item.trashId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
recalculateCartSummary();
|
||||||
|
},
|
||||||
|
child: const Icon(
|
||||||
|
Icons.remove,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap:
|
||||||
|
() => _showEditDialog(
|
||||||
|
item.trashId,
|
||||||
|
currentAmount,
|
||||||
|
index,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
formatAmount(currentAmount),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
final newAmount =
|
||||||
|
currentAmount + 0.25;
|
||||||
|
Provider.of<CartViewModel>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
).addOrUpdateItem(
|
||||||
|
CartItem(
|
||||||
|
trashId: item.trashId,
|
||||||
|
amount: newAmount,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
cart!.cartItems[index] =
|
||||||
|
CartItemResponse(
|
||||||
|
trashIcon:
|
||||||
|
item.trashIcon,
|
||||||
|
trashName:
|
||||||
|
item.trashName,
|
||||||
|
amount: newAmount,
|
||||||
|
estimatedSubTotalPrice:
|
||||||
|
newAmount *
|
||||||
|
(item.estimatedSubTotalPrice /
|
||||||
|
item.amount),
|
||||||
|
trashId: item.trashId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
recalculateCartSummary();
|
||||||
|
},
|
||||||
|
child: const Icon(
|
||||||
|
Icons.add,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: whiteColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: greyAbsolutColor.withValues(alpha: 0.1),
|
||||||
|
spreadRadius: 1,
|
||||||
|
blurRadius: 8,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Cart Total",
|
||||||
|
style: Tulisan.subheading(fontsize: 14),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${formatAmount(totalAmount)} kg",
|
||||||
|
style: Tulisan.body(fontsize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(10),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Harga Total",
|
||||||
|
style: Tulisan.subheading(fontsize: 14),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Rp${totalPrice.toStringAsFixed(0)}",
|
||||||
|
style: Tulisan.body(fontsize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(20),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: CardButtonOne(
|
||||||
|
textButton: "Request Pickup",
|
||||||
|
fontSized: 16.sp,
|
||||||
|
color: primaryColor,
|
||||||
|
colorText: whiteColor,
|
||||||
|
borderRadius: 9,
|
||||||
|
horizontal: double.infinity,
|
||||||
|
vertical: 50,
|
||||||
|
onTap: () async {
|
||||||
|
final vmod = Provider.of<CartViewModel>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
await vmod.flushCartToServer();
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
"Keranjang berhasil dikirim ke server!",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:rijig_mobile/features/cart/model/cartitem_model.dart';
|
||||||
|
import 'package:rijig_mobile/features/cart/repositories/cartitem_repo.dart';
|
||||||
|
|
||||||
|
class CartViewModel extends ChangeNotifier {
|
||||||
|
final CartRepository _repository;
|
||||||
|
|
||||||
|
CartViewModel(this._repository);
|
||||||
|
|
||||||
|
List<CartItem> _cartItems = [];
|
||||||
|
|
||||||
|
List<CartItem> get cartItems => _cartItems;
|
||||||
|
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
|
||||||
|
Future<void> loadLocalCart() async {
|
||||||
|
_isLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
_cartItems = await _repository.getLocalCart();
|
||||||
|
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addOrUpdateItem(CartItem item) {
|
||||||
|
final index = _cartItems.indexWhere((e) => e.trashId == item.trashId);
|
||||||
|
if (index != -1) {
|
||||||
|
_cartItems[index] = item;
|
||||||
|
} else {
|
||||||
|
_cartItems.add(item);
|
||||||
|
}
|
||||||
|
_repository.saveLocalCart(_cartItems);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeItem(String trashId) {
|
||||||
|
_cartItems.removeWhere((e) => e.trashId == trashId);
|
||||||
|
_repository.saveLocalCart(_cartItems);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearLocalCart() async {
|
||||||
|
_cartItems.clear();
|
||||||
|
await _repository.clearLocalCart();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> flushCartToServer() async {
|
||||||
|
if (_cartItems.isEmpty) return;
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
await _repository.flushCartToServer();
|
||||||
|
await clearLocalCart();
|
||||||
|
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CartResponse?> fetchCartFromServer() async {
|
||||||
|
try {
|
||||||
|
return await _repository.getCartFromServer();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error fetching cart: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> commitCart() async {
|
||||||
|
await _repository.commitCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshTTL() async {
|
||||||
|
await _repository.refreshCartTTL();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteItemFromServer(String trashId) async {
|
||||||
|
await _repository.deleteCartItemFromServer(trashId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearCartFromServer() async {
|
||||||
|
await _repository.clearServerCart();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import 'package:rijig_mobile/features/cart/model/cartitem_model.dart';
|
||||||
|
import 'package:rijig_mobile/features/cart/service/cartitem_service.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class CartRepository {
|
||||||
|
final CartService _cartService = CartService();
|
||||||
|
final String _localCartKey = 'local_cart';
|
||||||
|
|
||||||
|
Future<List<CartItem>> getLocalCart() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final raw = prefs.getString(_localCartKey);
|
||||||
|
if (raw == null || raw.isEmpty) return [];
|
||||||
|
|
||||||
|
return CartItem.decodeList(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveLocalCart(List<CartItem> items) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final encoded = CartItem.encodeList(items);
|
||||||
|
await prefs.setString(_localCartKey, encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearLocalCart() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove(_localCartKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> flushCartToServer() async {
|
||||||
|
final items = await getLocalCart();
|
||||||
|
if (items.isEmpty) return;
|
||||||
|
|
||||||
|
await _cartService.postCart(items);
|
||||||
|
await clearLocalCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CartResponse> getCartFromServer() async {
|
||||||
|
return await _cartService.getCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> commitCart() async {
|
||||||
|
await _cartService.commitCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearServerCart() async {
|
||||||
|
await _cartService.clearCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteCartItemFromServer(String trashId) async {
|
||||||
|
await _cartService.deleteCartItem(trashId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshCartTTL() async {
|
||||||
|
await _cartService.refreshCartTTL();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import 'package:rijig_mobile/core/api/api_services.dart';
|
||||||
|
import 'package:rijig_mobile/features/cart/model/cartitem_model.dart';
|
||||||
|
|
||||||
|
class CartService {
|
||||||
|
final Https _https = Https();
|
||||||
|
|
||||||
|
Future<void> postCart(List<CartItem> items) async {
|
||||||
|
final body = {"items": items.map((e) => e.toJson()).toList()};
|
||||||
|
|
||||||
|
await _https.post("/cart", body: body);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CartResponse> getCart() async {
|
||||||
|
final response = await _https.get("/cart");
|
||||||
|
return CartResponse.fromJson(response['data']);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteCartItem(String trashId) async {
|
||||||
|
await _https.delete("/cart/$trashId");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearCart() async {
|
||||||
|
await _https.delete("/cart");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshCartTTL() async {
|
||||||
|
await _https.put("/cart/refresh");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> commitCart() async {
|
||||||
|
await _https.post("/cart/commit");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:rijig_mobile/core/api/api_services.dart';
|
import 'package:rijig_mobile/core/api/api_services.dart';
|
||||||
import 'package:rijig_mobile/features/home/model/about_model.dart';
|
import 'package:rijig_mobile/features/home/model/about_model.dart';
|
||||||
|
|
||||||
|
@ -7,14 +6,14 @@ class AboutRepository {
|
||||||
|
|
||||||
Future<List<AboutModel>> getAboutList() async {
|
Future<List<AboutModel>> getAboutList() async {
|
||||||
final response = await _https.get('/about');
|
final response = await _https.get('/about');
|
||||||
debugPrint("response about: $response");
|
// debugPrint("response about: $response");
|
||||||
final List data = response['data'] ?? [];
|
final List data = response['data'] ?? [];
|
||||||
return data.map((e) => AboutModel.fromJson(e)).toList();
|
return data.map((e) => AboutModel.fromJson(e)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<AboutDetailModel>> getAboutDetail(String id) async {
|
Future<List<AboutDetailModel>> getAboutDetail(String id) async {
|
||||||
final response = await _https.get('/about/$id');
|
final response = await _https.get('/about/$id');
|
||||||
debugPrint("response about detail: $response");
|
// debugPrint("response about detail: $response");
|
||||||
final List aboutDetail = response['data']['about_detail'] ?? [];
|
final List aboutDetail = response['data']['about_detail'] ?? [];
|
||||||
return aboutDetail.map((e) => AboutDetailModel.fromJson(e)).toList();
|
return aboutDetail.map((e) => AboutDetailModel.fromJson(e)).toList();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:rijig_mobile/core/api/api_services.dart';
|
import 'package:rijig_mobile/core/api/api_services.dart';
|
||||||
import 'package:rijig_mobile/globaldata/article/article_model.dart';
|
import 'package:rijig_mobile/globaldata/article/article_model.dart';
|
||||||
|
|
||||||
|
@ -7,7 +6,7 @@ class ArticleRepository {
|
||||||
|
|
||||||
Future<List<ArticleModel>> fetchArticles() async {
|
Future<List<ArticleModel>> fetchArticles() async {
|
||||||
final response = await _https.get('/article-rijik/view-article');
|
final response = await _https.get('/article-rijik/view-article');
|
||||||
debugPrint("reponse article: $response");
|
// debugPrint("reponse article: $response");
|
||||||
final List data = response['data'];
|
final List data = response['data'];
|
||||||
return data.map((json) => ArticleModel.fromJson(json)).toList();
|
return data.map((json) => ArticleModel.fromJson(json)).toList();
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ class MyApp extends StatelessWidget {
|
||||||
ChangeNotifierProvider(create: (_) => sl<AboutViewModel>()),
|
ChangeNotifierProvider(create: (_) => sl<AboutViewModel>()),
|
||||||
ChangeNotifierProvider(create: (_) => sl<AboutDetailViewModel>()),
|
ChangeNotifierProvider(create: (_) => sl<AboutDetailViewModel>()),
|
||||||
ChangeNotifierProvider(create: (_) => sl<ArticleViewModel>()),
|
ChangeNotifierProvider(create: (_) => sl<ArticleViewModel>()),
|
||||||
|
ChangeNotifierProvider(create: (_) => sl<CartViewModel>()),
|
||||||
],
|
],
|
||||||
child: ScreenUtilInit(
|
child: ScreenUtilInit(
|
||||||
designSize: const Size(375, 812),
|
designSize: const Size(375, 812),
|
||||||
|
|
Loading…
Reference in New Issue