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