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_service.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(() => AboutDetailViewModel(AboutService(AboutRepository())));
|
||||
sl.registerFactory(() => ArticleViewModel(ArticleService(ArticleRepository())));
|
||||
sl.registerFactory(() => CartViewModel(CartRepository()));
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export 'package:go_router/go_router.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/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/profil/presentation/screen/profil_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(
|
||||
fontSize: 18.sp,
|
||||
fontSize: fontsize?.sp ?? 18.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
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/router.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/profil/presentation/screen/profil_screen.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/features/home/model/about_model.dart';
|
||||
|
||||
|
@ -7,14 +6,14 @@ class AboutRepository {
|
|||
|
||||
Future<List<AboutModel>> getAboutList() async {
|
||||
final response = await _https.get('/about');
|
||||
debugPrint("response about: $response");
|
||||
// debugPrint("response about: $response");
|
||||
final List data = response['data'] ?? [];
|
||||
return data.map((e) => AboutModel.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<List<AboutDetailModel>> getAboutDetail(String id) async {
|
||||
final response = await _https.get('/about/$id');
|
||||
debugPrint("response about detail: $response");
|
||||
// debugPrint("response about detail: $response");
|
||||
final List aboutDetail = response['data']['about_detail'] ?? [];
|
||||
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/globaldata/article/article_model.dart';
|
||||
|
||||
|
@ -7,7 +6,7 @@ class ArticleRepository {
|
|||
|
||||
Future<List<ArticleModel>> fetchArticles() async {
|
||||
final response = await _https.get('/article-rijik/view-article');
|
||||
debugPrint("reponse article: $response");
|
||||
// debugPrint("reponse article: $response");
|
||||
final List data = response['data'];
|
||||
return data.map((json) => ArticleModel.fromJson(json)).toList();
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ class MyApp extends StatelessWidget {
|
|||
ChangeNotifierProvider(create: (_) => sl<AboutViewModel>()),
|
||||
ChangeNotifierProvider(create: (_) => sl<AboutDetailViewModel>()),
|
||||
ChangeNotifierProvider(create: (_) => sl<ArticleViewModel>()),
|
||||
ChangeNotifierProvider(create: (_) => sl<CartViewModel>()),
|
||||
],
|
||||
child: ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
|
|
Loading…
Reference in New Issue