feat: feature cart and integrate with trash data us u can see
This commit is contained in:
parent
4db92f3d22
commit
918e3c0ced
|
@ -17,5 +17,9 @@ export 'package:rijig_mobile/globaldata/about/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';
|
||||
|
||||
export 'package:rijig_mobile/features/cart/presentation/viewmodel/trashcart_vmod.dart';
|
||||
export 'package:rijig_mobile/features/cart/repositories/trashcart_repo.dart';
|
||||
export 'package:rijig_mobile/features/cart/service/trashcart_service.dart';
|
||||
// export 'package:rijig_mobile/features/cart/presentation/viewmodel/cartitem_vmod.dart';
|
||||
// export 'package:rijig_mobile/features/cart/repositories/cartitem_repo.dart';
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import 'package:rijig_mobile/core/container/export_vmod.dart';
|
||||
// import 'package:rijig_mobile/features/cart/presentation/viewmodel/trashcart_vmod.dart';
|
||||
// import 'package:rijig_mobile/features/cart/repositories/trashcart_repo.dart';
|
||||
// import 'package:rijig_mobile/features/cart/service/trashcart_service.dart';
|
||||
|
||||
final sl = GetIt.instance;
|
||||
|
||||
|
@ -7,11 +10,35 @@ void init() {
|
|||
sl.registerFactory(() => OtpViewModel(OtpService(OtpRepository())));
|
||||
sl.registerFactory(() => LogoutViewModel(LogoutService(LogoutRepository())));
|
||||
|
||||
sl.registerFactory(
|
||||
() => TrashViewModel(TrashCategoryService(TrashCategoryRepository())),
|
||||
sl.registerLazySingleton<ITrashCategoryRepository>(
|
||||
() => TrashCategoryRepository(),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton<ITrashCategoryService>(
|
||||
() => TrashCategoryService(sl<ITrashCategoryRepository>()),
|
||||
);
|
||||
|
||||
sl.registerFactory<TrashViewModel>(
|
||||
() => TrashViewModel(sl<ITrashCategoryService>()),
|
||||
);
|
||||
|
||||
sl.registerFactory(() => AboutViewModel(AboutService(AboutRepository())));
|
||||
sl.registerFactory(() => AboutDetailViewModel(AboutService(AboutRepository())));
|
||||
sl.registerFactory(() => ArticleViewModel(ArticleService(ArticleRepository())));
|
||||
sl.registerFactory(() => CartViewModel(CartRepository()));
|
||||
sl.registerFactory(
|
||||
() => AboutDetailViewModel(AboutService(AboutRepository())),
|
||||
);
|
||||
sl.registerFactory(
|
||||
() => ArticleViewModel(ArticleService(ArticleRepository())),
|
||||
);
|
||||
// sl.registerFactory(() => CartViewModel(CartRepository()));
|
||||
sl.registerLazySingleton<CartRepository>(
|
||||
() => CartRepositoryImpl(),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton<CartService>(
|
||||
() => CartServiceImpl(repository: sl<CartRepository>()),
|
||||
);
|
||||
|
||||
sl.registerFactory<CartViewModel>(
|
||||
() => CartViewModel(cartService: sl<CartService>()),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -177,10 +177,10 @@ class NetworkDialogManager {
|
|||
required VoidCallback onRetry,
|
||||
required VoidCallback onExit,
|
||||
}) {
|
||||
CustomModalDialog.show(
|
||||
CustomModalDialog.showImage(
|
||||
context: context,
|
||||
showCloseIcon: false,
|
||||
variant: ModalVariant.imageVersion,
|
||||
// variant: ModalVariant.imageVersion,
|
||||
title: 'Tidak Ada Koneksi Internet',
|
||||
content:
|
||||
'Sepertinya koneksi internet Anda bermasalah. Periksa koneksi WiFi atau data seluler Anda, lalu coba lagi.',
|
||||
|
@ -209,10 +209,10 @@ class NetworkDialogManager {
|
|||
required BuildContext context,
|
||||
required VoidCallback onRetry,
|
||||
}) {
|
||||
CustomModalDialog.show(
|
||||
CustomModalDialog.showImage(
|
||||
context: context,
|
||||
showCloseIcon: false,
|
||||
variant: ModalVariant.imageVersion,
|
||||
// variant: ModalVariant.imageVersion,
|
||||
title: 'Koneksi Timeout',
|
||||
content:
|
||||
'Permintaan memakan waktu terlalu lama. Periksa koneksi internet Anda dan coba lagi.',
|
||||
|
@ -241,10 +241,10 @@ class NetworkDialogManager {
|
|||
required BuildContext context,
|
||||
required VoidCallback onRetry,
|
||||
}) {
|
||||
CustomModalDialog.show(
|
||||
CustomModalDialog.showImage(
|
||||
context: context,
|
||||
showCloseIcon: false,
|
||||
variant: ModalVariant.imageVersion,
|
||||
// variant: ModalVariant.imageVersion,
|
||||
title: 'Koneksi Gagal',
|
||||
content:
|
||||
'Tidak dapat terhubung ke server. Pastikan koneksi internet Anda stabil.',
|
||||
|
@ -274,15 +274,14 @@ class NetworkDialogManager {
|
|||
required BuildContext context,
|
||||
required VoidCallback onContinue,
|
||||
}) {
|
||||
CustomModalDialog.show(
|
||||
CustomModalDialog.showImage(
|
||||
context: context,
|
||||
showCloseIcon: false,
|
||||
variant: ModalVariant.imageVersion,
|
||||
// variant: ModalVariant.imageVersion,
|
||||
title: 'Koneksi Lambat',
|
||||
content:
|
||||
'Koneksi internet Anda lambat. Beberapa fitur mungkin tidak berfungsi optimal.',
|
||||
imageAsset:
|
||||
'assets/images/poor_connection.png',
|
||||
imageAsset: 'assets/images/poor_connection.png',
|
||||
buttonCount: 2,
|
||||
button1: ElevatedButton(
|
||||
onPressed: onContinue,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:rijig_mobile/core/utils/exportimportview.dart';
|
||||
import 'package:rijig_mobile/features/profil/components/secure_pin_input.dart';
|
||||
|
||||
final router = GoRouter(
|
||||
routes: [
|
||||
|
@ -72,11 +71,6 @@ final router = GoRouter(
|
|||
builder: (context, state) => DatavisualizedScreen(),
|
||||
),
|
||||
GoRoute(path: '/activity', builder: (context, state) => ActivityScreen()),
|
||||
GoRoute(
|
||||
path: '/requestpickup',
|
||||
builder: (context, state) => RequestPickScreen(),
|
||||
),
|
||||
GoRoute(path: '/cart', builder: (context, state) => CartScreen()),
|
||||
GoRoute(path: '/profil', builder: (context, state) => ProfilScreen()),
|
||||
|
||||
GoRoute(
|
||||
|
|
|
@ -2,11 +2,9 @@ export 'package:flutter/material.dart';
|
|||
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/screens/cart_screen.dart';
|
||||
export 'package:rijig_mobile/features/home/presentation/screen/home_screen.dart';
|
||||
export 'package:rijig_mobile/features/home/datavisualized/presentation/screen/datavisualized_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/auth/presentation/screen/inputpin_screen.dart';
|
||||
export 'package:rijig_mobile/features/auth/presentation/screen/login_screen.dart';
|
||||
export 'package:rijig_mobile/features/auth/presentation/screen/otp_screen.dart';
|
||||
|
@ -28,5 +26,6 @@ export 'package:rijig_mobile/features/pickup/presentation/screen/pickup_map_scre
|
|||
|
||||
|
||||
// remmovable
|
||||
export 'package:rijig_mobile/features/home/presentation/components/cart_test_screen.dart';
|
||||
export 'package:rijig_mobile/features/home/presentation/components/trash_testview.dart';
|
||||
export 'package:rijig_mobile/features/cart/presentation/screens/cart_test_screen.dart';
|
||||
export 'package:rijig_mobile/features/profil/components/secure_pin_input.dart';
|
||||
export 'package:rijig_mobile/features/requestpick/presentation/screen/trash_testview.dart';
|
|
@ -3,7 +3,8 @@ 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/screens/cart_screen.dart';
|
||||
// import 'package:rijig_mobile/features/cart/presentation/screens/cart_screen.dart';
|
||||
import 'package:rijig_mobile/features/cart/presentation/screens/cart_test_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';
|
||||
|
@ -82,7 +83,8 @@ class _NavigationPageState extends State<NavigationPage>
|
|||
HomeScreen(),
|
||||
ActivityScreen(),
|
||||
Text(""),
|
||||
CartScreen(),
|
||||
// CartScreen(),
|
||||
OrderSummaryScreen(),
|
||||
ProfilScreen(),
|
||||
],
|
||||
),
|
||||
|
@ -163,7 +165,8 @@ class _NavigationPageState extends State<NavigationPage>
|
|||
disabledElevation: 0,
|
||||
autofocus: false,
|
||||
focusElevation: 0,
|
||||
onPressed: () => router.push("/requestpickup"),
|
||||
onPressed: () => router.push("/trashview"),
|
||||
// onPressed: () => router.push("/requestpickup"),
|
||||
shape: const CircleBorder(),
|
||||
elevation: 0,
|
||||
child: Column(
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
class CartItem {
|
||||
final String id;
|
||||
final String trashId;
|
||||
final String trashName;
|
||||
final String trashIcon;
|
||||
final double trashPrice;
|
||||
final int amount;
|
||||
final double subtotalEstimatedPrice;
|
||||
|
||||
CartItem({
|
||||
required this.id,
|
||||
required this.trashId,
|
||||
required this.trashName,
|
||||
required this.trashIcon,
|
||||
required this.trashPrice,
|
||||
required this.amount,
|
||||
required this.subtotalEstimatedPrice,
|
||||
});
|
||||
|
||||
factory CartItem.fromJson(Map<String, dynamic> json) {
|
||||
return CartItem(
|
||||
id: json['id'] ?? '',
|
||||
trashId: json['trash_id'] ?? '',
|
||||
trashName: json['trash_name'] ?? '',
|
||||
trashIcon: json['trash_icon'] ?? '',
|
||||
trashPrice: (json['trash_price'] ?? 0).toDouble(),
|
||||
amount: json['amount'] ?? 0,
|
||||
subtotalEstimatedPrice:
|
||||
(json['subtotal_estimated_price'] ?? 0).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'trash_id': trashId,
|
||||
'trash_name': trashName,
|
||||
'trash_icon': trashIcon,
|
||||
'trash_price': trashPrice,
|
||||
'amount': amount,
|
||||
'subtotal_estimated_price': subtotalEstimatedPrice,
|
||||
};
|
||||
}
|
||||
|
||||
CartItem copyWith({
|
||||
String? id,
|
||||
String? trashId,
|
||||
String? trashName,
|
||||
String? trashIcon,
|
||||
double? trashPrice,
|
||||
int? amount,
|
||||
double? subtotalEstimatedPrice,
|
||||
}) {
|
||||
return CartItem(
|
||||
id: id ?? this.id,
|
||||
trashId: trashId ?? this.trashId,
|
||||
trashName: trashName ?? this.trashName,
|
||||
trashIcon: trashIcon ?? this.trashIcon,
|
||||
trashPrice: trashPrice ?? this.trashPrice,
|
||||
amount: amount ?? this.amount,
|
||||
subtotalEstimatedPrice:
|
||||
subtotalEstimatedPrice ?? this.subtotalEstimatedPrice,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CartItem(id: $id, trashId: $trashId, trashName: $trashName, amount: $amount, subtotalEstimatedPrice: $subtotalEstimatedPrice)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is CartItem && other.trashId == trashId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => trashId.hashCode;
|
||||
}
|
||||
|
||||
class Cart {
|
||||
final String id;
|
||||
final String userId;
|
||||
final int totalAmount;
|
||||
final double estimatedTotalPrice;
|
||||
final List<CartItem> cartItems;
|
||||
|
||||
Cart({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.totalAmount,
|
||||
required this.estimatedTotalPrice,
|
||||
required this.cartItems,
|
||||
});
|
||||
|
||||
factory Cart.fromJson(Map<String, dynamic> json) {
|
||||
final data = json['data'] ?? {};
|
||||
return Cart(
|
||||
id: data['id'] ?? '',
|
||||
userId: data['user_id'] ?? '',
|
||||
totalAmount: data['total_amount'] ?? 0,
|
||||
estimatedTotalPrice: (data['estimated_total_price'] ?? 0).toDouble(),
|
||||
cartItems:
|
||||
(data['cart_items'] as List<dynamic>?)
|
||||
?.map((item) => CartItem.fromJson(item))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'data': {
|
||||
'id': id,
|
||||
'user_id': userId,
|
||||
'total_amount': totalAmount,
|
||||
'estimated_total_price': estimatedTotalPrice,
|
||||
'cart_items': cartItems.map((item) => item.toJson()).toList(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Cart copyWith({
|
||||
String? id,
|
||||
String? userId,
|
||||
int? totalAmount,
|
||||
double? estimatedTotalPrice,
|
||||
List<CartItem>? cartItems,
|
||||
}) {
|
||||
return Cart(
|
||||
id: id ?? this.id,
|
||||
userId: userId ?? this.userId,
|
||||
totalAmount: totalAmount ?? this.totalAmount,
|
||||
estimatedTotalPrice: estimatedTotalPrice ?? this.estimatedTotalPrice,
|
||||
cartItems: cartItems ?? this.cartItems,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isEmpty => cartItems.isEmpty;
|
||||
bool get isNotEmpty => cartItems.isNotEmpty;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Cart(id: $id, userId: $userId, totalAmount: $totalAmount, estimatedTotalPrice: $estimatedTotalPrice, cartItems: ${cartItems.length})';
|
||||
}
|
||||
}
|
||||
|
||||
class AddOrUpdateCartRequest {
|
||||
final String trashId;
|
||||
final int amount;
|
||||
|
||||
AddOrUpdateCartRequest({required this.trashId, required this.amount});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'trash_id': trashId, 'amount': amount};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AddOrUpdateCartRequest(trashId: $trashId, amount: $amount)';
|
||||
}
|
||||
}
|
||||
|
||||
class CartApiResponse<T> {
|
||||
final int status;
|
||||
final String message;
|
||||
final T? data;
|
||||
|
||||
CartApiResponse({required this.status, required this.message, this.data});
|
||||
|
||||
factory CartApiResponse.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
T Function(Map<String, dynamic>)? fromJsonT,
|
||||
) {
|
||||
return CartApiResponse<T>(
|
||||
status: json['meta']?['status'] ?? 0,
|
||||
message: json['meta']?['message'] ?? '',
|
||||
data: json['data'] != null && fromJsonT != null ? fromJsonT(json) : null,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isSuccess => status >= 200 && status < 300;
|
||||
}
|
|
@ -1,466 +0,0 @@
|
|||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
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/router.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!",
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
onTap: () {
|
||||
router.push("/pickupmethod", extra: '');
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,911 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:custom_refresh_indicator/custom_refresh_indicator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:rijig_mobile/features/cart/presentation/viewmodel/trashcart_vmod.dart';
|
||||
import 'package:rijig_mobile/features/cart/model/trashcart_model.dart';
|
||||
|
||||
class OrderSummaryScreen extends StatefulWidget {
|
||||
const OrderSummaryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<OrderSummaryScreen> createState() => _OrderSummaryScreenState();
|
||||
}
|
||||
|
||||
class _OrderSummaryScreenState extends State<OrderSummaryScreen>
|
||||
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
|
||||
late CartViewModel _cartViewModel;
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_initializeCart();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed && mounted) {
|
||||
_refreshCartData();
|
||||
}
|
||||
}
|
||||
|
||||
void _refreshCartData() {
|
||||
if (mounted) {
|
||||
final cartViewModel = context.read<CartViewModel>();
|
||||
cartViewModel.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void _initializeCart() async {
|
||||
if (_isInitialized) return;
|
||||
_cartViewModel = context.read<CartViewModel>();
|
||||
|
||||
await _cartViewModel.loadCartItems(showLoading: false);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isInitialized = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
if (_isInitialized) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_refreshCartData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _removeItem(String trashId, String itemName) async {
|
||||
final success = await _cartViewModel.deleteItem(trashId);
|
||||
if (success) {
|
||||
_showSnackbar('$itemName berhasil dihapus');
|
||||
} else {
|
||||
_showSnackbar(_cartViewModel.errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearAllItems() async {
|
||||
if (_cartViewModel.isEmpty) return;
|
||||
|
||||
final confirmed = await _showConfirmationDialog(
|
||||
title: 'Hapus Semua Item',
|
||||
content: 'Apakah Anda yakin ingin menghapus semua item dari keranjang?',
|
||||
confirmText: 'Hapus Semua',
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
final success = await _cartViewModel.clearCart();
|
||||
if (success) {
|
||||
_showSnackbar('Semua item berhasil dihapus');
|
||||
} else {
|
||||
_showSnackbar(_cartViewModel.errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _incrementQuantity(String trashId) async {
|
||||
await _cartViewModel.incrementItemAmount(trashId);
|
||||
if (_cartViewModel.state == CartState.error) {
|
||||
_showSnackbar(_cartViewModel.errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _decrementQuantity(String trashId) async {
|
||||
await _cartViewModel.decrementItemAmount(trashId);
|
||||
if (_cartViewModel.state == CartState.error) {
|
||||
_showSnackbar(_cartViewModel.errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showQuantityDialog(CartItem item) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: item.amount.toString(),
|
||||
);
|
||||
|
||||
final result = await showDialog<int>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Input Jumlah ${item.trashName}'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Jumlah',
|
||||
border: OutlineInputBorder(),
|
||||
suffixText: 'kg',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final newAmount = int.tryParse(controller.text);
|
||||
if (newAmount != null && newAmount > 0) {
|
||||
Navigator.of(context).pop(newAmount);
|
||||
} else {
|
||||
_showSnackbar('Masukkan angka yang valid (lebih dari 0)');
|
||||
}
|
||||
},
|
||||
child: Text('Simpan'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
final success = await _cartViewModel.addOrUpdateItem(
|
||||
item.trashId,
|
||||
result,
|
||||
);
|
||||
if (!success) {
|
||||
_showSnackbar(_cartViewModel.errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool?> _showConfirmationDialog({
|
||||
required String title,
|
||||
required String content,
|
||||
required String confirmText,
|
||||
}) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Text(confirmText),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showSnackbar(String message) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message), duration: Duration(seconds: 2)),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getTrashTypeConfig(String trashName) {
|
||||
final name = trashName.toLowerCase();
|
||||
if (name.contains('plastik')) {
|
||||
return {
|
||||
'icon': Icons.local_drink,
|
||||
'backgroundColor': Colors.blue.shade100,
|
||||
'iconColor': Colors.blue,
|
||||
};
|
||||
} else if (name.contains('kertas')) {
|
||||
return {
|
||||
'icon': Icons.description,
|
||||
'backgroundColor': Colors.orange.shade100,
|
||||
'iconColor': Colors.orange,
|
||||
};
|
||||
} else if (name.contains('logam') || name.contains('metal')) {
|
||||
return {
|
||||
'icon': Icons.build,
|
||||
'backgroundColor': Colors.grey.shade100,
|
||||
'iconColor': Colors.grey.shade700,
|
||||
};
|
||||
} else if (name.contains('kaca')) {
|
||||
return {
|
||||
'icon': Icons.wine_bar,
|
||||
'backgroundColor': Colors.green.shade100,
|
||||
'iconColor': Colors.green,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'icon': Icons.delete_outline,
|
||||
'backgroundColor': Colors.grey.shade100,
|
||||
'iconColor': Colors.grey.shade600,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
double get totalWeight => _cartViewModel.totalItems.toDouble();
|
||||
int get estimatedEarnings => _cartViewModel.totalPrice.round();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: Text(
|
||||
'Detail Pesanan',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
Consumer<CartViewModel>(
|
||||
builder: (context, cartVM, child) {
|
||||
if (cartVM.isNotEmpty) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh, color: Colors.black),
|
||||
onPressed: () => _refreshCartData(),
|
||||
tooltip: 'Refresh Data',
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
icon: Icon(Icons.more_vert, color: Colors.black),
|
||||
onSelected: (value) {
|
||||
if (value == 'clear_all') {
|
||||
_clearAllItems();
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'clear_all',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.clear_all,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
Gap(8),
|
||||
Text(
|
||||
'Hapus Semua',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Consumer<CartViewModel>(
|
||||
builder: (context, cartVM, child) {
|
||||
if (cartVM.state == CartState.loading) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
Gap(16),
|
||||
Text('Memuat keranjang...'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (cartVM.state == CartState.error) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 48, color: Colors.red),
|
||||
Gap(16),
|
||||
Text(
|
||||
'Terjadi kesalahan',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Gap(8),
|
||||
Text(
|
||||
cartVM.errorMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
Gap(24),
|
||||
ElevatedButton(
|
||||
onPressed: () => _refreshCartData(),
|
||||
child: Text('Coba Lagi'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (cartVM.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return CustomMaterialIndicator(
|
||||
onRefresh: () async {
|
||||
await _cartViewModel.loadCartItems(showLoading: false);
|
||||
},
|
||||
backgroundColor: Colors.white,
|
||||
indicatorBuilder: (context, controller) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.blue,
|
||||
value: controller.state.isLoading
|
||||
? null
|
||||
: math.min(controller.value, 1.0),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildItemsSection(cartVM),
|
||||
Gap(20),
|
||||
_buildEarningsSection(cartVM),
|
||||
Gap(20),
|
||||
_buildBottomButton(cartVM),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.shopping_cart_outlined,
|
||||
size: 48,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
Gap(16),
|
||||
Text(
|
||||
'Keranjang kosong',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
Gap(8),
|
||||
Text(
|
||||
'Tambahkan item sampah untuk melanjutkan',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade500),
|
||||
),
|
||||
Gap(24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: Icon(Icons.add),
|
||||
label: Text('Tambah Item'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemsSection(CartViewModel cartVM) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSectionHeader(),
|
||||
Gap(16),
|
||||
...cartVM.cartItems.map((item) => _buildItemCard(item)),
|
||||
Gap(16),
|
||||
_buildTotalWeight(cartVM),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.delete_outline, color: Colors.orange, size: 20),
|
||||
),
|
||||
Gap(12),
|
||||
Text(
|
||||
'Jenis Sampah',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.add, color: Colors.blue, size: 16),
|
||||
Gap(4),
|
||||
Text(
|
||||
'Tambah',
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemCard(CartItem item) {
|
||||
final config = _getTrashTypeConfig(item.trashName);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
child: Consumer<CartViewModel>(
|
||||
builder: (context, cartVM, child) {
|
||||
return Slidable(
|
||||
key: ValueKey('${item.trashId}_${item.id}'),
|
||||
endActionPane: ActionPane(
|
||||
motion: ScrollMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (context) => _removeItem(item.trashId, item.trashName),
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
icon: Icons.delete,
|
||||
label: 'Hapus',
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildItemIcon(config),
|
||||
Gap(12),
|
||||
_buildItemInfo(item),
|
||||
_buildQuantityControls(item, cartVM),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemIcon(Map<String, dynamic> config) {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: config['backgroundColor'],
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Icon(config['icon'], color: config['iconColor'], size: 20),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemInfo(CartItem item) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.trashName,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
Gap(4),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Rp ${_formatCurrency(item.trashPrice.round())}/kg',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantityControls(CartItem item, CartViewModel cartVM) {
|
||||
return Row(
|
||||
children: [
|
||||
_buildQuantityButton(
|
||||
icon: Icons.remove,
|
||||
onTap: cartVM.isOperationInProgress
|
||||
? null
|
||||
: () => _decrementQuantity(item.trashId),
|
||||
backgroundColor: Colors.white,
|
||||
iconColor: Colors.grey.shade600,
|
||||
),
|
||||
Gap(8),
|
||||
GestureDetector(
|
||||
onTap: cartVM.isOperationInProgress
|
||||
? null
|
||||
: () => _showQuantityDialog(item),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Text(
|
||||
'${item.amount} kg',
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
Gap(8),
|
||||
_buildQuantityButton(
|
||||
icon: Icons.add,
|
||||
onTap: cartVM.isOperationInProgress
|
||||
? null
|
||||
: () => _incrementQuantity(item.trashId),
|
||||
backgroundColor: Colors.blue,
|
||||
iconColor: Colors.white,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantityButton({
|
||||
required IconData icon,
|
||||
required VoidCallback? onTap,
|
||||
required Color backgroundColor,
|
||||
required Color iconColor,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: onTap == null ? Colors.grey.shade300 : backgroundColor,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: backgroundColor == Colors.white
|
||||
? Border.all(color: Colors.grey.shade300)
|
||||
: null,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: onTap == null ? Colors.grey.shade500 : iconColor,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTotalWeight(CartViewModel cartVM) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(Icons.scale, color: Colors.grey.shade700, size: 16),
|
||||
),
|
||||
Gap(12),
|
||||
Text(
|
||||
'Berat total',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
'${cartVM.totalItems} kg',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEarningsSection(CartViewModel cartVM) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.folder, color: Colors.orange, size: 20),
|
||||
),
|
||||
Gap(12),
|
||||
Text(
|
||||
'Rincian Perhitungan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Gap(16),
|
||||
// Detail per item
|
||||
...cartVM.cartItems.map((item) => _buildItemCalculation(item)),
|
||||
Gap(12),
|
||||
// Divider
|
||||
Divider(color: Colors.grey.shade300),
|
||||
Gap(8),
|
||||
// Total
|
||||
_buildTotalCalculation(cartVM),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemCalculation(CartItem item) {
|
||||
final subtotal = (item.amount * item.trashPrice).round();
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.receipt_outlined, color: Colors.blue, size: 12),
|
||||
),
|
||||
Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.trashName,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
Gap(2),
|
||||
Text(
|
||||
'${item.amount} kg × Rp ${_formatCurrency(item.trashPrice.round())}/kg',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Rp ${_formatCurrency(subtotal)}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTotalCalculation(CartViewModel cartVM) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.account_balance_wallet, color: Colors.green, size: 16),
|
||||
),
|
||||
Gap(12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Total Estimasi Pendapatan',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Rp ${_formatCurrency(estimatedEarnings)}',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomButton(CartViewModel cartVM) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Consumer<CartViewModel>(
|
||||
builder: (context, cartVM, child) {
|
||||
final isLoading = cartVM.isOperationInProgress;
|
||||
final hasItems = cartVM.isNotEmpty;
|
||||
|
||||
return ElevatedButton(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: hasItems
|
||||
? () {
|
||||
_showSnackbar('Lanjut ke proses selanjutnya');
|
||||
}
|
||||
: () => Navigator.of(context).pop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: hasItems ? Colors.blue : Colors.grey.shade400,
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
hasItems ? 'Lanjut' : 'Tambah Item',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatCurrency(int amount) {
|
||||
return amount.toString().replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]}.',
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
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,278 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:rijig_mobile/features/cart/model/trashcart_model.dart';
|
||||
import 'package:rijig_mobile/features/cart/service/trashcart_service.dart';
|
||||
|
||||
enum CartState { initial, loading, loaded, error, empty, updating }
|
||||
|
||||
class CartViewModel extends ChangeNotifier {
|
||||
final CartService _cartService;
|
||||
|
||||
CartViewModel({CartService? cartService})
|
||||
: _cartService = cartService ?? CartServiceProvider.instance;
|
||||
|
||||
CartState _state = CartState.initial;
|
||||
Cart? _cart;
|
||||
String _errorMessage = '';
|
||||
bool _isOperationInProgress = false;
|
||||
|
||||
CartState get state => _state;
|
||||
Cart? get cart => _cart;
|
||||
String get errorMessage => _errorMessage;
|
||||
bool get isOperationInProgress => _isOperationInProgress;
|
||||
|
||||
List<CartItem> get cartItems => _cart?.cartItems ?? [];
|
||||
int get totalItems => _cart?.totalAmount ?? 0;
|
||||
double get totalPrice => _cart?.estimatedTotalPrice ?? 0.0;
|
||||
bool get isEmpty => cartItems.isEmpty;
|
||||
bool get isNotEmpty => cartItems.isNotEmpty;
|
||||
|
||||
void _setState(CartState newState) {
|
||||
if (_state != newState) {
|
||||
_state = newState;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _setError(String message) {
|
||||
_errorMessage = message;
|
||||
_setState(CartState.error);
|
||||
}
|
||||
|
||||
void _setOperationInProgress(bool inProgress) {
|
||||
_isOperationInProgress = inProgress;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadCartItems({bool showLoading = true}) async {
|
||||
if (showLoading) {
|
||||
_setState(CartState.loading);
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _cartService.getCartItems();
|
||||
|
||||
if (response.isSuccess && response.data != null) {
|
||||
_cart = response.data as Cart;
|
||||
_setState(_cart!.isEmpty ? CartState.empty : CartState.loaded);
|
||||
} else if (response.isUnauthorized) {
|
||||
_setError('Sesi Anda telah berakhir, silakan login kembali');
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('CartViewModel - loadCartItems error: $e');
|
||||
_setError('Terjadi kesalahan tidak terduga');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addOrUpdateItem(
|
||||
String trashId,
|
||||
int amount, {
|
||||
bool showUpdating = true,
|
||||
}) async {
|
||||
if (showUpdating) {
|
||||
_setOperationInProgress(true);
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _cartService.addOrUpdateItem(trashId, amount);
|
||||
|
||||
if (response.isSuccess) {
|
||||
await loadCartItems(showLoading: false);
|
||||
return true;
|
||||
} else {
|
||||
if (response.isUnauthorized) {
|
||||
_setError('Sesi Anda telah berakhir, silakan login kembali');
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('CartViewModel - addOrUpdateItem error: $e');
|
||||
_setError('Terjadi kesalahan tidak terduga');
|
||||
return false;
|
||||
} finally {
|
||||
if (showUpdating) {
|
||||
_setOperationInProgress(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteItem(String trashId, {bool showUpdating = true}) async {
|
||||
if (showUpdating) {
|
||||
_setOperationInProgress(true);
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _cartService.deleteItem(trashId);
|
||||
|
||||
if (response.isSuccess) {
|
||||
await loadCartItems(showLoading: false);
|
||||
return true;
|
||||
} else {
|
||||
if (response.isUnauthorized) {
|
||||
_setError('Sesi Anda telah berakhir, silakan login kembali');
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('CartViewModel - deleteItem error: $e');
|
||||
_setError('Terjadi kesalahan tidak terduga');
|
||||
return false;
|
||||
} finally {
|
||||
if (showUpdating) {
|
||||
_setOperationInProgress(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> clearCart({bool showUpdating = true}) async {
|
||||
if (showUpdating) {
|
||||
_setOperationInProgress(true);
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _cartService.clearCart();
|
||||
|
||||
if (response.isSuccess) {
|
||||
await loadCartItems(showLoading: false);
|
||||
return true;
|
||||
} else {
|
||||
if (response.isUnauthorized) {
|
||||
_setError('Sesi Anda telah berakhir, silakan login kembali');
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('CartViewModel - clearCart error: $e');
|
||||
_setError('Terjadi kesalahan tidak terduga');
|
||||
return false;
|
||||
} finally {
|
||||
if (showUpdating) {
|
||||
_setOperationInProgress(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> incrementItemAmount(String trashId) async {
|
||||
_setOperationInProgress(true);
|
||||
|
||||
try {
|
||||
final response = await _cartService.incrementItemAmount(trashId);
|
||||
|
||||
if (response.isSuccess) {
|
||||
await loadCartItems(showLoading: false);
|
||||
return true;
|
||||
} else {
|
||||
if (response.isUnauthorized) {
|
||||
_setError('Sesi Anda telah berakhir, silakan login kembali');
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('CartViewModel - incrementItemAmount error: $e');
|
||||
_setError('Terjadi kesalahan tidak terduga');
|
||||
return false;
|
||||
} finally {
|
||||
_setOperationInProgress(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> decrementItemAmount(String trashId) async {
|
||||
_setOperationInProgress(true);
|
||||
|
||||
try {
|
||||
final response = await _cartService.decrementItemAmount(trashId);
|
||||
|
||||
if (response.isSuccess) {
|
||||
await loadCartItems(showLoading: false);
|
||||
return true;
|
||||
} else {
|
||||
if (response.isUnauthorized) {
|
||||
_setError('Sesi Anda telah berakhir, silakan login kembali');
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('CartViewModel - decrementItemAmount error: $e');
|
||||
_setError('Terjadi kesalahan tidak terduga');
|
||||
return false;
|
||||
} finally {
|
||||
_setOperationInProgress(false);
|
||||
}
|
||||
}
|
||||
|
||||
CartItem? getItemByTrashId(String trashId) {
|
||||
try {
|
||||
return cartItems.firstWhere((item) => item.trashId == trashId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool isItemInCart(String trashId) {
|
||||
return getItemByTrashId(trashId) != null;
|
||||
}
|
||||
|
||||
int getItemAmount(String trashId) {
|
||||
final item = getItemByTrashId(trashId);
|
||||
return item?.amount ?? 0;
|
||||
}
|
||||
|
||||
double getItemSubtotal(String trashId) {
|
||||
final item = getItemByTrashId(trashId);
|
||||
return item?.subtotalEstimatedPrice ?? 0.0;
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
if (_state == CartState.error) {
|
||||
_errorMessage = '';
|
||||
_setState(
|
||||
_cart == null || _cart!.isEmpty ? CartState.empty : CartState.loaded,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
await loadCartItems(showLoading: true);
|
||||
}
|
||||
|
||||
// Future<void> refresh() async {
|
||||
// await loadCartItems(showLoading: false);
|
||||
// notifyListeners();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void dispose() {
|
||||
// super.dispose();
|
||||
// }
|
||||
}
|
||||
|
||||
extension CartViewModelExtension on CartViewModel {
|
||||
String get formattedTotalPrice {
|
||||
return 'Rp ${totalPrice.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
|
||||
}
|
||||
|
||||
String getFormattedItemPrice(String trashId) {
|
||||
final item = getItemByTrashId(trashId);
|
||||
if (item == null) return 'Rp 0';
|
||||
|
||||
return 'Rp ${item.trashPrice.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
|
||||
}
|
||||
|
||||
String getFormattedItemSubtotal(String trashId) {
|
||||
final item = getItemByTrashId(trashId);
|
||||
if (item == null) return 'Rp 0';
|
||||
|
||||
return 'Rp ${item.subtotalEstimatedPrice.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
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,189 @@
|
|||
import 'package:rijig_mobile/core/api/api_exception.dart';
|
||||
import 'package:rijig_mobile/core/api/api_services.dart';
|
||||
import 'package:rijig_mobile/features/cart/model/trashcart_model.dart';
|
||||
|
||||
abstract class CartRepository {
|
||||
Future<CartApiResponse<dynamic>> addOrUpdateCartItem(
|
||||
AddOrUpdateCartRequest request,
|
||||
);
|
||||
Future<Cart> getCartItems();
|
||||
Future<CartApiResponse<dynamic>> deleteCartItem(String trashId);
|
||||
Future<CartApiResponse<dynamic>> clearCart();
|
||||
}
|
||||
|
||||
class CartRepositoryImpl implements CartRepository {
|
||||
final Https _https = Https();
|
||||
|
||||
static const String _cartEndpoint = '/cart';
|
||||
static const String _cartItemEndpoint = '/cart/item';
|
||||
static const String _cartClearEndpoint = '/cart/clear';
|
||||
|
||||
@override
|
||||
Future<CartApiResponse<dynamic>> addOrUpdateCartItem(
|
||||
AddOrUpdateCartRequest request,
|
||||
) async {
|
||||
try {
|
||||
final response = await _https.post(
|
||||
_cartItemEndpoint,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
return CartApiResponse<dynamic>.fromJson(response, null);
|
||||
} on ApiException catch (e) {
|
||||
throw ApiException(e.message, e.statusCode);
|
||||
} catch (e) {
|
||||
throw ApiException(
|
||||
'Unexpected error occurred while adding/updating cart item',
|
||||
500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Cart> getCartItems() async {
|
||||
try {
|
||||
final response = await _https.get(_cartEndpoint);
|
||||
|
||||
final cartResponse = CartApiResponse<Cart>.fromJson(
|
||||
response,
|
||||
(json) => Cart.fromJson(json),
|
||||
);
|
||||
|
||||
if (!cartResponse.isSuccess) {
|
||||
throw ApiException(cartResponse.message, cartResponse.status);
|
||||
}
|
||||
|
||||
return cartResponse.data ??
|
||||
Cart(
|
||||
id: '',
|
||||
userId: '',
|
||||
totalAmount: 0,
|
||||
estimatedTotalPrice: 0.0,
|
||||
cartItems: [],
|
||||
);
|
||||
} on ApiException catch (e) {
|
||||
throw ApiException(e.message, e.statusCode);
|
||||
} catch (e) {
|
||||
throw ApiException(
|
||||
'Unexpected error occurred while fetching cart items',
|
||||
500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CartApiResponse<dynamic>> deleteCartItem(String trashId) async {
|
||||
try {
|
||||
final response = await _https.delete('$_cartItemEndpoint/$trashId');
|
||||
|
||||
return CartApiResponse<dynamic>.fromJson(response, null);
|
||||
} on ApiException catch (e) {
|
||||
throw ApiException(e.message, e.statusCode);
|
||||
} catch (e) {
|
||||
throw ApiException(
|
||||
'Unexpected error occurred while deleting cart item',
|
||||
500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CartApiResponse<dynamic>> clearCart() async {
|
||||
try {
|
||||
final response = await _https.delete(_cartClearEndpoint);
|
||||
|
||||
return CartApiResponse<dynamic>.fromJson(response, null);
|
||||
} on ApiException catch (e) {
|
||||
throw ApiException(e.message, e.statusCode);
|
||||
} catch (e) {
|
||||
throw ApiException('Unexpected error occurred while clearing cart', 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MockCartRepository implements CartRepository {
|
||||
static final List<CartItem> _mockCartItems = [];
|
||||
|
||||
@override
|
||||
Future<CartApiResponse<dynamic>> addOrUpdateCartItem(
|
||||
AddOrUpdateCartRequest request,
|
||||
) async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final existingIndex = _mockCartItems.indexWhere(
|
||||
(item) => item.trashId == request.trashId,
|
||||
);
|
||||
|
||||
if (existingIndex != -1) {
|
||||
final existingItem = _mockCartItems[existingIndex];
|
||||
_mockCartItems[existingIndex] = existingItem.copyWith(
|
||||
amount: request.amount,
|
||||
subtotalEstimatedPrice: existingItem.trashPrice * request.amount,
|
||||
);
|
||||
} else {
|
||||
_mockCartItems.add(
|
||||
CartItem(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
trashId: request.trashId,
|
||||
trashName: 'Mock Trash Item',
|
||||
trashIcon: '/mock/icon.png',
|
||||
trashPrice: 1200.0,
|
||||
amount: request.amount,
|
||||
subtotalEstimatedPrice: 1200.0 * request.amount,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return CartApiResponse<dynamic>(
|
||||
status: 200,
|
||||
message: 'Berhasil menambah/mengubah item keranjang',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Cart> getCartItems() async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
final totalAmount = _mockCartItems.fold<int>(
|
||||
0,
|
||||
(sum, item) => sum + item.amount,
|
||||
);
|
||||
|
||||
final estimatedTotalPrice = _mockCartItems.fold<double>(
|
||||
0.0,
|
||||
(sum, item) => sum + item.subtotalEstimatedPrice,
|
||||
);
|
||||
|
||||
return Cart(
|
||||
id: 'mock_cart_id',
|
||||
userId: 'mock_user_id',
|
||||
totalAmount: totalAmount,
|
||||
estimatedTotalPrice: estimatedTotalPrice,
|
||||
cartItems: List.from(_mockCartItems),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CartApiResponse<dynamic>> deleteCartItem(String trashId) async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
_mockCartItems.removeWhere((item) => item.trashId == trashId);
|
||||
|
||||
return CartApiResponse<dynamic>(
|
||||
status: 200,
|
||||
message: 'Berhasil menghapus item dari keranjang',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CartApiResponse<dynamic>> clearCart() async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
_mockCartItems.clear();
|
||||
|
||||
return CartApiResponse<dynamic>(
|
||||
status: 200,
|
||||
message: 'Berhasil mengosongkan keranjang',
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
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");
|
||||
debugPrint(response);
|
||||
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");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:rijig_mobile/core/api/api_exception.dart';
|
||||
import 'package:rijig_mobile/features/cart/model/trashcart_model.dart';
|
||||
import 'package:rijig_mobile/features/cart/repositories/trashcart_repo.dart';
|
||||
|
||||
enum CartOperationResult { success, failed, networkError, unauthorized }
|
||||
|
||||
class CartOperationResponse {
|
||||
final CartOperationResult result;
|
||||
final String message;
|
||||
final dynamic data;
|
||||
|
||||
CartOperationResponse({
|
||||
required this.result,
|
||||
required this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
bool get isSuccess => result == CartOperationResult.success;
|
||||
bool get isNetworkError => result == CartOperationResult.networkError;
|
||||
bool get isUnauthorized => result == CartOperationResult.unauthorized;
|
||||
}
|
||||
|
||||
abstract class CartService {
|
||||
Future<CartOperationResponse> addOrUpdateItem(String trashId, int amount);
|
||||
Future<CartOperationResponse> getCartItems();
|
||||
Future<CartOperationResponse> deleteItem(String trashId);
|
||||
Future<CartOperationResponse> clearCart();
|
||||
Future<CartOperationResponse> incrementItemAmount(String trashId);
|
||||
Future<CartOperationResponse> decrementItemAmount(String trashId);
|
||||
}
|
||||
|
||||
class CartServiceImpl implements CartService {
|
||||
final CartRepository _repository;
|
||||
|
||||
CartServiceImpl({CartRepository? repository})
|
||||
: _repository = repository ?? CartRepositoryImpl();
|
||||
|
||||
@override
|
||||
Future<CartOperationResponse> addOrUpdateItem(
|
||||
String trashId,
|
||||
int amount,
|
||||
) async {
|
||||
try {
|
||||
if (amount <= 0) {
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.failed,
|
||||
message: 'Jumlah item harus lebih dari 0',
|
||||
);
|
||||
}
|
||||
|
||||
final request = AddOrUpdateCartRequest(trashId: trashId, amount: amount);
|
||||
|
||||
final response = await _repository.addOrUpdateCartItem(request);
|
||||
|
||||
if (response.isSuccess) {
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.success,
|
||||
message: response.message,
|
||||
data: response.data,
|
||||
);
|
||||
} else {
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.failed,
|
||||
message: response.message,
|
||||
);
|
||||
}
|
||||
} on ApiException catch (e) {
|
||||
debugPrint('CartService - addOrUpdateItem error: ${e.message}');
|
||||
|
||||
if (e.statusCode == 401 || e.statusCode == 403) {
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.unauthorized,
|
||||
message: 'Sesi Anda telah berakhir, silakan login kembali',
|
||||
);
|
||||
}
|
||||
|
||||
return CartOperationResponse(
|
||||
result:
|
||||
e.statusCode >= 500
|
||||
? CartOperationResult.networkError
|
||||
: CartOperationResult.failed,
|
||||
message: e.message,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('CartService - addOrUpdateItem unexpected error: $e');
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.networkError,
|
||||
message: 'Terjadi kesalahan jaringan, silakan coba lagi',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CartOperationResponse> getCartItems() async {
|
||||
try {
|
||||
final cart = await _repository.getCartItems();
|
||||
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.success,
|
||||
message: 'Berhasil mengambil data keranjang',
|
||||
data: cart,
|
||||
);
|
||||
} on ApiException catch (e) {
|
||||
debugPrint('CartService - getCartItems error: ${e.message}');
|
||||
|
||||
if (e.statusCode == 401 || e.statusCode == 403) {
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.unauthorized,
|
||||
message: 'Sesi Anda telah berakhir, silakan login kembali',
|
||||
);
|
||||
}
|
||||
|
||||
return CartOperationResponse(
|
||||
result:
|
||||
e.statusCode >= 500
|
||||
? CartOperationResult.networkError
|
||||
: CartOperationResult.failed,
|
||||
message: e.message,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('CartService - getCartItems unexpected error: $e');
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.networkError,
|
||||
message: 'Terjadi kesalahan jaringan, silakan coba lagi',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CartOperationResponse> deleteItem(String trashId) async {
|
||||
try {
|
||||
final response = await _repository.deleteCartItem(trashId);
|
||||
|
||||
if (response.isSuccess) {
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.success,
|
||||
message: response.message,
|
||||
);
|
||||
} else {
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.failed,
|
||||
message: response.message,
|
||||
);
|
||||
}
|
||||
} on ApiException catch (e) {
|
||||
debugPrint('CartService - deleteItem error: ${e.message}');
|
||||
|
||||
if (e.statusCode == 401 || e.statusCode == 403) {
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.unauthorized,
|
||||
message: 'Sesi Anda telah berakhir, silakan login kembali',
|
||||
);
|
||||
}
|
||||
|
||||
return CartOperationResponse(
|
||||
result:
|
||||
e.statusCode >= 500
|
||||
? CartOperationResult.networkError
|
||||
: CartOperationResult.failed,
|
||||
message: e.message,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('CartService - deleteItem unexpected error: $e');
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.networkError,
|
||||
message: 'Terjadi kesalahan jaringan, silakan coba lagi',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CartOperationResponse> clearCart() async {
|
||||
try {
|
||||
final response = await _repository.clearCart();
|
||||
|
||||
if (response.isSuccess) {
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.success,
|
||||
message: response.message,
|
||||
);
|
||||
} else {
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.failed,
|
||||
message: response.message,
|
||||
);
|
||||
}
|
||||
} on ApiException catch (e) {
|
||||
debugPrint('CartService - clearCart error: ${e.message}');
|
||||
|
||||
if (e.statusCode == 401 || e.statusCode == 403) {
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.unauthorized,
|
||||
message: 'Sesi Anda telah berakhir, silakan login kembali',
|
||||
);
|
||||
}
|
||||
|
||||
return CartOperationResponse(
|
||||
result:
|
||||
e.statusCode >= 500
|
||||
? CartOperationResult.networkError
|
||||
: CartOperationResult.failed,
|
||||
message: e.message,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('CartService - clearCart unexpected error: $e');
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.networkError,
|
||||
message: 'Terjadi kesalahan jaringan, silakan coba lagi',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CartOperationResponse> incrementItemAmount(String trashId) async {
|
||||
try {
|
||||
final cartResponse = await getCartItems();
|
||||
if (!cartResponse.isSuccess) {
|
||||
return cartResponse;
|
||||
}
|
||||
|
||||
final cart = cartResponse.data as Cart;
|
||||
final item = cart.cartItems.firstWhere(
|
||||
(item) => item.trashId == trashId,
|
||||
orElse: () => throw Exception('Item tidak ditemukan di keranjang'),
|
||||
);
|
||||
|
||||
return await addOrUpdateItem(trashId, item.amount + 1);
|
||||
} catch (e) {
|
||||
debugPrint('CartService - incrementItemAmount error: $e');
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.failed,
|
||||
message: 'Gagal menambah jumlah item',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CartOperationResponse> decrementItemAmount(String trashId) async {
|
||||
try {
|
||||
final cartResponse = await getCartItems();
|
||||
if (!cartResponse.isSuccess) {
|
||||
return cartResponse;
|
||||
}
|
||||
|
||||
final cart = cartResponse.data as Cart;
|
||||
final item = cart.cartItems.firstWhere(
|
||||
(item) => item.trashId == trashId,
|
||||
orElse: () => throw Exception('Item tidak ditemukan di keranjang'),
|
||||
);
|
||||
|
||||
if (item.amount <= 1) {
|
||||
return await deleteItem(trashId);
|
||||
}
|
||||
|
||||
return await addOrUpdateItem(trashId, item.amount - 1);
|
||||
} catch (e) {
|
||||
debugPrint('CartService - decrementItemAmount error: $e');
|
||||
return CartOperationResponse(
|
||||
result: CartOperationResult.failed,
|
||||
message: 'Gagal mengurangi jumlah item',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CartServiceProvider {
|
||||
static CartService? _instance;
|
||||
|
||||
static CartService get instance {
|
||||
_instance ??= CartServiceImpl();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
static void setInstance(CartService service) {
|
||||
_instance = service;
|
||||
}
|
||||
}
|
|
@ -1,721 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:rijig_mobile/core/router.dart';
|
||||
import 'package:rijig_mobile/core/utils/guide.dart';
|
||||
|
||||
class OrderSummaryScreen extends StatefulWidget {
|
||||
const OrderSummaryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<OrderSummaryScreen> createState() => _OrderSummaryScreenState();
|
||||
}
|
||||
|
||||
class _OrderSummaryScreenState extends State<OrderSummaryScreen> {
|
||||
List<Map<String, dynamic>> selectedItems = [
|
||||
{
|
||||
'name': 'Plastik',
|
||||
'price': 1000,
|
||||
'quantity': 1.0,
|
||||
'icon': Icons.local_drink,
|
||||
'backgroundColor': Colors.blue.shade100,
|
||||
'iconColor': Colors.blue,
|
||||
},
|
||||
{
|
||||
'name': 'Kertas Campur',
|
||||
'price': 700,
|
||||
'quantity': 2.5,
|
||||
'icon': Icons.description,
|
||||
'backgroundColor': Colors.orange.shade100,
|
||||
'iconColor': Colors.orange,
|
||||
},
|
||||
];
|
||||
|
||||
void _removeItem(int index) {
|
||||
if (index < 0 || index >= selectedItems.length) return;
|
||||
|
||||
final removedItem = selectedItems[index];
|
||||
setState(() {
|
||||
selectedItems.removeAt(index);
|
||||
});
|
||||
|
||||
_showSnackbar('${removedItem['name']} berhasil dihapus');
|
||||
}
|
||||
|
||||
void _clearAllItems() {
|
||||
if (selectedItems.isEmpty) return;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Hapus Semua Item'),
|
||||
content: Text(
|
||||
'Apakah Anda yakin ingin menghapus semua item dari daftar?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => router.pop(context),
|
||||
child: Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
selectedItems.clear();
|
||||
});
|
||||
router.pop(context);
|
||||
_showSnackbar('Semua item berhasil dihapus');
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: redColor,
|
||||
foregroundColor: whiteColor,
|
||||
),
|
||||
child: Text('Hapus Semua'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _incrementQuantity(int index) {
|
||||
if (index < 0 || index >= selectedItems.length) return;
|
||||
|
||||
setState(() {
|
||||
selectedItems[index]['quantity'] += 0.5;
|
||||
});
|
||||
}
|
||||
|
||||
void _decrementQuantity(int index) {
|
||||
if (index < 0 || index >= selectedItems.length) return;
|
||||
|
||||
setState(() {
|
||||
final currentQuantity = selectedItems[index]['quantity'] as double;
|
||||
if (currentQuantity > 0.5) {
|
||||
selectedItems[index]['quantity'] = (currentQuantity - 0.5).clamp(
|
||||
0.0,
|
||||
double.infinity,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _showQuantityDialog(int index) {
|
||||
if (index < 0 || index >= selectedItems.length) return;
|
||||
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: selectedItems[index]['quantity'].toString(),
|
||||
);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Input Jumlah ${selectedItems[index]['name']}'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Jumlah (kg)',
|
||||
border: OutlineInputBorder(),
|
||||
suffixText: 'kg',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => router.pop(context),
|
||||
child: Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final newQuantity = double.tryParse(controller.text);
|
||||
if (newQuantity != null && newQuantity > 0) {
|
||||
setState(() {
|
||||
selectedItems[index]['quantity'] = newQuantity;
|
||||
});
|
||||
router.pop(context);
|
||||
} else {
|
||||
_showSnackbar('Masukkan angka yang valid (lebih dari 0)');
|
||||
}
|
||||
},
|
||||
child: Text('Simpan'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showSnackbar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message), duration: Duration(seconds: 2)),
|
||||
);
|
||||
}
|
||||
|
||||
double get totalWeight {
|
||||
return selectedItems.fold(
|
||||
0.0,
|
||||
(sum, item) => sum + (item['quantity'] as double),
|
||||
);
|
||||
}
|
||||
|
||||
int get estimatedEarnings {
|
||||
return selectedItems.fold<int>(
|
||||
0,
|
||||
(sum, item) =>
|
||||
sum + ((item['price'] as int) * (item['quantity'] as double)).round(),
|
||||
);
|
||||
}
|
||||
|
||||
int get applicationFee => 550;
|
||||
|
||||
int get estimatedIncome => estimatedEarnings - applicationFee;
|
||||
|
||||
bool get hasItems => selectedItems.isNotEmpty;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade50,
|
||||
appBar: AppBar(
|
||||
backgroundColor: whiteColor,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => router.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
'Detail Pesanan',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
if (hasItems)
|
||||
PopupMenuButton<String>(
|
||||
icon: Icon(Icons.more_vert, color: Colors.black),
|
||||
onSelected: (value) {
|
||||
if (value == 'clear_all') {
|
||||
_clearAllItems();
|
||||
}
|
||||
},
|
||||
itemBuilder:
|
||||
(BuildContext context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'clear_all',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.clear_all, color: redColor, size: 20),
|
||||
Gap(8),
|
||||
Text(
|
||||
'Hapus Semua',
|
||||
style: TextStyle(color: redColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child:
|
||||
hasItems
|
||||
? SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildItemsSection(),
|
||||
Gap(20),
|
||||
_buildEarningsSection(),
|
||||
],
|
||||
),
|
||||
)
|
||||
: _buildEmptyState(),
|
||||
),
|
||||
_buildBottomButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.delete_outline,
|
||||
size: 48,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
Gap(16),
|
||||
Text(
|
||||
'Belum ada item',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
Gap(8),
|
||||
Text(
|
||||
'Tambahkan item sampah untuk melanjutkan',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade500),
|
||||
),
|
||||
Gap(24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => router.pop(context),
|
||||
icon: Icon(Icons.add),
|
||||
label: Text('Tambah Item'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: whiteColor,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemsSection() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: whiteColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSectionHeader(),
|
||||
Gap(16),
|
||||
...selectedItems.asMap().entries.map(
|
||||
(entry) => _buildItemCard(entry.key, entry.value),
|
||||
),
|
||||
Gap(16),
|
||||
_buildTotalWeight(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.delete_outline, color: Colors.orange, size: 20),
|
||||
),
|
||||
Gap(12),
|
||||
Text(
|
||||
'Jenis Sampah',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
TextButton(
|
||||
onPressed: () => router.pop(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.add, color: Colors.blue, size: 16),
|
||||
Gap(4),
|
||||
Text(
|
||||
'Tambah',
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemCard(int index, Map<String, dynamic> item) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
child: Slidable(
|
||||
key: ValueKey('${item['name']}_$index'),
|
||||
endActionPane: ActionPane(
|
||||
motion: ScrollMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (context) => _removeItem(index),
|
||||
backgroundColor: redColor,
|
||||
foregroundColor: whiteColor,
|
||||
icon: Icons.delete,
|
||||
label: 'Hapus',
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildItemIcon(item),
|
||||
Gap(12),
|
||||
_buildItemInfo(item),
|
||||
_buildQuantityControls(index, item),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemIcon(Map<String, dynamic> item) {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: item['backgroundColor'],
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Icon(item['icon'], color: item['iconColor'], size: 20),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemInfo(Map<String, dynamic> item) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item['name'],
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
Gap(4),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Rp ${item['price']}/kg',
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantityControls(int index, Map<String, dynamic> item) {
|
||||
return Row(
|
||||
children: [
|
||||
_buildQuantityButton(
|
||||
icon: Icons.remove,
|
||||
onTap: () => _decrementQuantity(index),
|
||||
backgroundColor: whiteColor,
|
||||
iconColor: Colors.grey.shade600,
|
||||
),
|
||||
Gap(8),
|
||||
GestureDetector(
|
||||
onTap: () => _showQuantityDialog(index),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: whiteColor,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Text(
|
||||
'${_formatQuantity(item['quantity'])} kg',
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
Gap(8),
|
||||
_buildQuantityButton(
|
||||
icon: Icons.add,
|
||||
onTap: () => _incrementQuantity(index),
|
||||
backgroundColor: Colors.blue,
|
||||
iconColor: whiteColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantityButton({
|
||||
required IconData icon,
|
||||
required VoidCallback onTap,
|
||||
required Color backgroundColor,
|
||||
required Color iconColor,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border:
|
||||
backgroundColor == whiteColor
|
||||
? Border.all(color: Colors.grey.shade300)
|
||||
: null,
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 16),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTotalWeight() {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(Icons.scale, color: Colors.grey.shade700, size: 16),
|
||||
),
|
||||
Gap(12),
|
||||
Text(
|
||||
'Berat total',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
'${_formatQuantity(totalWeight)} kg',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEarningsSection() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: whiteColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.folder, color: Colors.orange, size: 20),
|
||||
),
|
||||
Gap(12),
|
||||
Text(
|
||||
'Perkiraan Pendapatan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Gap(16),
|
||||
_buildIncomeRow(
|
||||
'Estimasi pembayaran',
|
||||
estimatedEarnings,
|
||||
Colors.orange,
|
||||
),
|
||||
Gap(8),
|
||||
_buildIncomeRow(
|
||||
'Biaya jasa aplikasi',
|
||||
applicationFee,
|
||||
Colors.orange,
|
||||
showInfo: true,
|
||||
),
|
||||
Gap(8),
|
||||
_buildIncomeRow(
|
||||
'Estimasi pendapatan',
|
||||
estimatedIncome,
|
||||
Colors.orange,
|
||||
isBold: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIncomeRow(
|
||||
String title,
|
||||
int amount,
|
||||
Color color, {
|
||||
bool showInfo = false,
|
||||
bool isBold = false,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(Icons.currency_exchange, color: whiteColor, size: 12),
|
||||
),
|
||||
Gap(8),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isBold ? FontWeight.w600 : FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
if (showInfo) ...[
|
||||
Gap(4),
|
||||
Icon(Icons.info_outline, color: Colors.blue, size: 16),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Rp ${_formatCurrency(amount)}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isBold ? FontWeight.w600 : FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomButton() {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: whiteColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
hasItems
|
||||
? () {
|
||||
_showSnackbar('Lanjut ke proses selanjutnya');
|
||||
// Handle continue action
|
||||
}
|
||||
: () => router.pop(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: hasItems ? Colors.blue : Colors.grey.shade400,
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
hasItems ? 'Lanjut' : 'Tambah Item',
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatQuantity(double quantity) {
|
||||
return quantity % 1 == 0
|
||||
? quantity.toInt().toString()
|
||||
: quantity.toString();
|
||||
}
|
||||
|
||||
String _formatCurrency(int amount) {
|
||||
return amount.toString().replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]}.',
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,500 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:rijig_mobile/core/router.dart';
|
||||
import 'package:rijig_mobile/core/utils/guide.dart';
|
||||
import 'package:rijig_mobile/widget/appbar.dart';
|
||||
|
||||
class TestRequestPickScreen extends StatefulWidget {
|
||||
const TestRequestPickScreen({super.key});
|
||||
|
||||
@override
|
||||
State<TestRequestPickScreen> createState() => _TestRequestPickScreenState();
|
||||
}
|
||||
|
||||
class _TestRequestPickScreenState extends State<TestRequestPickScreen> {
|
||||
// Map untuk menyimpan quantity setiap item
|
||||
Map<String, double> quantities = {
|
||||
'Plastik': 1.0,
|
||||
'Kertas Campur': 1.0,
|
||||
'Kaca': 0.0,
|
||||
'Minyak Jelantah': 0.0,
|
||||
'Kaleng Alumunium': 0.0,
|
||||
};
|
||||
|
||||
// Map untuk menyimpan harga per kg
|
||||
Map<String, int> prices = {
|
||||
'Plastik': 1000,
|
||||
'Kertas Campur': 700,
|
||||
'Kaca': 300,
|
||||
'Minyak Jelantah': 2500,
|
||||
'Kaleng Alumunium': 3500,
|
||||
};
|
||||
|
||||
// Map untuk menyimpan icon data
|
||||
Map<String, IconData> icons = {
|
||||
'Plastik': Icons.local_drink,
|
||||
'Kertas Campur': Icons.description,
|
||||
'Kaca': Icons.wine_bar,
|
||||
'Minyak Jelantah': Icons.opacity,
|
||||
'Kaleng Alumunium': Icons.recycling,
|
||||
};
|
||||
|
||||
// Map untuk menyimpan warna background
|
||||
Map<String, Color> backgroundColors = {
|
||||
'Plastik': Colors.blue.shade100,
|
||||
'Kertas Campur': Colors.orange.shade100,
|
||||
'Kaca': Colors.red.shade100,
|
||||
'Minyak Jelantah': Colors.orange.shade200,
|
||||
'Kaleng Alumunium': Colors.green.shade100,
|
||||
};
|
||||
|
||||
// Map untuk menyimpan warna icon
|
||||
Map<String, Color> iconColors = {
|
||||
'Plastik': Colors.blue,
|
||||
'Kertas Campur': Colors.orange,
|
||||
'Kaca': Colors.red,
|
||||
'Minyak Jelantah': Colors.orange.shade700,
|
||||
'Kaleng Alumunium': Colors.green,
|
||||
};
|
||||
|
||||
void _resetQuantity(String itemName) {
|
||||
setState(() {
|
||||
quantities[itemName] = 0.0;
|
||||
});
|
||||
}
|
||||
|
||||
void _incrementQuantity(String itemName) {
|
||||
setState(() {
|
||||
quantities[itemName] = (quantities[itemName]! + 2.5);
|
||||
});
|
||||
}
|
||||
|
||||
void _decrementQuantity(String itemName) {
|
||||
setState(() {
|
||||
if (quantities[itemName]! > 0) {
|
||||
quantities[itemName] = (quantities[itemName]! - 2.5).clamp(
|
||||
0.0,
|
||||
double.infinity,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _showQuantityDialog(String itemName) {
|
||||
TextEditingController controller = TextEditingController(
|
||||
text: quantities[itemName]!.toString(),
|
||||
);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Input Jumlah $itemName'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Jumlah (kg)',
|
||||
border: OutlineInputBorder(),
|
||||
suffixText: 'kg',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
double? newQuantity = double.tryParse(controller.text);
|
||||
if (newQuantity != null && newQuantity >= 0) {
|
||||
setState(() {
|
||||
quantities[itemName] = newQuantity;
|
||||
});
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Masukkan angka yang valid')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text('Simpan'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double get totalWeight {
|
||||
return quantities.values.fold(0.0, (sum, quantity) => sum + quantity);
|
||||
}
|
||||
|
||||
int get totalPrice {
|
||||
int total = 0;
|
||||
quantities.forEach((item, quantity) {
|
||||
total += (prices[item]! * quantity).round();
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
int get totalItems {
|
||||
return quantities.values.where((quantity) => quantity > 0).length;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: whiteColor,
|
||||
appBar: CustomAppBar(judul: "Pilih Sampah"),
|
||||
// appBar: AppBar(
|
||||
// backgroundColor: whiteColor,
|
||||
// elevation: 0,
|
||||
// leading: IconButton(
|
||||
// icon: Icon(Icons.arrow_back, color: Colors.black),
|
||||
// onPressed: () => Navigator.pop(context),
|
||||
// ),
|
||||
// title: Row(
|
||||
// children: [
|
||||
// Icon(Icons.location_on, color: Colors.grey),
|
||||
// Gap(8),
|
||||
// Text(
|
||||
// 'Purbalingga',
|
||||
// style: TextStyle(
|
||||
// color: Colors.black,
|
||||
// fontSize: 16,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// Spacer(),
|
||||
// TextButton(
|
||||
// onPressed: () {},
|
||||
// child: Text('Ganti', style: TextStyle(color: Colors.blue)),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: PaddingCustom().paddingAll(16),
|
||||
color: whiteColor,
|
||||
child: Text(
|
||||
'Pilih Sampah',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// List Items
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: PaddingCustom().paddingAll(16),
|
||||
itemCount: quantities.keys.length,
|
||||
itemBuilder: (context, index) {
|
||||
String itemName = quantities.keys.elementAt(index);
|
||||
double quantity = quantities[itemName]!;
|
||||
int price = prices[itemName]!;
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 12),
|
||||
padding: PaddingCustom().paddingAll(16),
|
||||
decoration: BoxDecoration(
|
||||
color: whiteColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icon
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColors[itemName],
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
child: Icon(
|
||||
icons[itemName],
|
||||
color: iconColors[itemName],
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
Gap(16),
|
||||
|
||||
// Item info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
itemName,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
Gap(4),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'Rp $price/kg',
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Show delete icon when quantity > 0 (below price)
|
||||
Gap(4),
|
||||
SizedBox(
|
||||
height: 24, // Fixed height untuk consistency
|
||||
child:
|
||||
quantity > 0
|
||||
? GestureDetector(
|
||||
onTap: () => _resetQuantity(itemName),
|
||||
child: Container(
|
||||
padding: PaddingCustom().paddingAll(
|
||||
4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(
|
||||
4,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(), // Empty space when no quantity
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Quantity controls or Add button
|
||||
quantity > 0
|
||||
? Row(
|
||||
children: [
|
||||
// Decrease button
|
||||
GestureDetector(
|
||||
onTap: () => _decrementQuantity(itemName),
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.remove,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
Gap(12),
|
||||
|
||||
// Quantity display (clickable)
|
||||
GestureDetector(
|
||||
onTap: () => _showQuantityDialog(itemName),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'${quantity.toString().replaceAll('.0', '')} kg',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Gap(12),
|
||||
// Increase button
|
||||
GestureDetector(
|
||||
onTap: () => _incrementQuantity(itemName),
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: Colors.blue,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
// Add button when quantity is 0
|
||||
GestureDetector(
|
||||
onTap: () => _incrementQuantity(itemName),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Tambah',
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom summary
|
||||
Container(
|
||||
// padding: PaddingCustom().paddingAll(16),
|
||||
padding: PaddingCustom().paddingAll(16),
|
||||
decoration: BoxDecoration(
|
||||
color: whiteColor,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'$totalItems jenis ${totalWeight.toString().replaceAll('.0', '')} kg',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
Gap(4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Est. ',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Rp ${totalPrice.toString()}',
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Gap(4),
|
||||
Text(
|
||||
'Minimum total berat 3kg',
|
||||
style: TextStyle(fontSize: 12, color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Gap(16),
|
||||
ElevatedButton(
|
||||
onPressed:
|
||||
totalWeight >= 3
|
||||
? () {
|
||||
router.push('/ordersumary');
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
totalWeight >= 3 ? Colors.blue : Colors.grey,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Lanjut',
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import 'package:rijig_mobile/globaldata/about/about_vmod.dart';
|
|||
import 'package:rijig_mobile/globaldata/article/article_vmod.dart';
|
||||
import 'package:rijig_mobile/widget/buttoncard.dart';
|
||||
import 'package:rijig_mobile/widget/card_withicon.dart';
|
||||
import 'package:rijig_mobile/widget/formfiled.dart';
|
||||
import 'package:rijig_mobile/widget/showmodal.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
|
@ -59,40 +60,24 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Rijig",
|
||||
style: Tulisan.heading(color: primaryColor),
|
||||
Text("Rijig", style: Tulisan.heading(color: primaryColor)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => router.push('/trashview'),
|
||||
icon: Icon(
|
||||
Iconsax.notification_copy,
|
||||
color: primaryColor,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => router.push('/trashview'),
|
||||
icon: Icon(
|
||||
Iconsax.notification_copy,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
|
||||
Gap(10),
|
||||
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
debugPrint('message tapped');
|
||||
},
|
||||
icon: Icon(
|
||||
Iconsax.message_copy,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
debugPrint('message icon tapped');
|
||||
},
|
||||
icon: Icon(Iconsax.message_copy, color: primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -113,14 +98,29 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
text: 'Process',
|
||||
number: '1',
|
||||
onTap: () {
|
||||
CustomModalDialog.show(
|
||||
CustomModalDialog.showWidget(
|
||||
customWidget: FormFieldOne(
|
||||
// controllers: cPhoneController,
|
||||
hintText: 'Masukkan nomor whatsapp anda!',
|
||||
placeholder: "cth.62..",
|
||||
isRequired: true,
|
||||
textInputAction: TextInputAction.done,
|
||||
keyboardType: TextInputType.phone,
|
||||
onTap: () {},
|
||||
onChanged: (value) {},
|
||||
fontSize: 14,
|
||||
fontSizeField: 16,
|
||||
onFieldSubmitted: (value) {},
|
||||
readOnly: false,
|
||||
enabled: true,
|
||||
),
|
||||
context: context,
|
||||
variant: ModalVariant.textVersion,
|
||||
title: 'Belum Tersedia',
|
||||
content: 'Maaf, fitur ini belum tersedia',
|
||||
// variant: ModalVariant.textVersion,
|
||||
// title: 'Belum Tersedia',
|
||||
// content: 'Maaf, fitur ini belum tersedia',
|
||||
buttonCount: 2,
|
||||
button1: CardButtonOne(
|
||||
textButton: "Ya, Hapus",
|
||||
textButton: "oke, deh",
|
||||
onTap: () {},
|
||||
fontSized: 14,
|
||||
colorText: whiteColor,
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:pin_code_fields/pin_code_fields.dart';
|
||||
import 'package:rijig_mobile/core/utils/guide.dart';
|
||||
|
||||
|
@ -46,56 +48,47 @@ class _SecurityCodeScreenState extends State<SecurityCodeScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [secondaryColor, primaryColor],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
|
||||
Text(
|
||||
'Masukkan Security Code Kamu',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: whiteColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 60),
|
||||
|
||||
PinCodeTextField(
|
||||
backgroundColor: whiteColor,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: PaddingCustom().paddingHorizontal(24),
|
||||
child: Column(
|
||||
children: [
|
||||
Gap(60),
|
||||
Text(
|
||||
'Masukkan PIN Kamu',
|
||||
style: Tulisan.heading(fontsize: 20, color: primaryColor),
|
||||
// style: TextStyle(
|
||||
// fontSize: 20,
|
||||
// fontWeight: FontWeight.w600,
|
||||
// color: primaryColor,
|
||||
// ),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Gap(60),
|
||||
Padding(
|
||||
padding: PaddingCustom().paddingHorizontal(27),
|
||||
child: PinCodeTextField(
|
||||
appContext: context,
|
||||
length: 6,
|
||||
controller: textEditingController,
|
||||
readOnly: true,
|
||||
obscureText: true,
|
||||
|
||||
obscuringCharacter: '.',
|
||||
animationType: AnimationType.fade,
|
||||
animationType: AnimationType.slide,
|
||||
pinTheme: PinTheme(
|
||||
shape: PinCodeFieldShape.circle,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
fieldHeight: 20,
|
||||
fieldWidth: 20,
|
||||
activeFillColor: whiteColor,
|
||||
inactiveFillColor: whiteColor.withValues(alpha: 0.3),
|
||||
selectedFillColor: whiteColor.withValues(alpha: 0.7),
|
||||
activeColor: Colors.transparent,
|
||||
inactiveColor: Colors.transparent,
|
||||
selectedColor: Colors.transparent,
|
||||
activeFillColor: primaryColor,
|
||||
inactiveFillColor: greyColor,
|
||||
selectedFillColor: primaryColor,
|
||||
activeColor: primaryColor,
|
||||
inactiveColor: greyColor,
|
||||
selectedColor: blackNavyColor,
|
||||
),
|
||||
animationDuration: const Duration(milliseconds: 300),
|
||||
backgroundColor: Colors.transparent,
|
||||
animationDuration: const Duration(milliseconds: 200),
|
||||
enableActiveFill: true,
|
||||
onCompleted: (v) {
|
||||
_validatePin();
|
||||
|
@ -104,26 +97,21 @@ class _SecurityCodeScreenState extends State<SecurityCodeScreen> {
|
|||
currentText = value;
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
Text(
|
||||
'Lupa Security Code',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: whiteColor,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: whiteColor,
|
||||
),
|
||||
),
|
||||
Gap(40),
|
||||
Text(
|
||||
'Lupa PIN kamu?',
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
color: blackNavyColor,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: primaryColor,
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
_buildKeypad(),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_buildKeypad(),
|
||||
Gap(40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -143,8 +131,7 @@ class _SecurityCodeScreenState extends State<SecurityCodeScreen> {
|
|||
_buildKeypadButton('3'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Gap(20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
@ -153,8 +140,7 @@ class _SecurityCodeScreenState extends State<SecurityCodeScreen> {
|
|||
_buildKeypadButton('6'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Gap(20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
@ -163,15 +149,10 @@ class _SecurityCodeScreenState extends State<SecurityCodeScreen> {
|
|||
_buildKeypadButton('9'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Gap(20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SizedBox(width: 80),
|
||||
_buildKeypadButton('0'),
|
||||
_buildDeleteButton(),
|
||||
],
|
||||
children: [Gap(80), _buildKeypadButton('0'), _buildDeleteButton()],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -184,21 +165,14 @@ class _SecurityCodeScreenState extends State<SecurityCodeScreen> {
|
|||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: whiteColor.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: whiteColor.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(color: primaryColor, shape: BoxShape.circle),
|
||||
child: Center(
|
||||
child: Text(
|
||||
number,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w400,
|
||||
style: Tulisan.customText(
|
||||
color: whiteColor,
|
||||
fontWeight: extraBold,
|
||||
fontsize: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -213,15 +187,11 @@ class _SecurityCodeScreenState extends State<SecurityCodeScreen> {
|
|||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: whiteColor.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: whiteColor.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
border: Border.all(color: primaryColor, width: 1),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(Icons.backspace_outlined, color: whiteColor, size: 24),
|
||||
child: Icon(Icons.backspace_outlined, color: primaryColor, size: 28),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -48,19 +48,6 @@ class _ProfilScreenState extends State<ProfilScreen> {
|
|||
),
|
||||
),
|
||||
),
|
||||
// Positioned(
|
||||
// bottom: 5,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 24,
|
||||
// height: 24,
|
||||
// decoration: const BoxDecoration(
|
||||
// color: Color(0xFF3B82F6),
|
||||
// shape: BoxShape.circle,
|
||||
// ),
|
||||
// child: Icon(Icons.check, color: whiteColor, size: 14),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
Gap(16),
|
||||
|
@ -227,7 +214,6 @@ class _ProfilScreenState extends State<ProfilScreen> {
|
|||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (showDivider) ...[
|
||||
Gap(20),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
|
@ -247,7 +233,8 @@ class _ProfilScreenState extends State<ProfilScreen> {
|
|||
break;
|
||||
|
||||
case 'Ubah Pin':
|
||||
debugPrint('Uabh Pin');
|
||||
debugPrint('Ubah Pin');
|
||||
router.push('/pinsecureinput');
|
||||
break;
|
||||
|
||||
case 'Alamat':
|
||||
|
@ -263,7 +250,6 @@ class _ProfilScreenState extends State<ProfilScreen> {
|
|||
break;
|
||||
|
||||
case 'Keluar':
|
||||
// _showLogoutDialog();
|
||||
CustomBottomSheet.show(
|
||||
context: context,
|
||||
title: "Logout Sekarang?",
|
||||
|
@ -275,18 +261,6 @@ class _ProfilScreenState extends State<ProfilScreen> {
|
|||
],
|
||||
),
|
||||
button1: ButtonLogout(),
|
||||
// button1: CardButtonOne(
|
||||
// textButton: "Logout",
|
||||
// onTap: () {},
|
||||
// fontSized: 14,
|
||||
// colorText: Colors.white,
|
||||
// color: Colors.red,
|
||||
// borderRadius: 10,
|
||||
// horizontal: double.infinity,
|
||||
// vertical: 50,
|
||||
// loadingTrue: false,
|
||||
// usingRow: false,
|
||||
// ),
|
||||
button2: CardButtonOne(
|
||||
textButton: "Gak jadi..",
|
||||
onTap: () => router.pop(),
|
||||
|
|
|
@ -1,172 +0,0 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:custom_refresh_indicator/custom_refresh_indicator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.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/globaldata/trash/trash_viewmodel.dart';
|
||||
import 'package:rijig_mobile/widget/appbar.dart';
|
||||
import 'package:rijig_mobile/widget/skeletonize.dart';
|
||||
|
||||
class RequestPickScreen extends StatefulWidget {
|
||||
const RequestPickScreen({super.key});
|
||||
|
||||
@override
|
||||
RequestPickScreenState createState() => RequestPickScreenState();
|
||||
}
|
||||
|
||||
class RequestPickScreenState extends State<RequestPickScreen> {
|
||||
bool isCartLoaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final trashVM = Provider.of<TrashViewModel>(context, listen: false);
|
||||
final cartVM = Provider.of<CartViewModel>(context, listen: false);
|
||||
await trashVM.loadCategories();
|
||||
await cartVM.loadLocalCart();
|
||||
setState(() => isCartLoaded = true);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String? baseUrl = dotenv.env["BASE_URL"];
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: whiteColor,
|
||||
appBar: CustomAppBar(judul: "Pilih Sampah"),
|
||||
body: CustomMaterialIndicator(
|
||||
onRefresh: () async {
|
||||
await Provider.of<TrashViewModel>(
|
||||
context,
|
||||
listen: false,
|
||||
).loadCategories();
|
||||
},
|
||||
backgroundColor: whiteColor,
|
||||
indicatorBuilder: (context, controller) {
|
||||
return Padding(
|
||||
padding: PaddingCustom().paddingAll(6),
|
||||
child: CircularProgressIndicator(
|
||||
color: primaryColor,
|
||||
value:
|
||||
controller.state.isLoading
|
||||
? null
|
||||
: math.min(controller.value, 1.0),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Consumer2<TrashViewModel, CartViewModel>(
|
||||
builder: (context, trashViewModel, cartViewModel, child) {
|
||||
if (!isCartLoaded || trashViewModel.isLoading) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) => SkeletonCard(),
|
||||
);
|
||||
}
|
||||
|
||||
if (trashViewModel.errorMessage != null) {
|
||||
return Center(child: Text(trashViewModel.errorMessage!));
|
||||
}
|
||||
|
||||
final categories =
|
||||
trashViewModel.trashCategoryResponse?.categories ?? [];
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = categories[index];
|
||||
final cartItems = cartViewModel.cartItems;
|
||||
final existingItem = cartItems.firstWhereOrNull(
|
||||
(e) =>
|
||||
e.trashId.trim().toLowerCase() ==
|
||||
category.id.trim().toLowerCase(),
|
||||
);
|
||||
final amount = existingItem?.amount;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 15,
|
||||
),
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Image.network(
|
||||
"$baseUrl${category.icon}",
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
title: Text(category.name),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Rp${category.price} per kg"),
|
||||
const SizedBox(height: 6),
|
||||
(amount != null && amount > 0)
|
||||
? Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final newAmount = (amount - 0.25).clamp(
|
||||
0.25,
|
||||
double.infinity,
|
||||
);
|
||||
cartViewModel.addOrUpdateItem(
|
||||
CartItem(
|
||||
trashId: category.id,
|
||||
amount: newAmount,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.remove),
|
||||
),
|
||||
Text("${amount.toStringAsFixed(2)} kg"),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final newAmount = amount + 0.25;
|
||||
cartViewModel.addOrUpdateItem(
|
||||
CartItem(
|
||||
trashId: category.id,
|
||||
amount: newAmount,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
cartViewModel.addOrUpdateItem(
|
||||
CartItem(trashId: category.id, amount: 0.25),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text("Tambah ke Keranjang"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: whiteColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,723 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:rijig_mobile/core/router.dart';
|
||||
import 'package:rijig_mobile/core/utils/guide.dart';
|
||||
import 'package:rijig_mobile/features/cart/presentation/viewmodel/trashcart_vmod.dart';
|
||||
import 'package:rijig_mobile/globaldata/trash/trash_viewmodel.dart';
|
||||
import 'package:rijig_mobile/widget/appbar.dart';
|
||||
import 'package:rijig_mobile/widget/buttoncard.dart';
|
||||
import 'package:rijig_mobile/widget/formfiled.dart';
|
||||
import 'package:rijig_mobile/widget/showmodal.dart';
|
||||
import 'package:rijig_mobile/widget/skeletonize.dart';
|
||||
import 'package:rijig_mobile/widget/unhope_handler.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
|
||||
class TestRequestPickScreen extends StatefulWidget {
|
||||
const TestRequestPickScreen({super.key});
|
||||
|
||||
@override
|
||||
State<TestRequestPickScreen> createState() => _TestRequestPickScreenState();
|
||||
}
|
||||
|
||||
class _TestRequestPickScreenState extends State<TestRequestPickScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _slideAnimation;
|
||||
bool _isInitialized = false;
|
||||
|
||||
final Map<String, bool> _itemLoadingStates = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_slideAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_initializeData();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _initializeData() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
final trashViewModel = Provider.of<TrashViewModel>(context, listen: false);
|
||||
final cartViewModel = Provider.of<CartViewModel>(context, listen: false);
|
||||
|
||||
await Future.wait([
|
||||
trashViewModel.loadCategories(),
|
||||
cartViewModel.loadCartItems(showLoading: false),
|
||||
]);
|
||||
|
||||
if (mounted) {
|
||||
_updateBottomSheetVisibility();
|
||||
setState(() {
|
||||
_isInitialized = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateBottomSheetVisibility() {
|
||||
final cartViewModel = Provider.of<CartViewModel>(context, listen: false);
|
||||
|
||||
if (cartViewModel.isNotEmpty) {
|
||||
_animationController.forward();
|
||||
} else {
|
||||
_animationController.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleQuantityChange(
|
||||
String categoryId,
|
||||
double newQuantity,
|
||||
) async {
|
||||
setState(() {
|
||||
_itemLoadingStates[categoryId] = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final cartViewModel = Provider.of<CartViewModel>(context, listen: false);
|
||||
|
||||
if (newQuantity <= 0) {
|
||||
await cartViewModel.deleteItem(categoryId, showUpdating: false);
|
||||
} else {
|
||||
await cartViewModel.addOrUpdateItem(
|
||||
categoryId,
|
||||
newQuantity.toInt(),
|
||||
showUpdating: false,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
_updateBottomSheetVisibility();
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_itemLoadingStates[categoryId] = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _incrementQuantity(String categoryId) async {
|
||||
final cartViewModel = Provider.of<CartViewModel>(context, listen: false);
|
||||
final currentAmount = cartViewModel.getItemAmount(categoryId);
|
||||
await _handleQuantityChange(categoryId, (currentAmount + 2.5));
|
||||
}
|
||||
|
||||
Future<void> _decrementQuantity(String categoryId) async {
|
||||
final cartViewModel = Provider.of<CartViewModel>(context, listen: false);
|
||||
final currentAmount = cartViewModel.getItemAmount(categoryId);
|
||||
final newAmount = (currentAmount - 2.5).clamp(0.0, double.infinity);
|
||||
await _handleQuantityChange(categoryId, newAmount);
|
||||
}
|
||||
|
||||
Future<void> _resetQuantity(String categoryId) async {
|
||||
await _handleQuantityChange(categoryId, 0);
|
||||
}
|
||||
|
||||
void _showQuantityDialog(String categoryId, String itemName) {
|
||||
final cartViewModel = Provider.of<CartViewModel>(context, listen: false);
|
||||
final currentAmount = cartViewModel.getItemAmount(categoryId);
|
||||
|
||||
TextEditingController controller = TextEditingController(
|
||||
text: currentAmount.toString().replaceAll('.0', ''),
|
||||
);
|
||||
|
||||
CustomModalDialog.showWidget(
|
||||
customWidget: FormFieldOne(
|
||||
controllers: controller,
|
||||
hintText: 'masukkan berat dengan benar ya..',
|
||||
isRequired: true,
|
||||
textInputAction: TextInputAction.done,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onTap: () {},
|
||||
onChanged: (value) {},
|
||||
fontSize: 14.sp,
|
||||
fontSizeField: 16.sp,
|
||||
onFieldSubmitted: (value) {},
|
||||
readOnly: false,
|
||||
enabled: true,
|
||||
),
|
||||
context: context,
|
||||
buttonCount: 2,
|
||||
button1: CardButtonOne(
|
||||
textButton: "Simpan",
|
||||
onTap: () async {
|
||||
double? newQuantity = double.tryParse(controller.text);
|
||||
if (newQuantity != null && newQuantity >= 0) {
|
||||
Navigator.pop(context);
|
||||
await _handleQuantityChange(categoryId, newQuantity);
|
||||
} else {
|
||||
toastification.show(
|
||||
type: ToastificationType.warning,
|
||||
title: const Text("Masukkan angka yang valid"),
|
||||
autoCloseDuration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
},
|
||||
fontSized: 14.sp,
|
||||
colorText: whiteColor,
|
||||
color: primaryColor,
|
||||
borderRadius: 10.sp,
|
||||
horizontal: double.infinity,
|
||||
vertical: 50,
|
||||
loadingTrue: false,
|
||||
usingRow: false,
|
||||
),
|
||||
button2: CardButtonOne(
|
||||
textButton: "Batal",
|
||||
onTap: () => router.pop(),
|
||||
fontSized: 14.sp,
|
||||
colorText: primaryColor,
|
||||
color: Colors.transparent,
|
||||
borderRadius: 10,
|
||||
horizontal: double.infinity,
|
||||
vertical: 50,
|
||||
loadingTrue: false,
|
||||
usingRow: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantityControls(String categoryId) {
|
||||
final isItemLoading = _itemLoadingStates[categoryId] ?? false;
|
||||
|
||||
return Consumer<CartViewModel>(
|
||||
builder: (context, cartViewModel, child) {
|
||||
final amount = cartViewModel.getItemAmount(categoryId);
|
||||
|
||||
if (amount > 0) {
|
||||
return Row(
|
||||
children: [
|
||||
_buildControlButton(
|
||||
icon: Icons.remove,
|
||||
color: Colors.red,
|
||||
onTap:
|
||||
isItemLoading ? null : () => _decrementQuantity(categoryId),
|
||||
isLoading: isItemLoading,
|
||||
),
|
||||
const Gap(12),
|
||||
_buildQuantityDisplay(
|
||||
categoryId,
|
||||
amount.toDouble(),
|
||||
isItemLoading,
|
||||
),
|
||||
const Gap(12),
|
||||
_buildControlButton(
|
||||
icon: Icons.add,
|
||||
color: primaryColor,
|
||||
onTap:
|
||||
isItemLoading ? null : () => _incrementQuantity(categoryId),
|
||||
isLoading: isItemLoading,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return GestureDetector(
|
||||
onTap: isItemLoading ? null : () => _incrementQuantity(categoryId),
|
||||
child: Container(
|
||||
padding: PaddingCustom().paddingHorizontalVertical(16, 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isItemLoading ? Colors.grey : primaryColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child:
|
||||
isItemLoading
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(whiteColor),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'Tambah',
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControlButton({
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required VoidCallback? onTap,
|
||||
bool isLoading = false,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
onTap != null
|
||||
? color.withValues(alpha: 0.1)
|
||||
: Colors.grey.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child:
|
||||
isLoading
|
||||
? Center(
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
icon,
|
||||
color: onTap != null ? color : Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantityDisplay(
|
||||
String categoryId,
|
||||
double quantity,
|
||||
bool isLoading,
|
||||
) {
|
||||
final trashViewModel = Provider.of<TrashViewModel>(context, listen: false);
|
||||
final category = trashViewModel.getCategoryById(categoryId);
|
||||
|
||||
return GestureDetector(
|
||||
onTap:
|
||||
isLoading
|
||||
? null
|
||||
: () => _showQuantityDialog(categoryId, category?.name ?? ''),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isLoading ? Colors.orange.shade300 : Colors.grey.shade300,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: isLoading ? Colors.orange.shade50 : Colors.transparent,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isLoading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.orange),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${quantity.toString().replaceAll('.0', '')} kg',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isLoading ? Colors.orange.shade700 : Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTrashItem(dynamic category) {
|
||||
final String? baseUrl = dotenv.env["BASE_URL"];
|
||||
|
||||
return Consumer<CartViewModel>(
|
||||
builder: (context, cartViewModel, child) {
|
||||
final amount = cartViewModel.getItemAmount(category.id);
|
||||
num price =
|
||||
(category.price is String)
|
||||
? int.tryParse(category.price) ?? 0
|
||||
: category.price ?? 0;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: PaddingCustom().paddingAll(16),
|
||||
decoration: BoxDecoration(
|
||||
color: whiteColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border:
|
||||
amount > 0
|
||||
? Border.all(
|
||||
color: primaryColor.withValues(alpha: 0.3),
|
||||
width: 1.5,
|
||||
)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color:
|
||||
amount > 0
|
||||
? primaryColor.withValues(alpha: 0.1)
|
||||
: Colors.grey.withValues(alpha: 0.1),
|
||||
spreadRadius: amount > 0 ? 2 : 1,
|
||||
blurRadius: amount > 0 ? 6 : 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildCategoryIcon(baseUrl, category),
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: _buildCategoryInfo(category, price, amount.toDouble()),
|
||||
),
|
||||
_buildQuantityControls(category.id),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryIcon(String? baseUrl, dynamic category) {
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
child: Image.network(
|
||||
'$baseUrl${category.icon}',
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(Icons.category, color: Colors.grey, size: 24);
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
value:
|
||||
loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryInfo(dynamic category, num price, double quantity) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
category.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'Rp ${price.toString()}/kg',
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
SizedBox(
|
||||
height: 24,
|
||||
child:
|
||||
quantity > 0
|
||||
? GestureDetector(
|
||||
onTap: () => _resetQuantity(category.id),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: PaddingCustom().paddingAll(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomSheet() {
|
||||
return Consumer<CartViewModel>(
|
||||
builder: (context, cartViewModel, child) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: PaddingCustom().paddingAll(16),
|
||||
decoration: BoxDecoration(
|
||||
color: whiteColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: _buildOrderSummary(cartViewModel)),
|
||||
const Gap(16),
|
||||
_buildContinueButton(cartViewModel),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOrderSummary(CartViewModel cartViewModel) {
|
||||
final meetsMinimumWeight = cartViewModel.totalItems >= 3;
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Column(
|
||||
key: ValueKey(cartViewModel.totalItems),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${cartViewModel.cartItems.length} jenis ${cartViewModel.totalItems.toString()} kg',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade700),
|
||||
),
|
||||
const Gap(10),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Est. ',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade700),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
cartViewModel.formattedTotalPrice,
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!meetsMinimumWeight) ...[
|
||||
const Gap(10),
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
opacity: meetsMinimumWeight ? 0.0 : 1.0,
|
||||
child: const Text(
|
||||
'Minimum total berat 3kg',
|
||||
style: TextStyle(fontSize: 12, color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContinueButton(CartViewModel cartViewModel) {
|
||||
final meetsMinimumWeight = cartViewModel.totalItems >= 3;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
meetsMinimumWeight ? () => _handleContinue(cartViewModel) : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: meetsMinimumWeight ? Colors.blue : Colors.grey,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: Text(
|
||||
'Lanjut',
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleContinue(CartViewModel cartViewModel) {
|
||||
final trashViewModel = Provider.of<TrashViewModel>(context, listen: false);
|
||||
|
||||
Map<String, dynamic> orderData = {
|
||||
'selectedItems':
|
||||
cartViewModel.cartItems.map((cartItem) {
|
||||
trashViewModel.getCategoryById(cartItem.trashId);
|
||||
return {
|
||||
'id': cartItem.trashId,
|
||||
'name': cartItem.trashName,
|
||||
'quantity': cartItem.amount.toDouble(),
|
||||
'pricePerKg': cartItem.trashPrice,
|
||||
'totalPrice': cartItem.subtotalEstimatedPrice.round(),
|
||||
};
|
||||
}).toList(),
|
||||
'totalWeight': cartViewModel.totalItems.toDouble(),
|
||||
'totalPrice': cartViewModel.totalPrice.round(),
|
||||
'totalItems': cartViewModel.cartItems.length,
|
||||
};
|
||||
|
||||
context.push('/ordersumary', extra: orderData);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: whiteColor,
|
||||
appBar: const CustomAppBar(judul: "Pilih Sampah"),
|
||||
body: Consumer2<TrashViewModel, CartViewModel>(
|
||||
builder: (context, trashViewModel, cartViewModel, child) {
|
||||
if (trashViewModel.isLoading) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) => SkeletonCard(),
|
||||
);
|
||||
}
|
||||
|
||||
if (trashViewModel.hasError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 64, color: Colors.red),
|
||||
const Gap(16),
|
||||
const Text(
|
||||
'Terjadi kesalahan',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const Gap(8),
|
||||
Padding(
|
||||
padding: PaddingCustom().paddingHorizontal(32),
|
||||
child: Text(
|
||||
trashViewModel.errorMessage ?? 'Error tidak diketahui',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
ElevatedButton(
|
||||
onPressed: () => _initializeData(),
|
||||
child: const Text('Coba Lagi'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!trashViewModel.hasData || trashViewModel.categories.isEmpty) {
|
||||
return InfoStateWidget(type: InfoStateType.emptyData);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
ListView.builder(
|
||||
padding: PaddingCustom().paddingOnly(
|
||||
top: 16,
|
||||
right: 16,
|
||||
bottom: cartViewModel.isNotEmpty ? 120 : 16,
|
||||
left: 16,
|
||||
),
|
||||
itemCount: trashViewModel.categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = trashViewModel.categories[index];
|
||||
return _buildTrashItem(category);
|
||||
},
|
||||
),
|
||||
|
||||
AnimatedBuilder(
|
||||
animation: _slideAnimation,
|
||||
builder: (context, child) {
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom:
|
||||
cartViewModel.isNotEmpty
|
||||
? (_slideAnimation.value - 1) * 200
|
||||
: -200,
|
||||
child:
|
||||
cartViewModel.isNotEmpty
|
||||
? _buildBottomSheet()
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
class Category {
|
||||
final String id;
|
||||
final String name;
|
||||
final dynamic price;
|
||||
final dynamic price; // Consider using num or double instead of dynamic
|
||||
final String icon;
|
||||
final String createdAt;
|
||||
final String updatedAt;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
Category({
|
||||
required this.id,
|
||||
|
@ -17,14 +17,39 @@ class Category {
|
|||
|
||||
factory Category.fromJson(Map<String, dynamic> json) {
|
||||
return Category(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
price: json['estimatedprice'],
|
||||
icon: json['icon'],
|
||||
createdAt: json['createdAt'],
|
||||
updatedAt: json['updatedAt'],
|
||||
id: json['id'] ?? '',
|
||||
name: json['name'] ?? '',
|
||||
price: json['estimatedprice'] ?? 0,
|
||||
icon: json['icon'] ?? '',
|
||||
createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
|
||||
updatedAt: DateTime.tryParse(json['updatedAt'] ?? '') ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'estimatedprice': price,
|
||||
'icon': icon,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Category(id: $id, name: $name, price: $price, icon: $icon)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is Category && other.id == id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
||||
class TrashCategoryResponse {
|
||||
|
@ -39,11 +64,32 @@ class TrashCategoryResponse {
|
|||
});
|
||||
|
||||
factory TrashCategoryResponse.fromJson(Map<String, dynamic> json) {
|
||||
return TrashCategoryResponse(
|
||||
categories:
|
||||
(json['data'] as List).map((e) => Category.fromJson(e)).toList(),
|
||||
message: json['meta']['message'],
|
||||
total: json['meta']['total'],
|
||||
);
|
||||
try {
|
||||
final dataList = json['data'] as List? ?? [];
|
||||
final meta = json['meta'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return TrashCategoryResponse(
|
||||
categories:
|
||||
dataList
|
||||
.map((e) => Category.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
message: meta['message'] as String? ?? '',
|
||||
total: meta['total'] as int? ?? 0,
|
||||
);
|
||||
} catch (e) {
|
||||
throw FormatException('Failed to parse TrashCategoryResponse: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'data': categories.map((e) => e.toJson()).toList(),
|
||||
'meta': {'message': message, 'total': total},
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TrashCategoryResponse(categories: ${categories.length}, message: $message, total: $total)';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
import 'package:rijig_mobile/core/api/api_services.dart';
|
||||
import 'package:rijig_mobile/globaldata/trash/trash_model.dart';
|
||||
|
||||
class TrashCategoryRepository {
|
||||
abstract class ITrashCategoryRepository {
|
||||
Future<TrashCategoryResponse> fetchCategories();
|
||||
}
|
||||
|
||||
class TrashCategoryRepository implements ITrashCategoryRepository {
|
||||
final Https _https = Https();
|
||||
|
||||
@override
|
||||
Future<TrashCategoryResponse> fetchCategories() async {
|
||||
final response = await _https.get('/trash/categories');
|
||||
return TrashCategoryResponse.fromJson(response);
|
||||
try {
|
||||
final response = await _https.get('/trash/categories');
|
||||
return TrashCategoryResponse.fromJson(response);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,37 @@
|
|||
import 'package:rijig_mobile/globaldata/trash/trash_model.dart';
|
||||
import 'package:rijig_mobile/globaldata/trash/trash_repository.dart';
|
||||
|
||||
class TrashCategoryService {
|
||||
final TrashCategoryRepository _trashCategoryRepository;
|
||||
abstract class ITrashCategoryService {
|
||||
Future<TrashCategoryResponse> getCategories();
|
||||
}
|
||||
|
||||
TrashCategoryService(this._trashCategoryRepository);
|
||||
class TrashCategoryService implements ITrashCategoryService {
|
||||
final ITrashCategoryRepository _repository;
|
||||
|
||||
TrashCategoryService(this._repository);
|
||||
|
||||
@override
|
||||
Future<TrashCategoryResponse> getCategories() async {
|
||||
try {
|
||||
return await _trashCategoryRepository.fetchCategories();
|
||||
final response = await _repository.fetchCategories();
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load categories: $e');
|
||||
throw TrashCategoryServiceException(
|
||||
'Service Error: Failed to load categories - $e',
|
||||
500,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TrashCategoryServiceException implements Exception {
|
||||
final String message;
|
||||
final int statusCode;
|
||||
|
||||
TrashCategoryServiceException(this.message, this.statusCode);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'TrashCategoryServiceException: $message (Status: $statusCode)';
|
||||
}
|
||||
|
|
|
@ -2,27 +2,82 @@ import 'package:flutter/material.dart';
|
|||
import 'package:rijig_mobile/globaldata/trash/trash_model.dart';
|
||||
import 'package:rijig_mobile/globaldata/trash/trash_service.dart';
|
||||
|
||||
enum TrashCategoryState { initial, loading, loaded, error }
|
||||
|
||||
class TrashViewModel extends ChangeNotifier {
|
||||
final TrashCategoryService _trashCategoryService;
|
||||
final ITrashCategoryService _service;
|
||||
|
||||
TrashViewModel(this._trashCategoryService);
|
||||
TrashViewModel(this._service);
|
||||
|
||||
bool isLoading = false;
|
||||
String? errorMessage;
|
||||
TrashCategoryResponse? trashCategoryResponse;
|
||||
TrashCategoryState _state = TrashCategoryState.initial;
|
||||
String? _errorMessage;
|
||||
TrashCategoryResponse? _trashCategoryResponse;
|
||||
|
||||
Future<void> loadCategories() async {
|
||||
isLoading = true;
|
||||
errorMessage = null;
|
||||
notifyListeners();
|
||||
TrashCategoryState get state => _state;
|
||||
bool get isLoading => _state == TrashCategoryState.loading;
|
||||
bool get hasError => _state == TrashCategoryState.error;
|
||||
bool get hasData =>
|
||||
_state == TrashCategoryState.loaded && _trashCategoryResponse != null;
|
||||
String? get errorMessage => _errorMessage;
|
||||
TrashCategoryResponse? get trashCategoryResponse => _trashCategoryResponse;
|
||||
List<Category> get categories => _trashCategoryResponse?.categories ?? [];
|
||||
|
||||
try {
|
||||
trashCategoryResponse = await _trashCategoryService.getCategories();
|
||||
} catch (e) {
|
||||
errorMessage = "Error: ${e.toString()}";
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
void _setState(TrashCategoryState newState) {
|
||||
_state = newState;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
Future<void> loadCategories({bool forceRefresh = false}) async {
|
||||
if (_state == TrashCategoryState.loading) return;
|
||||
|
||||
if (_state == TrashCategoryState.loaded && !forceRefresh) return;
|
||||
|
||||
_setState(TrashCategoryState.loading);
|
||||
_errorMessage = null;
|
||||
|
||||
try {
|
||||
_trashCategoryResponse = await _service.getCategories();
|
||||
_setState(TrashCategoryState.loaded);
|
||||
} on TrashCategoryServiceException catch (e) {
|
||||
_errorMessage = e.message;
|
||||
_setState(TrashCategoryState.error);
|
||||
} catch (e) {
|
||||
_errorMessage = "Unexpected error: ${e.toString()}";
|
||||
_setState(TrashCategoryState.error);
|
||||
}
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
if (_state == TrashCategoryState.error) {
|
||||
_errorMessage = null;
|
||||
_setState(TrashCategoryState.initial);
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_state = TrashCategoryState.initial;
|
||||
_errorMessage = null;
|
||||
_trashCategoryResponse = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Category? getCategoryById(String id) {
|
||||
try {
|
||||
return categories.firstWhere((category) => category.id == id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<Category> searchCategories(String query) {
|
||||
if (query.isEmpty) return categories;
|
||||
|
||||
return categories
|
||||
.where(
|
||||
(category) =>
|
||||
category.name.toLowerCase().contains(query.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +1,125 @@
|
|||
// ===lib/widget/showmodal.dart===
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:rijig_mobile/core/router.dart';
|
||||
import 'package:rijig_mobile/core/utils/guide.dart';
|
||||
|
||||
enum ModalVariant { textVersion, imageVersion }
|
||||
enum ModalVariant { textVersion, imageVersion, widgetSupportVersion }
|
||||
|
||||
class CustomModalDialog extends StatelessWidget {
|
||||
final ModalVariant variant;
|
||||
|
||||
final String title;
|
||||
final String content;
|
||||
// Text and Image version properties
|
||||
final String? title;
|
||||
final String? content;
|
||||
final String? imageAsset;
|
||||
|
||||
// Widget support version properties
|
||||
final Widget? customWidget;
|
||||
|
||||
final int buttonCount;
|
||||
final Widget? button1;
|
||||
final Widget? button2;
|
||||
|
||||
// Parameter boolean untuk mengontrol tampilan close icon
|
||||
|
||||
final bool showCloseIcon;
|
||||
|
||||
const CustomModalDialog({
|
||||
super.key,
|
||||
required this.variant,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.title,
|
||||
this.content,
|
||||
this.imageAsset,
|
||||
this.customWidget,
|
||||
this.buttonCount = 0,
|
||||
this.button1,
|
||||
this.button2,
|
||||
this.showCloseIcon = true, // Default true untuk backward compatibility
|
||||
});
|
||||
this.showCloseIcon = true,
|
||||
}) : assert(
|
||||
variant == ModalVariant.widgetSupportVersion
|
||||
? customWidget != null
|
||||
: (title != null && content != null),
|
||||
'For widgetSupportVersion, customWidget must be provided. '
|
||||
'For other variants, title and content must be provided.',
|
||||
);
|
||||
|
||||
static void show({
|
||||
// Static method for text version
|
||||
static void showText({
|
||||
required BuildContext context,
|
||||
required ModalVariant variant,
|
||||
required String title,
|
||||
required String content,
|
||||
String? imageAsset,
|
||||
int buttonCount = 0,
|
||||
Widget? button1,
|
||||
Widget? button2,
|
||||
bool showCloseIcon = true, // Parameter baru di static method
|
||||
bool showCloseIcon = true,
|
||||
}) {
|
||||
_showModal(
|
||||
context: context,
|
||||
variant: ModalVariant.textVersion,
|
||||
title: title,
|
||||
content: content,
|
||||
buttonCount: buttonCount,
|
||||
button1: button1,
|
||||
button2: button2,
|
||||
showCloseIcon: showCloseIcon,
|
||||
);
|
||||
}
|
||||
|
||||
// Static method for image version
|
||||
static void showImage({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String content,
|
||||
required String imageAsset,
|
||||
int buttonCount = 0,
|
||||
Widget? button1,
|
||||
Widget? button2,
|
||||
bool showCloseIcon = true,
|
||||
}) {
|
||||
_showModal(
|
||||
context: context,
|
||||
variant: ModalVariant.imageVersion,
|
||||
title: title,
|
||||
content: content,
|
||||
imageAsset: imageAsset,
|
||||
buttonCount: buttonCount,
|
||||
button1: button1,
|
||||
button2: button2,
|
||||
showCloseIcon: showCloseIcon,
|
||||
);
|
||||
}
|
||||
|
||||
// Static method for widget support version
|
||||
static void showWidget({
|
||||
required BuildContext context,
|
||||
required Widget customWidget,
|
||||
int buttonCount = 0,
|
||||
Widget? button1,
|
||||
Widget? button2,
|
||||
bool showCloseIcon = true,
|
||||
}) {
|
||||
_showModal(
|
||||
context: context,
|
||||
variant: ModalVariant.widgetSupportVersion,
|
||||
customWidget: customWidget,
|
||||
buttonCount: buttonCount,
|
||||
button1: button1,
|
||||
button2: button2,
|
||||
showCloseIcon: showCloseIcon,
|
||||
);
|
||||
}
|
||||
|
||||
// Private method to handle the modal display
|
||||
static void _showModal({
|
||||
required BuildContext context,
|
||||
required ModalVariant variant,
|
||||
String? title,
|
||||
String? content,
|
||||
String? imageAsset,
|
||||
Widget? customWidget,
|
||||
int buttonCount = 0,
|
||||
Widget? button1,
|
||||
Widget? button2,
|
||||
bool showCloseIcon = true,
|
||||
}) {
|
||||
showGeneralDialog(
|
||||
context: context,
|
||||
|
@ -66,10 +145,11 @@ class CustomModalDialog extends StatelessWidget {
|
|||
title: title,
|
||||
content: content,
|
||||
imageAsset: imageAsset,
|
||||
customWidget: customWidget,
|
||||
buttonCount: buttonCount,
|
||||
button1: button1,
|
||||
button2: button2,
|
||||
showCloseIcon: showCloseIcon, // Pass parameter ke constructor
|
||||
showCloseIcon: showCloseIcon,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -81,6 +161,20 @@ class CustomModalDialog extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget contentWidget;
|
||||
|
||||
switch (variant) {
|
||||
case ModalVariant.textVersion:
|
||||
contentWidget = _buildTextContent();
|
||||
break;
|
||||
case ModalVariant.imageVersion:
|
||||
contentWidget = _buildImageContent(context);
|
||||
break;
|
||||
case ModalVariant.widgetSupportVersion:
|
||||
contentWidget = customWidget!;
|
||||
break;
|
||||
}
|
||||
|
||||
final modalContent = Container(
|
||||
padding: PaddingCustom().paddingHorizontalVertical(20, 24),
|
||||
margin: const EdgeInsets.only(top: 30),
|
||||
|
@ -91,58 +185,88 @@ class CustomModalDialog extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (variant == ModalVariant.imageVersion && imageAsset != null)
|
||||
Padding(
|
||||
padding: PaddingCustom().paddingOnly(bottom: 20),
|
||||
child: Image.asset(
|
||||
imageAsset!,
|
||||
width: MediaQuery.of(context).size.width * 0.6,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(title, style: Tulisan.subheading(fontsize: 19)),
|
||||
),
|
||||
Gap(8),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(content, style: Tulisan.customText(fontsize: 14)),
|
||||
),
|
||||
Gap(24),
|
||||
if (buttonCount == 1 && button1 != null) button1!,
|
||||
if (buttonCount == 2 && button1 != null && button2 != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Expanded(child: button2!),
|
||||
Gap(16),
|
||||
Expanded(child: button1!),
|
||||
],
|
||||
),
|
||||
contentWidget,
|
||||
if (buttonCount > 0) ...[const Gap(24), _buildButtons()],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [modalContent, if (showCloseIcon) _buildCloseButton()],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextContent() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
modalContent,
|
||||
// Kondisional untuk menampilkan close icon
|
||||
if (showCloseIcon)
|
||||
Positioned(
|
||||
top: -15,
|
||||
right: 5,
|
||||
child: GestureDetector(
|
||||
onTap: () => router.pop(),
|
||||
child: CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundColor: whiteColor,
|
||||
child: Icon(Icons.close, color: blackNavyColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(title!, style: Tulisan.subheading(fontsize: 19)),
|
||||
),
|
||||
const Gap(8),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(content!, style: Tulisan.customText(fontsize: 14)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildImageContent(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (imageAsset != null) ...[
|
||||
Image.asset(
|
||||
imageAsset!,
|
||||
width: MediaQuery.of(context).size.width * 0.6,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const Gap(20),
|
||||
],
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(title!, style: Tulisan.subheading(fontsize: 19)),
|
||||
),
|
||||
const Gap(8),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(content!, style: Tulisan.customText(fontsize: 14)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButtons() {
|
||||
if (buttonCount == 1 && button1 != null) {
|
||||
return button1!;
|
||||
} else if (buttonCount == 2 && button1 != null && button2 != null) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Expanded(child: button2!),
|
||||
const Gap(16),
|
||||
Expanded(child: button1!),
|
||||
],
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildCloseButton() {
|
||||
return Positioned(
|
||||
top: -15,
|
||||
right: 5,
|
||||
child: GestureDetector(
|
||||
onTap: () => router.pop(),
|
||||
child: CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundColor: whiteColor,
|
||||
child: Icon(Icons.close, color: blackNavyColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,726 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:rijig_mobile/core/router.dart';
|
||||
import 'package:rijig_mobile/core/utils/guide.dart';
|
||||
import 'package:rijig_mobile/features/cart/presentation/viewmodel/trashcart_vmod.dart';
|
||||
import 'package:rijig_mobile/globaldata/trash/trash_viewmodel.dart';
|
||||
import 'package:rijig_mobile/widget/appbar.dart';
|
||||
import 'package:rijig_mobile/widget/buttoncard.dart';
|
||||
import 'package:rijig_mobile/widget/formfiled.dart';
|
||||
import 'package:rijig_mobile/widget/showmodal.dart';
|
||||
import 'package:rijig_mobile/widget/skeletonize.dart';
|
||||
import 'package:rijig_mobile/widget/unhope_handler.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
|
||||
class TestRequestPickScreen extends StatefulWidget {
|
||||
const TestRequestPickScreen({super.key});
|
||||
|
||||
@override
|
||||
State<TestRequestPickScreen> createState() => _TestRequestPickScreenState();
|
||||
}
|
||||
|
||||
class _TestRequestPickScreenState extends State<TestRequestPickScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _slideAnimation;
|
||||
bool _isInitialized = false;
|
||||
|
||||
// Track loading states per item
|
||||
final Map<String, bool> _itemLoadingStates = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_slideAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_initializeData();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _initializeData() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
final trashViewModel = Provider.of<TrashViewModel>(context, listen: false);
|
||||
final cartViewModel = Provider.of<CartViewModel>(context, listen: false);
|
||||
|
||||
await Future.wait([
|
||||
trashViewModel.loadCategories(),
|
||||
cartViewModel.loadCartItems(showLoading: false),
|
||||
]);
|
||||
|
||||
if (mounted) {
|
||||
_updateBottomSheetVisibility();
|
||||
setState(() {
|
||||
_isInitialized = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateBottomSheetVisibility() {
|
||||
final cartViewModel = Provider.of<CartViewModel>(context, listen: false);
|
||||
|
||||
if (cartViewModel.isNotEmpty) {
|
||||
_animationController.forward();
|
||||
} else {
|
||||
_animationController.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleQuantityChange(
|
||||
String categoryId,
|
||||
double newQuantity,
|
||||
) async {
|
||||
// Set loading state
|
||||
setState(() {
|
||||
_itemLoadingStates[categoryId] = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final cartViewModel = Provider.of<CartViewModel>(context, listen: false);
|
||||
|
||||
if (newQuantity <= 0) {
|
||||
await cartViewModel.deleteItem(categoryId, showUpdating: false);
|
||||
} else {
|
||||
await cartViewModel.addOrUpdateItem(
|
||||
categoryId,
|
||||
newQuantity.toInt(),
|
||||
showUpdating: false,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
_updateBottomSheetVisibility();
|
||||
}
|
||||
} finally {
|
||||
// Clear loading state
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_itemLoadingStates[categoryId] = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _incrementQuantity(String categoryId) async {
|
||||
final cartViewModel = Provider.of<CartViewModel>(context, listen: false);
|
||||
final currentAmount = cartViewModel.getItemAmount(categoryId);
|
||||
await _handleQuantityChange(categoryId, (currentAmount + 2.5));
|
||||
}
|
||||
|
||||
Future<void> _decrementQuantity(String categoryId) async {
|
||||
final cartViewModel = Provider.of<CartViewModel>(context, listen: false);
|
||||
final currentAmount = cartViewModel.getItemAmount(categoryId);
|
||||
final newAmount = (currentAmount - 2.5).clamp(0.0, double.infinity);
|
||||
await _handleQuantityChange(categoryId, newAmount);
|
||||
}
|
||||
|
||||
Future<void> _resetQuantity(String categoryId) async {
|
||||
await _handleQuantityChange(categoryId, 0);
|
||||
}
|
||||
|
||||
void _showQuantityDialog(String categoryId, String itemName) {
|
||||
final cartViewModel = Provider.of<CartViewModel>(context, listen: false);
|
||||
final currentAmount = cartViewModel.getItemAmount(categoryId);
|
||||
|
||||
TextEditingController controller = TextEditingController(
|
||||
text: currentAmount.toString().replaceAll('.0', ''),
|
||||
);
|
||||
|
||||
CustomModalDialog.showWidget(
|
||||
customWidget: FormFieldOne(
|
||||
controllers: controller,
|
||||
hintText: 'masukkan berat dengan benar ya..',
|
||||
isRequired: true,
|
||||
textInputAction: TextInputAction.done,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onTap: () {},
|
||||
onChanged: (value) {},
|
||||
fontSize: 14.sp,
|
||||
fontSizeField: 16.sp,
|
||||
onFieldSubmitted: (value) {},
|
||||
readOnly: false,
|
||||
enabled: true,
|
||||
),
|
||||
context: context,
|
||||
buttonCount: 2,
|
||||
button1: CardButtonOne(
|
||||
textButton: "Simpan",
|
||||
onTap: () async {
|
||||
double? newQuantity = double.tryParse(controller.text);
|
||||
if (newQuantity != null && newQuantity >= 0) {
|
||||
Navigator.pop(context);
|
||||
await _handleQuantityChange(categoryId, newQuantity);
|
||||
} else {
|
||||
toastification.show(
|
||||
type: ToastificationType.warning,
|
||||
title: const Text("Masukkan angka yang valid"),
|
||||
autoCloseDuration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
},
|
||||
fontSized: 14.sp,
|
||||
colorText: whiteColor,
|
||||
color: primaryColor,
|
||||
borderRadius: 10.sp,
|
||||
horizontal: double.infinity,
|
||||
vertical: 50,
|
||||
loadingTrue: false,
|
||||
usingRow: false,
|
||||
),
|
||||
button2: CardButtonOne(
|
||||
textButton: "Batal",
|
||||
onTap: () => router.pop(),
|
||||
fontSized: 14.sp,
|
||||
colorText: primaryColor,
|
||||
color: Colors.transparent,
|
||||
borderRadius: 10,
|
||||
horizontal: double.infinity,
|
||||
vertical: 50,
|
||||
loadingTrue: false,
|
||||
usingRow: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantityControls(String categoryId) {
|
||||
final isItemLoading = _itemLoadingStates[categoryId] ?? false;
|
||||
|
||||
return Consumer<CartViewModel>(
|
||||
builder: (context, cartViewModel, child) {
|
||||
final amount = cartViewModel.getItemAmount(categoryId);
|
||||
|
||||
if (amount > 0) {
|
||||
return Row(
|
||||
children: [
|
||||
_buildControlButton(
|
||||
icon: Icons.remove,
|
||||
color: Colors.red,
|
||||
onTap:
|
||||
isItemLoading ? null : () => _decrementQuantity(categoryId),
|
||||
isLoading: isItemLoading,
|
||||
),
|
||||
const Gap(12),
|
||||
_buildQuantityDisplay(
|
||||
categoryId,
|
||||
amount.toDouble(),
|
||||
isItemLoading,
|
||||
),
|
||||
const Gap(12),
|
||||
_buildControlButton(
|
||||
icon: Icons.add,
|
||||
color: primaryColor,
|
||||
onTap:
|
||||
isItemLoading ? null : () => _incrementQuantity(categoryId),
|
||||
isLoading: isItemLoading,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return GestureDetector(
|
||||
onTap: isItemLoading ? null : () => _incrementQuantity(categoryId),
|
||||
child: Container(
|
||||
padding: PaddingCustom().paddingHorizontalVertical(16, 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isItemLoading ? Colors.grey : primaryColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child:
|
||||
isItemLoading
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(whiteColor),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'Tambah',
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControlButton({
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required VoidCallback? onTap,
|
||||
bool isLoading = false,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
onTap != null
|
||||
? color.withValues(alpha: 0.1)
|
||||
: Colors.grey.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child:
|
||||
isLoading
|
||||
? Center(
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
icon,
|
||||
color: onTap != null ? color : Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantityDisplay(
|
||||
String categoryId,
|
||||
double quantity,
|
||||
bool isLoading,
|
||||
) {
|
||||
final trashViewModel = Provider.of<TrashViewModel>(context, listen: false);
|
||||
final category = trashViewModel.getCategoryById(categoryId);
|
||||
|
||||
return GestureDetector(
|
||||
onTap:
|
||||
isLoading
|
||||
? null
|
||||
: () => _showQuantityDialog(categoryId, category?.name ?? ''),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isLoading ? Colors.orange.shade300 : Colors.grey.shade300,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: isLoading ? Colors.orange.shade50 : Colors.transparent,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isLoading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.orange),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${quantity.toString().replaceAll('.0', '')} kg',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isLoading ? Colors.orange.shade700 : Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTrashItem(dynamic category) {
|
||||
final String? baseUrl = dotenv.env["BASE_URL"];
|
||||
|
||||
return Consumer<CartViewModel>(
|
||||
builder: (context, cartViewModel, child) {
|
||||
final amount = cartViewModel.getItemAmount(category.id);
|
||||
num price =
|
||||
(category.price is String)
|
||||
? int.tryParse(category.price) ?? 0
|
||||
: category.price ?? 0;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: PaddingCustom().paddingAll(16),
|
||||
decoration: BoxDecoration(
|
||||
color: whiteColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border:
|
||||
amount > 0
|
||||
? Border.all(
|
||||
color: primaryColor.withValues(alpha: 0.3),
|
||||
width: 1.5,
|
||||
)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color:
|
||||
amount > 0
|
||||
? primaryColor.withValues(alpha: 0.1)
|
||||
: Colors.grey.withValues(alpha: 0.1),
|
||||
spreadRadius: amount > 0 ? 2 : 1,
|
||||
blurRadius: amount > 0 ? 6 : 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildCategoryIcon(baseUrl, category),
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: _buildCategoryInfo(category, price, amount.toDouble()),
|
||||
),
|
||||
_buildQuantityControls(category.id),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryIcon(String? baseUrl, dynamic category) {
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
child: Image.network(
|
||||
'$baseUrl${category.icon}',
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(Icons.category, color: Colors.grey, size: 24);
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
value:
|
||||
loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryInfo(dynamic category, num price, double quantity) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
category.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'Rp ${price.toString()}/kg',
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
SizedBox(
|
||||
height: 24,
|
||||
child:
|
||||
quantity > 0
|
||||
? GestureDetector(
|
||||
onTap: () => _resetQuantity(category.id),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: PaddingCustom().paddingAll(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomSheet() {
|
||||
return Consumer<CartViewModel>(
|
||||
builder: (context, cartViewModel, child) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: PaddingCustom().paddingAll(16),
|
||||
decoration: BoxDecoration(
|
||||
color: whiteColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: _buildOrderSummary(cartViewModel)),
|
||||
const Gap(16),
|
||||
_buildContinueButton(cartViewModel),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOrderSummary(CartViewModel cartViewModel) {
|
||||
final meetsMinimumWeight = cartViewModel.totalItems >= 3;
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Column(
|
||||
key: ValueKey(cartViewModel.totalItems),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${cartViewModel.cartItems.length} jenis ${cartViewModel.totalItems.toString()} kg',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade700),
|
||||
),
|
||||
const Gap(10),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Est. ',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade700),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
cartViewModel.formattedTotalPrice,
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!meetsMinimumWeight) ...[
|
||||
const Gap(10),
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
opacity: meetsMinimumWeight ? 0.0 : 1.0,
|
||||
child: const Text(
|
||||
'Minimum total berat 3kg',
|
||||
style: TextStyle(fontSize: 12, color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContinueButton(CartViewModel cartViewModel) {
|
||||
final meetsMinimumWeight = cartViewModel.totalItems >= 3;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
meetsMinimumWeight ? () => _handleContinue(cartViewModel) : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: meetsMinimumWeight ? Colors.blue : Colors.grey,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: Text(
|
||||
'Lanjut',
|
||||
style: TextStyle(
|
||||
color: whiteColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleContinue(CartViewModel cartViewModel) {
|
||||
final trashViewModel = Provider.of<TrashViewModel>(context, listen: false);
|
||||
|
||||
Map<String, dynamic> orderData = {
|
||||
'selectedItems':
|
||||
cartViewModel.cartItems.map((cartItem) {
|
||||
trashViewModel.getCategoryById(cartItem.trashId);
|
||||
return {
|
||||
'id': cartItem.trashId,
|
||||
'name': cartItem.trashName,
|
||||
'quantity': cartItem.amount.toDouble(),
|
||||
'pricePerKg': cartItem.trashPrice,
|
||||
'totalPrice': cartItem.subtotalEstimatedPrice.round(),
|
||||
};
|
||||
}).toList(),
|
||||
'totalWeight': cartViewModel.totalItems.toDouble(),
|
||||
'totalPrice': cartViewModel.totalPrice.round(),
|
||||
'totalItems': cartViewModel.cartItems.length,
|
||||
};
|
||||
|
||||
context.push('/ordersumary', extra: orderData);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: whiteColor,
|
||||
appBar: const CustomAppBar(judul: "Pilih Sampah"),
|
||||
body: Consumer2<TrashViewModel, CartViewModel>(
|
||||
builder: (context, trashViewModel, cartViewModel, child) {
|
||||
if (trashViewModel.isLoading) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) => SkeletonCard(),
|
||||
);
|
||||
}
|
||||
|
||||
if (trashViewModel.hasError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 64, color: Colors.red),
|
||||
const Gap(16),
|
||||
const Text(
|
||||
'Terjadi kesalahan',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const Gap(8),
|
||||
Padding(
|
||||
padding: PaddingCustom().paddingHorizontal(32),
|
||||
child: Text(
|
||||
trashViewModel.errorMessage ?? 'Error tidak diketahui',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
ElevatedButton(
|
||||
onPressed: () => _initializeData(),
|
||||
child: const Text('Coba Lagi'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!trashViewModel.hasData || trashViewModel.categories.isEmpty) {
|
||||
return InfoStateWidget(type: InfoStateType.emptyData);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
ListView.builder(
|
||||
padding: PaddingCustom().paddingOnly(
|
||||
top: 16,
|
||||
right: 16,
|
||||
bottom: cartViewModel.isNotEmpty ? 120 : 16,
|
||||
left: 16,
|
||||
),
|
||||
itemCount: trashViewModel.categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = trashViewModel.categories[index];
|
||||
return _buildTrashItem(category);
|
||||
},
|
||||
),
|
||||
// Bottom Sheet with Animation
|
||||
AnimatedBuilder(
|
||||
animation: _slideAnimation,
|
||||
builder: (context, child) {
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom:
|
||||
cartViewModel.isNotEmpty
|
||||
? (_slideAnimation.value - 1) * 200
|
||||
: -200,
|
||||
child:
|
||||
cartViewModel.isNotEmpty
|
||||
? _buildBottomSheet()
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue