From 918e3c0ced16df326c0dcb8aed19a64245625f99 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 29 May 2025 05:28:09 +0700 Subject: [PATCH] feat: feature cart and integrate with trash data us u can see --- lib/core/container/export_vmod.dart | 8 +- lib/core/container/injection_container.dart | 37 +- lib/core/network/network_aware_widgets.dart | 19 +- lib/core/router.dart | 6 - lib/core/utils/exportimportview.dart | 7 +- lib/core/utils/navigation.dart | 9 +- lib/features/cart/model/cartitem_model.dart | 89 -- lib/features/cart/model/trashcart_model.dart | 183 ++++ .../presentation/screens/cart_screen.dart | 466 --------- .../screens/cart_test_screen.dart | 911 ++++++++++++++++++ .../presentation/viewmodel/cartitem_vmod.dart | 85 -- .../viewmodel/trashcart_vmod.dart | 278 ++++++ .../cart/repositories/cartitem_repo.dart | 55 -- .../cart/repositories/trashcart_repo.dart | 189 ++++ .../cart/service/cartitem_service.dart | 35 - .../cart/service/trashcart_service.dart | 278 ++++++ .../components/cart_test_screen.dart | 721 -------------- .../components/trash_testview.dart | 500 ---------- .../home/presentation/screen/home_screen.dart | 76 +- .../profil/components/secure_pin_input.dart | 140 ++- .../presentation/screen/profil_screen.dart | 30 +- .../screen/requestpickup_screen.dart | 172 ---- .../presentation/screen/trash_testview.dart | 723 ++++++++++++++ lib/globaldata/trash/trash_model.dart | 76 +- lib/globaldata/trash/trash_repository.dart | 15 +- lib/globaldata/trash/trash_service.dart | 31 +- lib/globaldata/trash/trash_viewmodel.dart | 87 +- lib/widget/showmodal.dart | 242 +++-- screen/trash_testview.dart | 726 ++++++++++++++ 29 files changed, 3792 insertions(+), 2402 deletions(-) delete mode 100644 lib/features/cart/model/cartitem_model.dart create mode 100644 lib/features/cart/model/trashcart_model.dart delete mode 100644 lib/features/cart/presentation/screens/cart_screen.dart create mode 100644 lib/features/cart/presentation/screens/cart_test_screen.dart delete mode 100644 lib/features/cart/presentation/viewmodel/cartitem_vmod.dart create mode 100644 lib/features/cart/presentation/viewmodel/trashcart_vmod.dart delete mode 100644 lib/features/cart/repositories/cartitem_repo.dart create mode 100644 lib/features/cart/repositories/trashcart_repo.dart delete mode 100644 lib/features/cart/service/cartitem_service.dart create mode 100644 lib/features/cart/service/trashcart_service.dart delete mode 100644 lib/features/home/presentation/components/cart_test_screen.dart delete mode 100644 lib/features/home/presentation/components/trash_testview.dart delete mode 100644 lib/features/requestpick/presentation/screen/requestpickup_screen.dart create mode 100644 lib/features/requestpick/presentation/screen/trash_testview.dart create mode 100644 screen/trash_testview.dart diff --git a/lib/core/container/export_vmod.dart b/lib/core/container/export_vmod.dart index 8fea4c5..7a56366 100644 --- a/lib/core/container/export_vmod.dart +++ b/lib/core/container/export_vmod.dart @@ -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'; diff --git a/lib/core/container/injection_container.dart b/lib/core/container/injection_container.dart index 50b6ee3..4fca749 100644 --- a/lib/core/container/injection_container.dart +++ b/lib/core/container/injection_container.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( + () => TrashCategoryRepository(), ); + + sl.registerLazySingleton( + () => TrashCategoryService(sl()), + ); + + sl.registerFactory( + () => TrashViewModel(sl()), + ); + 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( + () => CartRepositoryImpl(), +); + +sl.registerLazySingleton( + () => CartServiceImpl(repository: sl()), +); + +sl.registerFactory( + () => CartViewModel(cartService: sl()), +); } diff --git a/lib/core/network/network_aware_widgets.dart b/lib/core/network/network_aware_widgets.dart index 6e63e73..06880c4 100644 --- a/lib/core/network/network_aware_widgets.dart +++ b/lib/core/network/network_aware_widgets.dart @@ -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, diff --git a/lib/core/router.dart b/lib/core/router.dart index ecff919..e5f3411 100644 --- a/lib/core/router.dart +++ b/lib/core/router.dart @@ -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( diff --git a/lib/core/utils/exportimportview.dart b/lib/core/utils/exportimportview.dart index 12843db..235f901 100644 --- a/lib/core/utils/exportimportview.dart +++ b/lib/core/utils/exportimportview.dart @@ -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'; \ No newline at end of file +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'; \ No newline at end of file diff --git a/lib/core/utils/navigation.dart b/lib/core/utils/navigation.dart index f7d5e55..c59483a 100644 --- a/lib/core/utils/navigation.dart +++ b/lib/core/utils/navigation.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 HomeScreen(), ActivityScreen(), Text(""), - CartScreen(), + // CartScreen(), + OrderSummaryScreen(), ProfilScreen(), ], ), @@ -163,7 +165,8 @@ class _NavigationPageState extends State 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( diff --git a/lib/features/cart/model/cartitem_model.dart b/lib/features/cart/model/cartitem_model.dart deleted file mode 100644 index 774f753..0000000 --- a/lib/features/cart/model/cartitem_model.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:convert'; - -class CartItem { - final String trashId; - final double amount; - - CartItem({required this.trashId, required this.amount}); - - Map toJson() => {'trashid': trashId, 'amount': amount}; - - factory CartItem.fromJson(Map json) { - return CartItem( - trashId: json['trashid'], - amount: (json['amount'] as num).toDouble(), - ); - } - - static String encodeList(List items) => - jsonEncode(items.map((e) => e.toJson()).toList()); - - static List decodeList(String source) => - (jsonDecode(source) as List) - .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 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 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 json) { - var items = - (json['cartitems'] as List) - .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, - ); - } -} diff --git a/lib/features/cart/model/trashcart_model.dart b/lib/features/cart/model/trashcart_model.dart new file mode 100644 index 0000000..59c3bb8 --- /dev/null +++ b/lib/features/cart/model/trashcart_model.dart @@ -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 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 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 cartItems; + + Cart({ + required this.id, + required this.userId, + required this.totalAmount, + required this.estimatedTotalPrice, + required this.cartItems, + }); + + factory Cart.fromJson(Map 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?) + ?.map((item) => CartItem.fromJson(item)) + .toList() ?? + [], + ); + } + + Map 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? 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 toJson() { + return {'trash_id': trashId, 'amount': amount}; + } + + @override + String toString() { + return 'AddOrUpdateCartRequest(trashId: $trashId, amount: $amount)'; + } +} + +class CartApiResponse { + final int status; + final String message; + final T? data; + + CartApiResponse({required this.status, required this.message, this.data}); + + factory CartApiResponse.fromJson( + Map json, + T Function(Map)? fromJsonT, + ) { + return CartApiResponse( + 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; +} diff --git a/lib/features/cart/presentation/screens/cart_screen.dart b/lib/features/cart/presentation/screens/cart_screen.dart deleted file mode 100644 index 63a1fcd..0000000 --- a/lib/features/cart/presentation/screens/cart_screen.dart +++ /dev/null @@ -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 createState() => _CartScreenState(); -} - -class _CartScreenState extends State 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(context, listen: false); - vmod.flushCartToServer(); - } - } - - Future fetchCart() async { - final vmod = Provider.of(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(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( - 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( - 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( - 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( - // 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), - ], - ), - ), - ), - ); - } -} diff --git a/lib/features/cart/presentation/screens/cart_test_screen.dart b/lib/features/cart/presentation/screens/cart_test_screen.dart new file mode 100644 index 0000000..fb82d00 --- /dev/null +++ b/lib/features/cart/presentation/screens/cart_test_screen.dart @@ -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 createState() => _OrderSummaryScreenState(); +} + +class _OrderSummaryScreenState extends State + 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.refresh(); + } + } + + void _initializeCart() async { + if (_isInitialized) return; + _cartViewModel = context.read(); + + await _cartViewModel.loadCartItems(showLoading: false); + + if (mounted) { + setState(() { + _isInitialized = true; + }); + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + if (_isInitialized) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _refreshCartData(); + }); + } + } + + Future _removeItem(String trashId, String itemName) async { + final success = await _cartViewModel.deleteItem(trashId); + if (success) { + _showSnackbar('$itemName berhasil dihapus'); + } else { + _showSnackbar(_cartViewModel.errorMessage); + } + } + + Future _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 _incrementQuantity(String trashId) async { + await _cartViewModel.incrementItemAmount(trashId); + if (_cartViewModel.state == CartState.error) { + _showSnackbar(_cartViewModel.errorMessage); + } + } + + Future _decrementQuantity(String trashId) async { + await _cartViewModel.decrementItemAmount(trashId); + if (_cartViewModel.state == CartState.error) { + _showSnackbar(_cartViewModel.errorMessage); + } + } + + Future _showQuantityDialog(CartItem item) async { + final TextEditingController controller = TextEditingController( + text: item.amount.toString(), + ); + + final result = await showDialog( + 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 _showConfirmationDialog({ + required String title, + required String content, + required String confirmText, + }) { + return showDialog( + 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 _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( + 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( + icon: Icon(Icons.more_vert, color: Colors.black), + onSelected: (value) { + if (value == 'clear_all') { + _clearAllItems(); + } + }, + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + 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( + 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( + 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 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( + 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( + 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]}.', + ); + } +} \ No newline at end of file diff --git a/lib/features/cart/presentation/viewmodel/cartitem_vmod.dart b/lib/features/cart/presentation/viewmodel/cartitem_vmod.dart deleted file mode 100644 index 06cad83..0000000 --- a/lib/features/cart/presentation/viewmodel/cartitem_vmod.dart +++ /dev/null @@ -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 _cartItems = []; - - List get cartItems => _cartItems; - - bool _isLoading = false; - bool get isLoading => _isLoading; - - Future 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 clearLocalCart() async { - _cartItems.clear(); - await _repository.clearLocalCart(); - notifyListeners(); - } - - Future flushCartToServer() async { - if (_cartItems.isEmpty) return; - - _isLoading = true; - notifyListeners(); - - await _repository.flushCartToServer(); - await clearLocalCart(); - - _isLoading = false; - notifyListeners(); - } - - Future fetchCartFromServer() async { - try { - return await _repository.getCartFromServer(); - } catch (e) { - debugPrint("Error fetching cart: $e"); - return null; - } - } - - Future commitCart() async { - await _repository.commitCart(); - } - - Future refreshTTL() async { - await _repository.refreshCartTTL(); - } - - Future deleteItemFromServer(String trashId) async { - await _repository.deleteCartItemFromServer(trashId); - } - - Future clearCartFromServer() async { - await _repository.clearServerCart(); - } -} diff --git a/lib/features/cart/presentation/viewmodel/trashcart_vmod.dart b/lib/features/cart/presentation/viewmodel/trashcart_vmod.dart new file mode 100644 index 0000000..1eb8aac --- /dev/null +++ b/lib/features/cart/presentation/viewmodel/trashcart_vmod.dart @@ -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 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 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 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 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 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 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 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 refresh() async { + await loadCartItems(showLoading: true); + } + +// Future 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]}.')}'; + } +} diff --git a/lib/features/cart/repositories/cartitem_repo.dart b/lib/features/cart/repositories/cartitem_repo.dart deleted file mode 100644 index 640e184..0000000 --- a/lib/features/cart/repositories/cartitem_repo.dart +++ /dev/null @@ -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> getLocalCart() async { - final prefs = await SharedPreferences.getInstance(); - final raw = prefs.getString(_localCartKey); - if (raw == null || raw.isEmpty) return []; - - return CartItem.decodeList(raw); - } - - Future saveLocalCart(List items) async { - final prefs = await SharedPreferences.getInstance(); - final encoded = CartItem.encodeList(items); - await prefs.setString(_localCartKey, encoded); - } - - Future clearLocalCart() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.remove(_localCartKey); - } - - Future flushCartToServer() async { - final items = await getLocalCart(); - if (items.isEmpty) return; - - await _cartService.postCart(items); - await clearLocalCart(); - } - - Future getCartFromServer() async { - return await _cartService.getCart(); - } - - Future commitCart() async { - await _cartService.commitCart(); - } - - Future clearServerCart() async { - await _cartService.clearCart(); - } - - Future deleteCartItemFromServer(String trashId) async { - await _cartService.deleteCartItem(trashId); - } - - Future refreshCartTTL() async { - await _cartService.refreshCartTTL(); - } -} diff --git a/lib/features/cart/repositories/trashcart_repo.dart b/lib/features/cart/repositories/trashcart_repo.dart new file mode 100644 index 0000000..c72a4e5 --- /dev/null +++ b/lib/features/cart/repositories/trashcart_repo.dart @@ -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> addOrUpdateCartItem( + AddOrUpdateCartRequest request, + ); + Future getCartItems(); + Future> deleteCartItem(String trashId); + Future> 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> addOrUpdateCartItem( + AddOrUpdateCartRequest request, + ) async { + try { + final response = await _https.post( + _cartItemEndpoint, + body: request.toJson(), + ); + + return CartApiResponse.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 getCartItems() async { + try { + final response = await _https.get(_cartEndpoint); + + final cartResponse = CartApiResponse.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> deleteCartItem(String trashId) async { + try { + final response = await _https.delete('$_cartItemEndpoint/$trashId'); + + return CartApiResponse.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> clearCart() async { + try { + final response = await _https.delete(_cartClearEndpoint); + + return CartApiResponse.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 _mockCartItems = []; + + @override + Future> 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( + status: 200, + message: 'Berhasil menambah/mengubah item keranjang', + ); + } + + @override + Future getCartItems() async { + await Future.delayed(const Duration(milliseconds: 300)); + + final totalAmount = _mockCartItems.fold( + 0, + (sum, item) => sum + item.amount, + ); + + final estimatedTotalPrice = _mockCartItems.fold( + 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> deleteCartItem(String trashId) async { + await Future.delayed(const Duration(milliseconds: 300)); + + _mockCartItems.removeWhere((item) => item.trashId == trashId); + + return CartApiResponse( + status: 200, + message: 'Berhasil menghapus item dari keranjang', + ); + } + + @override + Future> clearCart() async { + await Future.delayed(const Duration(milliseconds: 300)); + + _mockCartItems.clear(); + + return CartApiResponse( + status: 200, + message: 'Berhasil mengosongkan keranjang', + ); + } +} diff --git a/lib/features/cart/service/cartitem_service.dart b/lib/features/cart/service/cartitem_service.dart deleted file mode 100644 index df51144..0000000 --- a/lib/features/cart/service/cartitem_service.dart +++ /dev/null @@ -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 postCart(List items) async { - final body = {"items": items.map((e) => e.toJson()).toList()}; - - await _https.post("/cart", body: body); - } - - Future getCart() async { - final response = await _https.get("/cart"); - debugPrint(response); - return CartResponse.fromJson(response['data']); - } - - Future deleteCartItem(String trashId) async { - await _https.delete("/cart/$trashId"); - } - - Future clearCart() async { - await _https.delete("/cart"); - } - - Future refreshCartTTL() async { - await _https.put("/cart/refresh"); - } - - Future commitCart() async { - await _https.post("/cart/commit"); - } -} diff --git a/lib/features/cart/service/trashcart_service.dart b/lib/features/cart/service/trashcart_service.dart new file mode 100644 index 0000000..f3d8df9 --- /dev/null +++ b/lib/features/cart/service/trashcart_service.dart @@ -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 addOrUpdateItem(String trashId, int amount); + Future getCartItems(); + Future deleteItem(String trashId); + Future clearCart(); + Future incrementItemAmount(String trashId); + Future decrementItemAmount(String trashId); +} + +class CartServiceImpl implements CartService { + final CartRepository _repository; + + CartServiceImpl({CartRepository? repository}) + : _repository = repository ?? CartRepositoryImpl(); + + @override + Future 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 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 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 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 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 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; + } +} diff --git a/lib/features/home/presentation/components/cart_test_screen.dart b/lib/features/home/presentation/components/cart_test_screen.dart deleted file mode 100644 index d0fb7c5..0000000 --- a/lib/features/home/presentation/components/cart_test_screen.dart +++ /dev/null @@ -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 createState() => _OrderSummaryScreenState(); -} - -class _OrderSummaryScreenState extends State { - List> 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( - 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( - icon: Icon(Icons.more_vert, color: Colors.black), - onSelected: (value) { - if (value == 'clear_all') { - _clearAllItems(); - } - }, - itemBuilder: - (BuildContext context) => [ - PopupMenuItem( - 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 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 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 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 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]}.', - ); - } -} diff --git a/lib/features/home/presentation/components/trash_testview.dart b/lib/features/home/presentation/components/trash_testview.dart deleted file mode 100644 index 9fb7d8b..0000000 --- a/lib/features/home/presentation/components/trash_testview.dart +++ /dev/null @@ -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 createState() => _TestRequestPickScreenState(); -} - -class _TestRequestPickScreenState extends State { - // Map untuk menyimpan quantity setiap item - Map 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 prices = { - 'Plastik': 1000, - 'Kertas Campur': 700, - 'Kaca': 300, - 'Minyak Jelantah': 2500, - 'Kaleng Alumunium': 3500, - }; - - // Map untuk menyimpan icon data - Map 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 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 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, - ), - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/features/home/presentation/screen/home_screen.dart b/lib/features/home/presentation/screen/home_screen.dart index b5ce7df..412a8f6 100644 --- a/lib/features/home/presentation/screen/home_screen.dart +++ b/lib/features/home/presentation/screen/home_screen.dart @@ -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 { 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 { 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, diff --git a/lib/features/profil/components/secure_pin_input.dart b/lib/features/profil/components/secure_pin_input.dart index f826bf1..8a5c89c 100644 --- a/lib/features/profil/components/secure_pin_input.dart +++ b/lib/features/profil/components/secure_pin_input.dart @@ -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 { @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 { 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 { _buildKeypadButton('3'), ], ), - const SizedBox(height: 20), - + Gap(20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -153,8 +140,7 @@ class _SecurityCodeScreenState extends State { _buildKeypadButton('6'), ], ), - const SizedBox(height: 20), - + Gap(20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -163,15 +149,10 @@ class _SecurityCodeScreenState extends State { _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 { 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 { 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), ), ), ); diff --git a/lib/features/profil/presentation/screen/profil_screen.dart b/lib/features/profil/presentation/screen/profil_screen.dart index 2b9eaec..15dc13f 100644 --- a/lib/features/profil/presentation/screen/profil_screen.dart +++ b/lib/features/profil/presentation/screen/profil_screen.dart @@ -48,19 +48,6 @@ class _ProfilScreenState extends State { ), ), ), - // 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 { ), ], ), - if (showDivider) ...[ Gap(20), Divider(height: 1, color: Colors.grey.shade200), @@ -247,7 +233,8 @@ class _ProfilScreenState extends State { break; case 'Ubah Pin': - debugPrint('Uabh Pin'); + debugPrint('Ubah Pin'); + router.push('/pinsecureinput'); break; case 'Alamat': @@ -263,7 +250,6 @@ class _ProfilScreenState extends State { break; case 'Keluar': - // _showLogoutDialog(); CustomBottomSheet.show( context: context, title: "Logout Sekarang?", @@ -275,18 +261,6 @@ class _ProfilScreenState extends State { ], ), 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(), diff --git a/lib/features/requestpick/presentation/screen/requestpickup_screen.dart b/lib/features/requestpick/presentation/screen/requestpickup_screen.dart deleted file mode 100644 index 3304d3c..0000000 --- a/lib/features/requestpick/presentation/screen/requestpickup_screen.dart +++ /dev/null @@ -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 { - bool isCartLoaded = false; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) async { - final trashVM = Provider.of(context, listen: false); - final cartVM = Provider.of(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( - 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( - 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, - ), - ), - ], - ), - ), - ); - }, - ); - }, - ), - ), - ); - } -} diff --git a/lib/features/requestpick/presentation/screen/trash_testview.dart b/lib/features/requestpick/presentation/screen/trash_testview.dart new file mode 100644 index 0000000..8d4aaf7 --- /dev/null +++ b/lib/features/requestpick/presentation/screen/trash_testview.dart @@ -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 createState() => _TestRequestPickScreenState(); +} + +class _TestRequestPickScreenState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _slideAnimation; + bool _isInitialized = false; + + final Map _itemLoadingStates = {}; + + @override + void initState() { + super.initState(); + + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _slideAnimation = Tween(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 _initializeData() async { + if (_isInitialized) return; + + final trashViewModel = Provider.of(context, listen: false); + final cartViewModel = Provider.of(context, listen: false); + + await Future.wait([ + trashViewModel.loadCategories(), + cartViewModel.loadCartItems(showLoading: false), + ]); + + if (mounted) { + _updateBottomSheetVisibility(); + setState(() { + _isInitialized = true; + }); + } + } + + void _updateBottomSheetVisibility() { + final cartViewModel = Provider.of(context, listen: false); + + if (cartViewModel.isNotEmpty) { + _animationController.forward(); + } else { + _animationController.reverse(); + } + } + + Future _handleQuantityChange( + String categoryId, + double newQuantity, + ) async { + setState(() { + _itemLoadingStates[categoryId] = true; + }); + + try { + final cartViewModel = Provider.of(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 _incrementQuantity(String categoryId) async { + final cartViewModel = Provider.of(context, listen: false); + final currentAmount = cartViewModel.getItemAmount(categoryId); + await _handleQuantityChange(categoryId, (currentAmount + 2.5)); + } + + Future _decrementQuantity(String categoryId) async { + final cartViewModel = Provider.of(context, listen: false); + final currentAmount = cartViewModel.getItemAmount(categoryId); + final newAmount = (currentAmount - 2.5).clamp(0.0, double.infinity); + await _handleQuantityChange(categoryId, newAmount); + } + + Future _resetQuantity(String categoryId) async { + await _handleQuantityChange(categoryId, 0); + } + + void _showQuantityDialog(String categoryId, String itemName) { + final cartViewModel = Provider.of(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( + 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(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), + ), + ), + ) + : Icon( + icon, + color: onTap != null ? color : Colors.grey, + size: 20, + ), + ), + ); + } + + Widget _buildQuantityDisplay( + String categoryId, + double quantity, + bool isLoading, + ) { + final trashViewModel = Provider.of(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(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( + 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( + 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(context, listen: false); + + Map 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( + 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(), + ); + }, + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/globaldata/trash/trash_model.dart b/lib/globaldata/trash/trash_model.dart index 90c7dee..a344b93 100644 --- a/lib/globaldata/trash/trash_model.dart +++ b/lib/globaldata/trash/trash_model.dart @@ -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 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 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 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? ?? {}; + + return TrashCategoryResponse( + categories: + dataList + .map((e) => Category.fromJson(e as Map)) + .toList(), + message: meta['message'] as String? ?? '', + total: meta['total'] as int? ?? 0, + ); + } catch (e) { + throw FormatException('Failed to parse TrashCategoryResponse: $e'); + } + } + + Map 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)'; } } diff --git a/lib/globaldata/trash/trash_repository.dart b/lib/globaldata/trash/trash_repository.dart index 7d8c890..8a42c0a 100644 --- a/lib/globaldata/trash/trash_repository.dart +++ b/lib/globaldata/trash/trash_repository.dart @@ -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 fetchCategories(); +} + +class TrashCategoryRepository implements ITrashCategoryRepository { final Https _https = Https(); + @override Future 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; + } } } diff --git a/lib/globaldata/trash/trash_service.dart b/lib/globaldata/trash/trash_service.dart index f2f6a24..821382b 100644 --- a/lib/globaldata/trash/trash_service.dart +++ b/lib/globaldata/trash/trash_service.dart @@ -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 getCategories(); +} - TrashCategoryService(this._trashCategoryRepository); +class TrashCategoryService implements ITrashCategoryService { + final ITrashCategoryRepository _repository; + TrashCategoryService(this._repository); + + @override Future 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)'; +} diff --git a/lib/globaldata/trash/trash_viewmodel.dart b/lib/globaldata/trash/trash_viewmodel.dart index b7f5265..8b3bfe6 100644 --- a/lib/globaldata/trash/trash_viewmodel.dart +++ b/lib/globaldata/trash/trash_viewmodel.dart @@ -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 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 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 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 searchCategories(String query) { + if (query.isEmpty) return categories; + + return categories + .where( + (category) => + category.name.toLowerCase().contains(query.toLowerCase()), + ) + .toList(); + } } diff --git a/lib/widget/showmodal.dart b/lib/widget/showmodal.dart index 9a409ed..97a4f27 100644 --- a/lib/widget/showmodal.dart +++ b/lib/widget/showmodal.dart @@ -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)), + ), ], ); } -} \ No newline at end of file + + 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), + ), + ), + ); + } +} diff --git a/screen/trash_testview.dart b/screen/trash_testview.dart new file mode 100644 index 0000000..e815f2d --- /dev/null +++ b/screen/trash_testview.dart @@ -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 createState() => _TestRequestPickScreenState(); +} + +class _TestRequestPickScreenState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _slideAnimation; + bool _isInitialized = false; + + // Track loading states per item + final Map _itemLoadingStates = {}; + + @override + void initState() { + super.initState(); + + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _slideAnimation = Tween(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 _initializeData() async { + if (_isInitialized) return; + + final trashViewModel = Provider.of(context, listen: false); + final cartViewModel = Provider.of(context, listen: false); + + await Future.wait([ + trashViewModel.loadCategories(), + cartViewModel.loadCartItems(showLoading: false), + ]); + + if (mounted) { + _updateBottomSheetVisibility(); + setState(() { + _isInitialized = true; + }); + } + } + + void _updateBottomSheetVisibility() { + final cartViewModel = Provider.of(context, listen: false); + + if (cartViewModel.isNotEmpty) { + _animationController.forward(); + } else { + _animationController.reverse(); + } + } + + Future _handleQuantityChange( + String categoryId, + double newQuantity, + ) async { + // Set loading state + setState(() { + _itemLoadingStates[categoryId] = true; + }); + + try { + final cartViewModel = Provider.of(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 _incrementQuantity(String categoryId) async { + final cartViewModel = Provider.of(context, listen: false); + final currentAmount = cartViewModel.getItemAmount(categoryId); + await _handleQuantityChange(categoryId, (currentAmount + 2.5)); + } + + Future _decrementQuantity(String categoryId) async { + final cartViewModel = Provider.of(context, listen: false); + final currentAmount = cartViewModel.getItemAmount(categoryId); + final newAmount = (currentAmount - 2.5).clamp(0.0, double.infinity); + await _handleQuantityChange(categoryId, newAmount); + } + + Future _resetQuantity(String categoryId) async { + await _handleQuantityChange(categoryId, 0); + } + + void _showQuantityDialog(String categoryId, String itemName) { + final cartViewModel = Provider.of(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( + 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(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), + ), + ), + ) + : Icon( + icon, + color: onTap != null ? color : Colors.grey, + size: 20, + ), + ), + ); + } + + Widget _buildQuantityDisplay( + String categoryId, + double quantity, + bool isLoading, + ) { + final trashViewModel = Provider.of(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(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( + 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( + 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(context, listen: false); + + Map 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( + 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(), + ); + }, + ), + ], + ); + }, + ), + ); + } +}