1845 lines
61 KiB
Dart
1845 lines
61 KiB
Dart
import 'dart:io';
|
|
import 'dart:math';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:latlong2/latlong.dart';
|
|
import 'package:niogu_ecommerce_v1/core/constant/app_asset.dart';
|
|
import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart';
|
|
import 'package:niogu_ecommerce_v1/core/enums/action_type.dart';
|
|
import 'package:niogu_ecommerce_v1/core/enums/delivery_fee_type.dart';
|
|
import 'package:niogu_ecommerce_v1/core/enums/delivery_type.dart';
|
|
import 'package:niogu_ecommerce_v1/core/enums/order_status.dart';
|
|
import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart';
|
|
import 'package:niogu_ecommerce_v1/core/models/tenant_payment_method_model.dart';
|
|
import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart';
|
|
import 'package:niogu_ecommerce_v1/core/router/app_route.dart';
|
|
import 'package:niogu_ecommerce_v1/core/system/system_setting.dart';
|
|
import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart';
|
|
import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart';
|
|
import 'package:niogu_ecommerce_v1/core/utils/image_service.dart';
|
|
import 'package:niogu_ecommerce_v1/core/utils/log_message.dart';
|
|
import 'package:niogu_ecommerce_v1/core/utils/time_zone.dart';
|
|
import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart';
|
|
import 'package:niogu_ecommerce_v1/features/account/domain/entities/account.dart';
|
|
import 'package:niogu_ecommerce_v1/features/account/presentation/providers/account_provider.dart';
|
|
import 'package:niogu_ecommerce_v1/features/cart/domain/entities/cart.dart';
|
|
import 'package:niogu_ecommerce_v1/features/cart/presentation/providers/cart_provider.dart';
|
|
import 'package:niogu_ecommerce_v1/features/checkout/domain/entities/checkout.dart';
|
|
import 'package:niogu_ecommerce_v1/features/checkout/presentation/providers/checkout_provider.dart';
|
|
import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart';
|
|
import 'package:sizer/sizer.dart';
|
|
import 'package:niogu_ecommerce_v1/core/constant/app_color.dart';
|
|
|
|
class CheckoutScreen extends ConsumerStatefulWidget {
|
|
const CheckoutScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<CheckoutScreen> createState() => _CheckoutScreenState();
|
|
}
|
|
|
|
class _CheckoutScreenState extends ConsumerState<CheckoutScreen> {
|
|
final _noteController = TextEditingController();
|
|
|
|
DeliveryType _deliveryType = DeliveryType.delivery;
|
|
|
|
String _selectedPaymentMethod = "COD";
|
|
|
|
String? _deliveryPreference;
|
|
|
|
bool _isPaymentExpanded = false;
|
|
|
|
final List<String?> _paymentProofPathTemps = [];
|
|
|
|
final ImagePicker _picker = ImagePicker();
|
|
|
|
String? _paymentProofPath;
|
|
|
|
@override
|
|
void initState() {
|
|
// TODO: implement initState
|
|
super.initState();
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
ref.invalidate(selectedAddressProvider);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// TODO: implement dispose
|
|
_noteController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
double _calculateTotalOrder(List<CartItem> cartItems) {
|
|
return cartItems.fold(0.0, (sum, cartItem) {
|
|
return sum + cartItem.sellingPrice * cartItem.quantity;
|
|
});
|
|
}
|
|
|
|
List<DateTime> _generateAvailableSlots({
|
|
required int onlineOpenTime,
|
|
required int onlineCloseTime,
|
|
required int preparationTimeMinutes,
|
|
required int orderIntervalMinutes,
|
|
}) {
|
|
final slotDuration = Duration(
|
|
minutes: preparationTimeMinutes + orderIntervalMinutes,
|
|
);
|
|
|
|
final now = DateTime.now();
|
|
|
|
final List<DateTime> slots = [];
|
|
|
|
final asap = _getEarliestDeliveryTime(
|
|
onlineOpenTime: onlineOpenTime,
|
|
preparationTimeMinutes: preparationTimeMinutes,
|
|
orderIntervalMinutes: orderIntervalMinutes,
|
|
);
|
|
|
|
final openingTime = DateTime(
|
|
now.year,
|
|
now.month,
|
|
now.day,
|
|
onlineOpenTime,
|
|
0,
|
|
);
|
|
|
|
final closingTime = DateTime(
|
|
now.year,
|
|
now.month,
|
|
now.day,
|
|
onlineCloseTime,
|
|
0,
|
|
);
|
|
|
|
DateTime currentSlot = now.isBefore(openingTime)
|
|
? openingTime
|
|
: now.add(Duration(minutes: preparationTimeMinutes));
|
|
|
|
currentSlot = _roundToNextSlot(
|
|
currentSlot,
|
|
preparationTimeMinutes: preparationTimeMinutes,
|
|
orderIntervalMinutes: orderIntervalMinutes,
|
|
);
|
|
|
|
while (currentSlot.isBefore(closingTime)) {
|
|
final end = currentSlot.add(slotDuration);
|
|
|
|
if (end.isAfter(_getLastAllowedEndTime(now, onlineCloseTime))) break;
|
|
|
|
if (currentSlot.isAfter(asap)) {
|
|
slots.add(currentSlot);
|
|
}
|
|
currentSlot = currentSlot.add(slotDuration);
|
|
}
|
|
|
|
return slots;
|
|
}
|
|
|
|
DateTime _getLastAllowedEndTime(DateTime now, int onlineCloseTime) {
|
|
return DateTime(now.year, now.month, now.day, onlineCloseTime);
|
|
}
|
|
|
|
DateTime _getEarliestDeliveryTime({
|
|
required int onlineOpenTime,
|
|
required int preparationTimeMinutes,
|
|
required int orderIntervalMinutes,
|
|
}) {
|
|
final now = DateTime.now();
|
|
|
|
final openingTime = DateTime(now.year, now.month, now.day, onlineOpenTime);
|
|
|
|
final baseTime = now.isBefore(openingTime) ? openingTime : now;
|
|
|
|
final earliest = baseTime.add(Duration(minutes: preparationTimeMinutes));
|
|
|
|
return _roundToNextSlot(
|
|
earliest,
|
|
preparationTimeMinutes: preparationTimeMinutes,
|
|
orderIntervalMinutes: orderIntervalMinutes,
|
|
);
|
|
}
|
|
|
|
DateTime _roundToNextSlot(
|
|
DateTime time, {
|
|
required int preparationTimeMinutes,
|
|
required int orderIntervalMinutes,
|
|
}) {
|
|
final slotDuration = Duration(
|
|
minutes: preparationTimeMinutes + orderIntervalMinutes,
|
|
);
|
|
final totalMinutes = time.hour * 60 + time.minute;
|
|
final slotMinutes = slotDuration.inMinutes;
|
|
|
|
final remainder = totalMinutes % slotMinutes;
|
|
if (remainder == 0) return time;
|
|
|
|
final minutesToAdd = slotMinutes - remainder;
|
|
|
|
return time.add(Duration(minutes: minutesToAdd));
|
|
}
|
|
|
|
String _formatRange(DateTime start) {
|
|
final end = start.add(Duration(minutes: 25));
|
|
final timeZone = TimeZone.getCurrentTimeZone();
|
|
return "${DateFormat('HH:mm').format(start)} $timeZone - ${DateFormat('HH:mm').format(end)} $timeZone";
|
|
}
|
|
|
|
Future<void> _showPickerOptions() async {
|
|
final bool isTablet = 100.w >= 600;
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)),
|
|
),
|
|
constraints: const BoxConstraints(maxWidth: double.infinity),
|
|
builder: (BuildContext context) {
|
|
return SafeArea(
|
|
child: Container(
|
|
width: 100.w,
|
|
padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 5.w),
|
|
clipBehavior: Clip.hardEdge,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ListTile(
|
|
leading: Icon(Icons.photo_library, size: 6.w),
|
|
title: Text(
|
|
'Galeri',
|
|
style: TextStyle(
|
|
fontSize: isTablet
|
|
? AppFontSize.medium.sp
|
|
: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
onTap: () async {
|
|
await _getImage(ImageSource.gallery);
|
|
context.pop();
|
|
},
|
|
),
|
|
|
|
SizedBox(height: 2.h),
|
|
|
|
ListTile(
|
|
leading: Icon(Icons.photo_camera, size: 6.w),
|
|
title: Text(
|
|
'Kamera',
|
|
style: TextStyle(
|
|
fontSize: isTablet
|
|
? AppFontSize.medium.sp
|
|
: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
onTap: () async {
|
|
await _getImage(ImageSource.camera);
|
|
context.pop();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _getImage(ImageSource ImageSource) async {
|
|
try {
|
|
final XFile? pickedFile = await _picker.pickImage(
|
|
source: ImageSource,
|
|
imageQuality: 70,
|
|
);
|
|
|
|
if (pickedFile != null) {
|
|
File tempFile = File(pickedFile.path);
|
|
|
|
String? imagePath = await ImageService.saveImageToLocalDirectory(
|
|
tempFile,
|
|
"payment_proof_orders",
|
|
);
|
|
|
|
if (imagePath != null) {
|
|
_paymentProofPathTemps.add(imagePath);
|
|
setState(() {
|
|
_paymentProofPath = imagePath;
|
|
});
|
|
}
|
|
}
|
|
} catch (e, st) {
|
|
LogMessage.log.w(e.toString(), error: e, stackTrace: st);
|
|
CustomSnackbar.showWarning(context, "Akses ditolak");
|
|
}
|
|
}
|
|
|
|
Future<void> _removeImage() async {
|
|
setState(() {
|
|
_paymentProofPath = null;
|
|
});
|
|
}
|
|
|
|
Future<MultipartFile?> _getPaymentProofFile() async {
|
|
if (_paymentProofPath == null) return null;
|
|
|
|
final file = File(_paymentProofPath!);
|
|
|
|
if (!await file.exists()) {
|
|
LogMessage.log.e("File fisik tidak ditemukan: $_paymentProofPath");
|
|
return null;
|
|
}
|
|
|
|
return await MultipartFile.fromFile(_paymentProofPath!);
|
|
}
|
|
|
|
String _fetchPaymentMethodAsset(String method) {
|
|
return switch (method) {
|
|
'dana' => AppAsset.DANA_LOGO,
|
|
'gopay' => AppAsset.GOPAY_LOGO,
|
|
'shopeepay' => AppAsset.SHOPEEPAY_LOGO,
|
|
'bri' => AppAsset.MANDIRI_LOGO,
|
|
'mandiri' => AppAsset.MANDIRI_LOGO,
|
|
'bni' => AppAsset.BNI_LOGO,
|
|
'bca' => AppAsset.BCA_LOGO,
|
|
String() => 'error',
|
|
};
|
|
}
|
|
|
|
double _calculateDistance(
|
|
double lat1,
|
|
double lon1,
|
|
double lat2,
|
|
double lon2,
|
|
) {
|
|
final p = 0.017453292519943295;
|
|
final c = cos;
|
|
final a =
|
|
0.5 -
|
|
c((lat2 - lat1) * p) / 2 +
|
|
c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p)) / 2;
|
|
return 12742 * asin(sqrt(a));
|
|
}
|
|
|
|
Future<void> _checkout({
|
|
String? deliveryFeeType,
|
|
required double deliveryFee,
|
|
required double totalDeliveryFee,
|
|
required double maxDeliveryRadiusKm,
|
|
}) async {
|
|
final currentOutletId = ref.read(currentOutletIdProvider);
|
|
|
|
final currentOutletName = ref.read(currentOutletNameProvider);
|
|
|
|
final currentOutletPhoneNumber = ref.read(currentOutletPhoneProvider);
|
|
|
|
final currentOutletLocation = ref.read(currentOutletLocationProvider);
|
|
|
|
final currentOutletCoordinate = ref.read(currentOutletCoordinateProvider);
|
|
|
|
final currentOutletHasLocation =
|
|
currentOutletLocation != null && currentOutletCoordinate != null;
|
|
|
|
final currentCustomerId = ref.read(currentCustomerIdProvider);
|
|
|
|
final currentCustomerName = ref.read(currentCustomerNameProvider);
|
|
|
|
final currentCustomerPhone = ref.read(currentCustomerPhoneProvider);
|
|
|
|
final selectedAddress = ref.read(selectedAddressProvider);
|
|
|
|
if (_deliveryType == DeliveryType.delivery) {
|
|
if (!currentOutletHasLocation) {
|
|
CustomSnackbar.showError(context, "Metode pengiriman belum tersedia");
|
|
return;
|
|
}
|
|
|
|
if (selectedAddress == null) {
|
|
CustomSnackbar.showError(context, "Alamat pengiriman belum diatur");
|
|
return;
|
|
}
|
|
|
|
final distance = _calculateDistance(
|
|
currentOutletCoordinate.latitude,
|
|
currentOutletCoordinate.longitude,
|
|
selectedAddress.latitude,
|
|
selectedAddress.longitude,
|
|
);
|
|
|
|
if (maxDeliveryRadiusKm > 0 && distance > maxDeliveryRadiusKm) {
|
|
CustomSnackbar.showError(
|
|
context,
|
|
"Jangkauan pengiriman kami maks ${maxDeliveryRadiusKm.toStringWithoutTrailingZero()} Km",
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (_deliveryPreference == null) {
|
|
CustomSnackbar.showError(context, "Waktu pengiriman belum dipilih");
|
|
return;
|
|
}
|
|
|
|
final isPayOnTheSpot =
|
|
((_deliveryType == DeliveryType.delivery &&
|
|
_selectedPaymentMethod == 'COD') ||
|
|
(_deliveryType == DeliveryType.pick_up &&
|
|
_selectedPaymentMethod == 'Bayar Ditoko'));
|
|
|
|
if (!isPayOnTheSpot && _paymentProofPath == null) {
|
|
CustomSnackbar.showError(context, "Bukti pembayaran belum di upload");
|
|
return;
|
|
}
|
|
|
|
final deliveryPreference = _deliveryPreference == "Segera"
|
|
? "(Segera)"
|
|
: "($_deliveryPreference)";
|
|
|
|
final cartItems = ref.watch(cartItemProvider);
|
|
|
|
String? notes;
|
|
|
|
if (_noteController.text.trim().isNotEmpty) {
|
|
notes = _noteController.text.trim();
|
|
}
|
|
|
|
final totalOrder = _calculateTotalOrder(cartItems);
|
|
|
|
final order = OnlineOrder(
|
|
outletId: currentOutletId!,
|
|
outletNameSnapshot: currentOutletName!,
|
|
outletPhoneNumberSnapshot: currentOutletPhoneNumber,
|
|
outletAddressSnapshot: !currentOutletHasLocation
|
|
? null
|
|
: OutletAddressSnapshot(
|
|
fullAddress: currentOutletLocation,
|
|
latitude: currentOutletCoordinate.latitude,
|
|
longitude: currentOutletCoordinate.longitude,
|
|
),
|
|
customerId: currentCustomerId!,
|
|
customerNameSnapshot: currentCustomerName!,
|
|
customerPhoneNumberSnapshot: currentCustomerPhone!,
|
|
customerAddressSnapshot: selectedAddress == null
|
|
? null
|
|
: CustomerAddressSnapshot(
|
|
label: selectedAddress.label!,
|
|
fullAddress: selectedAddress.fullAddress,
|
|
latitude: selectedAddress.latitude,
|
|
longitude: selectedAddress.longitude,
|
|
),
|
|
orderStatus: isPayOnTheSpot
|
|
? OrderStatus.pending_confirmation
|
|
: OrderStatus.pending_payment,
|
|
deliveryType: _deliveryType,
|
|
deliveryPreference: "Hari ini $deliveryPreference",
|
|
deliveryFeeType: deliveryFeeType,
|
|
deliveryFee: deliveryFee,
|
|
totalDeliveryFee: totalDeliveryFee,
|
|
totalOrder: totalOrder,
|
|
totalAmount: totalOrder + totalDeliveryFee,
|
|
notes: notes,
|
|
paymentProofFile: await _getPaymentProofFile(),
|
|
paymentMethod: _selectedPaymentMethod,
|
|
items: cartItems.map((item) {
|
|
return OnlineOrderItem(
|
|
productVariantId: item.id,
|
|
quantity: item.quantity,
|
|
productImageUrlSnapshot: item.image?.toRelativeImagePath(),
|
|
productNameSnapshot: item.name,
|
|
productVariantNameSnapshot: item.isProductVariant
|
|
? item.variantName
|
|
: null,
|
|
sellingPriceSnapshot: item.sellingPrice,
|
|
subtotal: item.quantity * item.sellingPrice,
|
|
);
|
|
}).toList(),
|
|
);
|
|
try {
|
|
final data = await ref
|
|
.read(checkoutControllerProvider.notifier)
|
|
.checkout(order);
|
|
|
|
if (!mounted) return;
|
|
|
|
ref.read(cartItemControllerProvider.notifier).clear();
|
|
|
|
await SystemSetting.saveCartItemByOutlet([]);
|
|
|
|
if (data != null) {
|
|
context.pushNamed(
|
|
AppRoute.customActionScreen,
|
|
extra: {
|
|
'order_id': data.orderId,
|
|
'order_number': data.orderNumber,
|
|
'type': ActionType.checkout,
|
|
},
|
|
);
|
|
}
|
|
} on ServerException catch (e, st) {
|
|
LogMessage.log.e(e.toString(), error: e, stackTrace: st);
|
|
CustomSnackbar.showError(context, e.message);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final currentCustomerName = ref.watch(currentCustomerNameProvider);
|
|
|
|
final currentCustomerPhone = ref.watch(currentCustomerPhoneProvider);
|
|
|
|
final currentOutletName = ref.watch(currentOutletNameProvider);
|
|
|
|
final currentOutletCoordinate = ref.watch(
|
|
currentOutletCoordinateProvider,
|
|
);
|
|
|
|
final selectedAddress = ref.watch(selectedAddressProvider);
|
|
|
|
final cartItems = ref.watch(cartItemProvider);
|
|
|
|
final totalOrder = _calculateTotalOrder(cartItems);
|
|
|
|
final isAddressSelected = selectedAddress != null;
|
|
|
|
final configureState = ref.watch(configurationControllerProvider);
|
|
|
|
final checkoutControllerState = ref.watch(checkoutControllerProvider);
|
|
|
|
final isLoading = checkoutControllerState.isLoading;
|
|
|
|
return SafeArea(
|
|
top: false,
|
|
bottom: true,
|
|
right: false,
|
|
left: false,
|
|
child: Scaffold(
|
|
backgroundColor: const Color(0xFFF5F5F5),
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.white,
|
|
elevation: 0.5,
|
|
leading: IconButton(
|
|
icon: Icon(
|
|
Icons.arrow_back,
|
|
size: 7.w,
|
|
color: AppColor.primaryColor,
|
|
),
|
|
onPressed: () => context.pop(),
|
|
),
|
|
title: Text(
|
|
"Checkout",
|
|
style: TextStyle(
|
|
color: Colors.black,
|
|
fontSize: AppFontSize.medium.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
_buildDeliveryAddressSection(
|
|
customerName: currentCustomerName!,
|
|
customerPhone: currentCustomerPhone!,
|
|
address: selectedAddress,
|
|
),
|
|
|
|
SizedBox(height: 1.25.h),
|
|
|
|
_buildDeliveryTypeSection(),
|
|
|
|
SizedBox(height: 1.25.h),
|
|
|
|
_buildProductSummarySection(
|
|
cartItems,
|
|
currentOutletName!,
|
|
currentOutletCoordinate,
|
|
selectedAddress,
|
|
),
|
|
|
|
SizedBox(height: 1.25.h),
|
|
|
|
configureState.maybeWhen(
|
|
data: (configure) {
|
|
if (configure == null) return const SizedBox();
|
|
|
|
var paymentMethods = configure.paymentMethods;
|
|
|
|
if (configure.allowCod!) {
|
|
paymentMethods = [
|
|
TenantPaymentMethodModel(
|
|
uuid: '',
|
|
sourceName:
|
|
_deliveryType == DeliveryType.delivery
|
|
? 'COD'
|
|
: 'Bayar Ditoko',
|
|
accountNumber: '',
|
|
receipentName: '',
|
|
createdAt: '',
|
|
updatedAt: '',
|
|
),
|
|
...paymentMethods,
|
|
];
|
|
}
|
|
|
|
final timeFormat = DateFormat('HH.mm');
|
|
|
|
final isCloseService =
|
|
configure.isCloseService ?? false;
|
|
|
|
final onlineOpenTime = timeFormat
|
|
.parse(configure.onlineOpenTime!)
|
|
.hour;
|
|
|
|
final onlineCloseTime = timeFormat
|
|
.parse(configure.onlineCloseTime!)
|
|
.hour;
|
|
|
|
final preparationTimeMinutes =
|
|
configure.preparationTimeMinutes!;
|
|
|
|
final orderIntervalMinutes =
|
|
configure.orderIntervalMinutes!;
|
|
|
|
final deliveryFeeType = DeliveryFeeType.values.byName(
|
|
configure.deliveryFeeType!,
|
|
);
|
|
|
|
double deliveryFee = 0.0;
|
|
|
|
double totalDeliveryFee = 0.0;
|
|
|
|
double distance = 1;
|
|
|
|
if (deliveryFeeType == DeliveryFeeType.fixed) {
|
|
deliveryFee = configure.deliveryFlatFee!;
|
|
totalDeliveryFee = deliveryFee;
|
|
} else if (currentOutletCoordinate != null &&
|
|
selectedAddress != null) {
|
|
distance = _calculateDistance(
|
|
currentOutletCoordinate.latitude,
|
|
currentOutletCoordinate.longitude,
|
|
selectedAddress.latitude,
|
|
selectedAddress.longitude,
|
|
);
|
|
deliveryFee = configure.deliveryFeePerKm!;
|
|
totalDeliveryFee = deliveryFee * distance;
|
|
}
|
|
|
|
var strDistance = '${distance.toStringAsFixed(1)} Km';
|
|
|
|
if (distance < 1) {
|
|
strDistance =
|
|
'${(distance * 1000).toStringAsFixed(0)} meter';
|
|
}
|
|
|
|
var totalAmount = totalOrder + totalDeliveryFee;
|
|
|
|
if (_deliveryType == DeliveryType.pick_up)
|
|
totalAmount = totalOrder;
|
|
|
|
return Column(
|
|
children: [
|
|
_buildDeliveryPreferenceSection(
|
|
isCloseService: isCloseService,
|
|
onlineOpenTime: onlineOpenTime,
|
|
onlineCloseTime: onlineCloseTime,
|
|
preparationTimeMinutes: preparationTimeMinutes,
|
|
orderIntervalMinutes: orderIntervalMinutes,
|
|
),
|
|
|
|
SizedBox(height: 1.25.h),
|
|
|
|
_buildPaymentMethodSection(paymentMethods),
|
|
|
|
SizedBox(height: 1.25.h),
|
|
|
|
_buildPaymentDetailSection(
|
|
totalOrder: totalOrder,
|
|
isAddressSelcted: isAddressSelected,
|
|
deliveryFeeType: deliveryFeeType,
|
|
deliveryFee: deliveryFee,
|
|
strDistance: strDistance,
|
|
totalDeliveryFee: totalDeliveryFee,
|
|
totalAmount: totalAmount,
|
|
),
|
|
],
|
|
);
|
|
},
|
|
orElse: () => const SizedBox(),
|
|
),
|
|
|
|
SizedBox(height: 1.25.h),
|
|
|
|
_buildNoteSection(),
|
|
|
|
SizedBox(height: 1.25.h),
|
|
],
|
|
),
|
|
),
|
|
|
|
if (configureState.isRefreshing)
|
|
Container(
|
|
color: Colors.black.withOpacity(0.5),
|
|
child: Center(
|
|
child: const CircularProgressIndicator(
|
|
color: AppColor.primaryColor,
|
|
backgroundColor: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
|
|
if (isLoading)
|
|
Container(
|
|
color: Colors.white54,
|
|
child: Center(
|
|
child: const CircularProgressIndicator(
|
|
color: AppColor.primaryColor,
|
|
backgroundColor: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
bottomNavigationBar: configureState.maybeWhen(
|
|
data: (configure) {
|
|
if (configure == null) return const SizedBox();
|
|
|
|
final deliveryType = DeliveryFeeType.values.byName(
|
|
configure.deliveryFeeType!,
|
|
);
|
|
|
|
double deliveryFee = 0.0;
|
|
|
|
double totalDeliveryFee = 0.0;
|
|
|
|
if (_deliveryType == DeliveryType.delivery) {
|
|
if (deliveryType == DeliveryFeeType.fixed) {
|
|
deliveryFee = configure.deliveryFlatFee!;
|
|
totalDeliveryFee = deliveryFee;
|
|
} else {
|
|
double distance = 1;
|
|
|
|
if (currentOutletCoordinate != null &&
|
|
selectedAddress != null)
|
|
distance = _calculateDistance(
|
|
currentOutletCoordinate.latitude,
|
|
currentOutletCoordinate.longitude,
|
|
selectedAddress.latitude,
|
|
selectedAddress.longitude,
|
|
);
|
|
deliveryFee = configure.deliveryFeePerKm!;
|
|
totalDeliveryFee = deliveryFee * distance;
|
|
}
|
|
}
|
|
|
|
final totalAmount = totalOrder + totalDeliveryFee;
|
|
|
|
final maxDeliveryRadiusKm = configure.maxDeliveryRadiusKm;
|
|
|
|
return _buildBottomCheckoutAction(
|
|
deliveryFeeType: deliveryType.type,
|
|
deliveryFee: deliveryFee,
|
|
isLoading: isLoading,
|
|
totalAmount: totalAmount,
|
|
totalDeliveryFee: totalDeliveryFee,
|
|
maxDeliveryRadiusKm: maxDeliveryRadiusKm ?? 0.0,
|
|
);
|
|
},
|
|
orElse: () => const SizedBox(),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildDeliveryAddressSection({
|
|
required String customerName,
|
|
required String customerPhone,
|
|
SelectedAddress? address,
|
|
}) {
|
|
if (address == null)
|
|
return GestureDetector(
|
|
onTap: () {
|
|
context.pushNamed(AppRoute.shippingAddressScreen);
|
|
},
|
|
child: Container(
|
|
color: Colors.white,
|
|
padding: EdgeInsets.all(4.w),
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(vertical: 2.h, horizontal: 4.w),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.primaryColor.withOpacity(0.05),
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
border: Border.all(
|
|
color: AppColor.primaryColor.withOpacity(0.2),
|
|
style: BorderStyle.solid,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 6.w,
|
|
backgroundColor: Colors.white,
|
|
child: Icon(
|
|
Icons.add_location_alt_outlined,
|
|
color: AppColor.primaryColor,
|
|
size: 7.w,
|
|
),
|
|
),
|
|
SizedBox(width: 4.w),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"Alamat Belum Dipilih",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: (AppFontSize.small - 0.5).sp,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
SizedBox(height: 0.5.h),
|
|
Text(
|
|
"Klik di sini untuk memilih alamat pengiriman",
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.5).sp,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(
|
|
Icons.arrow_forward_ios,
|
|
color: AppColor.primaryColor,
|
|
size: 4.w,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
return Container(
|
|
color: Colors.white,
|
|
padding: EdgeInsets.all(4.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.location_on,
|
|
color: AppColor.primaryColor,
|
|
size: 5.w,
|
|
),
|
|
SizedBox(width: 2.w),
|
|
Text(
|
|
"Alamat Pengiriman",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
GestureDetector(
|
|
onTap: () {
|
|
context.pushNamed(AppRoute.shippingAddressScreen);
|
|
},
|
|
child: Text(
|
|
"Ubah",
|
|
style: TextStyle(
|
|
color: AppColor.primaryColor,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
SizedBox(height: 1.5.h),
|
|
Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 1.h),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade200,
|
|
borderRadius: BorderRadius.circular(1.5.w),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.home, size: 3.5.w, color: Colors.grey.shade500),
|
|
|
|
SizedBox(width: 2.w),
|
|
|
|
Text(
|
|
address.label!,
|
|
style: TextStyle(
|
|
color: Colors.grey.shade700,
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 0.75.h),
|
|
Text(
|
|
"$customerName ($customerPhone)",
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
address.fullAddress,
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
color: Colors.black87,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDeliveryTypeSection() {
|
|
return Container(
|
|
color: Colors.white,
|
|
padding: EdgeInsets.all(4.w),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
_buildChoiceChip(
|
|
"Pengiriman",
|
|
DeliveryType.delivery,
|
|
_deliveryType == DeliveryType.delivery,
|
|
),
|
|
SizedBox(width: 3.w),
|
|
_buildChoiceChip(
|
|
"Ambil Ditoko",
|
|
DeliveryType.pick_up,
|
|
_deliveryType == DeliveryType.pick_up,
|
|
),
|
|
],
|
|
),
|
|
/**
|
|
Divider(height: 3.h),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
"Waktu Pengiriman",
|
|
style: TextStyle(fontSize: (AppFontSize.small - 1.25).sp),
|
|
),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.access_time, size: 14.sp, color: Colors.grey),
|
|
SizedBox(width: 1.w),
|
|
Text(
|
|
"Sekarang (30-40 Menit)",
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Icon(Icons.chevron_right, color: Colors.grey),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
*/
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildChoiceChip(
|
|
String label,
|
|
DeliveryType deliveryType,
|
|
bool isSelected,
|
|
) {
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => setState(() => _deliveryType = deliveryType),
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(vertical: 1.2.h),
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? AppColor.primaryColor : Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(2.w),
|
|
),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: isSelected ? Colors.white : Colors.black87,
|
|
fontSize: AppFontSize.small.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildProductSummarySection(
|
|
List<CartItem> items,
|
|
String outletName,
|
|
LatLng? outletCoordinate,
|
|
SelectedAddress? address,
|
|
) {
|
|
var distance = 0.0;
|
|
|
|
if (outletCoordinate != null && address != null) {
|
|
distance = _calculateDistance(
|
|
outletCoordinate.latitude,
|
|
outletCoordinate.longitude,
|
|
address.latitude,
|
|
address.longitude,
|
|
);
|
|
}
|
|
|
|
var strDistance = '${distance.toStringAsFixed(1)} Km';
|
|
|
|
if (distance < 1) {
|
|
strDistance = '${(distance * 1000).toStringAsFixed(0)} meter';
|
|
}
|
|
|
|
return Container(
|
|
color: Colors.white,
|
|
padding: EdgeInsets.all(4.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(3.w),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.primaryColor.withOpacity(0.05),
|
|
borderRadius: BorderRadius.circular(2.w),
|
|
border: Border.all(color: AppColor.primaryColor.withOpacity(0.1)),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(2.w),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.storefront,
|
|
color: AppColor.primaryColor,
|
|
size: 5.w,
|
|
),
|
|
),
|
|
SizedBox(width: 3.w),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_deliveryType == DeliveryType.delivery
|
|
? "Pesanan dikirim dari:"
|
|
: "Tempat pengambilan:",
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 0.5.h),
|
|
|
|
Text(
|
|
outletName,
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
if (distance > 0) ...[
|
|
SizedBox(height: 0.5.h),
|
|
Text(
|
|
"Jarak \u00B1 $strDistance dari lokasi kamu",
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
color: AppColor.primaryColor,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
Divider(height: 4.h),
|
|
...List.generate(items.length, (index) {
|
|
final item = items[index];
|
|
return Padding(
|
|
padding: EdgeInsets.only(bottom: 2.h),
|
|
child: Row(
|
|
children: [
|
|
CachedNetworkImage(
|
|
imageUrl: item.image ?? 'error',
|
|
imageBuilder: (context, imageProvider) {
|
|
return Container(
|
|
width: 15.w,
|
|
height: 15.w,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
border: BoxBorder.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(2.w),
|
|
image: DecorationImage(
|
|
image: imageProvider,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
errorWidget: (context, url, error) {
|
|
return Container(
|
|
width: 15.w,
|
|
height: 15.w,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(2.w),
|
|
),
|
|
child: Icon(Icons.image, color: Colors.grey.shade300),
|
|
);
|
|
},
|
|
),
|
|
|
|
SizedBox(width: 3.w),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
item.name,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(fontSize: AppFontSize.small.sp),
|
|
),
|
|
if (item.isProductVariant)
|
|
Text(
|
|
item.variantName,
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
color: Colors.grey.shade500,
|
|
),
|
|
),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
CurrencyFormat.formatToIdr(item.sellingPrice, 0),
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
color: AppColor.primaryColor,
|
|
),
|
|
),
|
|
Text(
|
|
"x ${item.quantity}",
|
|
style: TextStyle(
|
|
color: Colors.grey.shade500,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDeliveryPreferenceSection({
|
|
required bool isCloseService,
|
|
required int onlineOpenTime,
|
|
required int onlineCloseTime,
|
|
required int preparationTimeMinutes,
|
|
required int orderIntervalMinutes,
|
|
}) {
|
|
var slots = [];
|
|
if (preparationTimeMinutes > 0 || orderIntervalMinutes > 0)
|
|
slots = _generateAvailableSlots(
|
|
onlineOpenTime: onlineOpenTime,
|
|
onlineCloseTime: onlineCloseTime,
|
|
preparationTimeMinutes: preparationTimeMinutes,
|
|
orderIntervalMinutes: orderIntervalMinutes,
|
|
);
|
|
|
|
return Container(
|
|
color: Colors.white,
|
|
padding: EdgeInsets.all(4.w),
|
|
child: isCloseService || DateTime.now().hour >= onlineCloseTime
|
|
? Center(
|
|
child: Text(
|
|
"Waktu pengiriman tidak tersedia",
|
|
style: TextStyle(
|
|
color: Colors.grey.shade500,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
)
|
|
: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_deliveryType == DeliveryType.delivery
|
|
? "Waktu Pengiriman"
|
|
: "Waktu Pengambilan",
|
|
style: TextStyle(
|
|
fontSize: AppFontSize.small.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
SizedBox(height: 1.h),
|
|
Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.info_outline,
|
|
size: 3.5.w,
|
|
color: AppColor.primaryColor,
|
|
),
|
|
SizedBox(width: 0.75.w),
|
|
Text(
|
|
_deliveryType == DeliveryType.delivery
|
|
? "Waktu produk kamu dikirim dari toko"
|
|
: "Waktu produk kamu di ambil di toko",
|
|
style: TextStyle(
|
|
color: AppColor.primaryColor,
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
/**
|
|
SizedBox(height: 2.h),
|
|
Row(
|
|
children: [
|
|
_buildDayRadio("Sekarang"),
|
|
SizedBox(width: 5.w),
|
|
_buildDayRadio("Besok"),
|
|
],
|
|
),
|
|
*/
|
|
SizedBox(height: 2.h),
|
|
|
|
_buildDayRadio(),
|
|
|
|
SizedBox(height: 2.5.h),
|
|
|
|
SingleChildScrollView(
|
|
clipBehavior: Clip.none,
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: [
|
|
if (DateTime.now().hour >= onlineOpenTime)
|
|
_buildTimeButton(
|
|
label: "Segera",
|
|
icon: Icons.history,
|
|
isActive: _deliveryPreference == "Segera",
|
|
onTap: () =>
|
|
setState(() => _deliveryPreference = "Segera"),
|
|
),
|
|
|
|
if (slots.isNotEmpty)
|
|
...List.generate(slots.length, (index) {
|
|
final label = _formatRange(slots[index]);
|
|
return _buildTimeButton(
|
|
label: label,
|
|
icon: Icons.access_time,
|
|
isActive: _deliveryPreference == label,
|
|
onTap: () {
|
|
setState(() => _deliveryPreference = label);
|
|
},
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDayRadio() {
|
|
return Row(
|
|
children: [
|
|
Icon(
|
|
Icons.radio_button_checked,
|
|
color: AppColor.primaryColor,
|
|
size: 5.w,
|
|
),
|
|
SizedBox(width: 2.w),
|
|
Text(
|
|
"Hari Ini",
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
color: AppColor.primaryColor,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTimeButton({
|
|
required String label,
|
|
required IconData icon,
|
|
required bool isActive,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: 45.w,
|
|
padding: EdgeInsets.symmetric(vertical: 1.2.h, horizontal: 2.w),
|
|
margin: EdgeInsets.only(right: 3.w),
|
|
decoration: BoxDecoration(
|
|
color: isActive ? AppColor.primaryColor : Colors.grey.shade50,
|
|
borderRadius: BorderRadius.circular(2.w),
|
|
border: Border.all(
|
|
color: isActive ? AppColor.primaryColor : Colors.grey.shade300,
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(icon, size: 5.w, color: isActive ? Colors.white : Colors.grey),
|
|
SizedBox(width: 2.w),
|
|
Flexible(
|
|
child: Text(
|
|
label,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: isActive ? Colors.white : Colors.grey.shade700,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildNoteSection() {
|
|
return Container(
|
|
color: Colors.white,
|
|
padding: EdgeInsets.all(4.w),
|
|
child: TextField(
|
|
controller: _noteController,
|
|
style: TextStyle(fontSize: AppFontSize.small.sp),
|
|
decoration: InputDecoration(
|
|
hintText: "Tambah Catatan",
|
|
hintStyle: TextStyle(fontSize: AppFontSize.small.sp),
|
|
prefixIcon: Icon(
|
|
Icons.note_alt_outlined,
|
|
color: Colors.grey,
|
|
size: 5.w,
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPaymentMethodSection(
|
|
List<TenantPaymentMethodModel> paymentMethods,
|
|
) {
|
|
if (_selectedPaymentMethod == "COD" ||
|
|
_selectedPaymentMethod == 'Bayar Ditoko') {
|
|
_selectedPaymentMethod = _deliveryType == DeliveryType.delivery
|
|
? "COD"
|
|
: "Bayar Ditoko";
|
|
}
|
|
|
|
final visibleMethods = _isPaymentExpanded
|
|
? paymentMethods
|
|
: paymentMethods.take(3).toList();
|
|
|
|
return Container(
|
|
color: Colors.white,
|
|
padding: EdgeInsets.all(4.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"Metode Pembayaran",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
SizedBox(height: 2.h),
|
|
|
|
...List.generate(visibleMethods.length, (index) {
|
|
final method = visibleMethods[index];
|
|
return Column(
|
|
children: [
|
|
RadioListTile(
|
|
value: method.sourceName.capitalize(),
|
|
groupValue: _selectedPaymentMethod,
|
|
onChanged: (val) {
|
|
setState(() {
|
|
_selectedPaymentMethod = val!;
|
|
_paymentProofPath = null;
|
|
});
|
|
},
|
|
activeColor: AppColor.primaryColor,
|
|
contentPadding: EdgeInsets.zero,
|
|
title: Row(
|
|
children: [
|
|
method.sourceName == 'COD' ||
|
|
method.sourceName == 'Bayar Ditoko'
|
|
? Icon(
|
|
Icons.payments_outlined,
|
|
size: 7.w,
|
|
color: AppColor.primaryColor,
|
|
)
|
|
: Image.asset(
|
|
_fetchPaymentMethodAsset(method.sourceName),
|
|
width: 7.w,
|
|
fit: BoxFit.contain,
|
|
),
|
|
SizedBox(width: 2.5.w),
|
|
Text(
|
|
method.sourceName.capitalize(),
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
if (_selectedPaymentMethod == method.sourceName.capitalize() &&
|
|
method.sourceName != 'COD' &&
|
|
method.sourceName != 'Bayar Ditoko')
|
|
_buildUploadProofSection(method),
|
|
],
|
|
);
|
|
}),
|
|
|
|
if (paymentMethods.length > 3)
|
|
Center(
|
|
child: TextButton.icon(
|
|
style: TextButton.styleFrom(overlayColor: Colors.transparent),
|
|
onPressed: () =>
|
|
setState(() => _isPaymentExpanded = !_isPaymentExpanded),
|
|
icon: Icon(
|
|
_isPaymentExpanded
|
|
? Icons.keyboard_arrow_up
|
|
: Icons.keyboard_arrow_down,
|
|
color: Colors.grey,
|
|
size: 5.w,
|
|
),
|
|
label: Text(
|
|
_isPaymentExpanded ? "Tutup" : "Selengkapnya",
|
|
style: TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildUploadProofSection(TenantPaymentMethodModel paymentMethod) {
|
|
final File imageFile = File(_paymentProofPath ?? "image not found");
|
|
|
|
final bool imageFileExists = imageFile.existsSync();
|
|
|
|
return Container(
|
|
margin: EdgeInsets.only(left: 12.w, bottom: 2.h, right: 2.w),
|
|
padding: EdgeInsets.all(3.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade50,
|
|
borderRadius: BorderRadius.circular(2.w),
|
|
border: Border.all(color: Colors.grey.shade200),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"Tujuan Transfer:",
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
),
|
|
SizedBox(height: 0.5.h),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
paymentMethod.accountNumber,
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColor.primaryColor,
|
|
),
|
|
),
|
|
Text(
|
|
paymentMethod.receipentName,
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
Clipboard.setData(
|
|
ClipboardData(text: paymentMethod.accountNumber),
|
|
);
|
|
CustomSnackbar.showSuccess(
|
|
context,
|
|
"Nomor akun berhasil disalin",
|
|
);
|
|
},
|
|
icon: Icon(Icons.copy, size: 5.w, color: Colors.grey),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(),
|
|
),
|
|
],
|
|
),
|
|
|
|
Divider(height: 3.h, color: Colors.grey.shade200),
|
|
|
|
Text(
|
|
"Upload Bukti Pembayaran",
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
SizedBox(height: 1.5.h),
|
|
InkWell(
|
|
onTap: _showPickerOptions,
|
|
child: Stack(
|
|
children: [
|
|
imageFileExists
|
|
? GestureDetector(
|
|
onTap: () {
|
|
if (!imageFileExists) {
|
|
CustomSnackbar.showError(
|
|
context,
|
|
"Ups, terjadi kesalahan",
|
|
);
|
|
return;
|
|
}
|
|
|
|
showDialog(
|
|
context: context,
|
|
useRootNavigator: true,
|
|
builder: (context) => Dialog(
|
|
insetPadding: EdgeInsets.symmetric(
|
|
horizontal: 2.w,
|
|
vertical: 2.h,
|
|
),
|
|
backgroundColor: Colors.transparent,
|
|
child: GestureDetector(
|
|
onTap: () => Navigator.pop(context),
|
|
child: Container(
|
|
width: MediaQuery.of(context).size.width,
|
|
constraints: BoxConstraints(
|
|
maxWidth: 85.w,
|
|
maxHeight: 85.h,
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(4.w),
|
|
child: InteractiveViewer(
|
|
child: Image.file(
|
|
imageFile,
|
|
fit: BoxFit.contain,
|
|
width: double.infinity,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: 12.h,
|
|
decoration: BoxDecoration(
|
|
border: BoxBorder.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(2.w),
|
|
image: DecorationImage(
|
|
image: FileImage(imageFile),
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
: Container(
|
|
width: double.infinity,
|
|
height: 12.h,
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: Colors.grey.shade300,
|
|
style: BorderStyle.solid,
|
|
),
|
|
borderRadius: BorderRadius.circular(2.w),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.cloud_upload_outlined,
|
|
color: AppColor.primaryColor,
|
|
size: 7.w,
|
|
),
|
|
SizedBox(height: 1.h),
|
|
Text(
|
|
"Klik untuk upload gambar",
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - 1.25).sp,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
if (_paymentProofPath != null)
|
|
Positioned(
|
|
top: 1.w,
|
|
right: 3.5.w,
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
type: MaterialType.transparency,
|
|
child: InkWell(
|
|
onTap: _removeImage,
|
|
child: Container(
|
|
padding: EdgeInsets.all(1.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red[50],
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: Colors.red.withOpacity(0.2),
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 2,
|
|
offset: Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Icon(
|
|
Icons.remove,
|
|
color: Colors.red,
|
|
size: 4.w,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPaymentDetailSection({
|
|
required double totalOrder,
|
|
required bool isAddressSelcted,
|
|
required DeliveryFeeType deliveryFeeType,
|
|
required double deliveryFee,
|
|
required String strDistance,
|
|
required double totalDeliveryFee,
|
|
required double totalAmount,
|
|
}) {
|
|
return Container(
|
|
color: Colors.white,
|
|
padding: EdgeInsets.all(4.w),
|
|
child: Column(
|
|
children: [
|
|
_buildRowDetail(
|
|
"Subtotal Pesanan",
|
|
CurrencyFormat.formatToIdr(totalOrder, 0),
|
|
),
|
|
if (_deliveryType == DeliveryType.delivery && isAddressSelcted) ...[
|
|
if (deliveryFeeType == DeliveryFeeType.per_km) ...[
|
|
_buildRowDetail(
|
|
"Biaya Pengiriman Per Km",
|
|
CurrencyFormat.formatToIdr(deliveryFee, 0),
|
|
),
|
|
_buildRowDetail(
|
|
"Jarak alamatmu dengan toko",
|
|
"\u00B1 $strDistance",
|
|
),
|
|
],
|
|
_buildRowDetail(
|
|
"Subtotal Pengiriman",
|
|
CurrencyFormat.formatToIdr(totalDeliveryFee, 0),
|
|
),
|
|
],
|
|
const Divider(),
|
|
_buildRowDetail(
|
|
"Total Pembayaran",
|
|
CurrencyFormat.formatToIdr(totalAmount, 0),
|
|
isBold: true,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRowDetail(String label, String value, {bool isBold = false}) {
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 0.5.h),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - (isBold ? 0 : 1.25)).sp,
|
|
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: (AppFontSize.small - (isBold ? 0 : 1.25)).sp,
|
|
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
|
|
color: isBold ? AppColor.primaryColor : Colors.black,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBottomCheckoutAction({
|
|
String? deliveryFeeType,
|
|
required double deliveryFee,
|
|
required bool isLoading,
|
|
required double totalAmount,
|
|
required double totalDeliveryFee,
|
|
required double maxDeliveryRadiusKm,
|
|
}) {
|
|
return Container(
|
|
height: 8.h,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border(top: BorderSide(color: Colors.grey.shade200)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Padding(
|
|
padding: EdgeInsets.only(left: 4.w),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"Total Pembayaran",
|
|
style: TextStyle(fontSize: AppFontSize.small.sp),
|
|
),
|
|
Text(
|
|
CurrencyFormat.formatToIdr(totalAmount, 0),
|
|
style: TextStyle(
|
|
fontSize: AppFontSize.small.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColor.primaryColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
InkWell(
|
|
onTap: isLoading
|
|
? null
|
|
: () async => await _checkout(
|
|
deliveryFeeType: deliveryFeeType,
|
|
deliveryFee: deliveryFee,
|
|
totalDeliveryFee: totalDeliveryFee,
|
|
maxDeliveryRadiusKm: maxDeliveryRadiusKm,
|
|
),
|
|
child: Container(
|
|
width: 40.w,
|
|
height: double.infinity,
|
|
color: isLoading ? Colors.grey.shade300 : AppColor.primaryColor,
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
"Buat Pesanan",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|