QueenFruits/Mobile Operasional/lib/features/pos/presentation/screens/pos_screen.dart

1184 lines
41 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:niogu_app/core/constants/app_color.dart';
import 'package:niogu_app/core/constants/app_font_size.dart';
import 'package:niogu_app/core/utils/login_required.dart';
import 'package:niogu_app/core/widgets/custom_error_screen.dart';
import 'package:niogu_app/core/providers/app_provider.dart';
import 'package:niogu_app/core/router/app_route.dart';
import 'package:niogu_app/core/utils/currency_format.dart';
import 'package:niogu_app/core/utils/extension_format.dart';
import 'package:niogu_app/core/utils/image_service.dart';
import 'package:niogu_app/core/utils/log_message.dart';
import 'package:niogu_app/core/enums/stock_type.dart';
import 'package:niogu_app/core/system/system_setting.dart';
import 'package:niogu_app/core/widgets/custom_empty_screen.dart';
import 'package:niogu_app/core/widgets/custom_not_login.dart';
import 'package:niogu_app/core/widgets/custom_snackbar.dart';
import 'package:niogu_app/features/goods/products/domain/entities/product.dart';
import 'package:niogu_app/features/pos/domain/entities/pos.dart';
import 'package:niogu_app/features/pos/presentation/providers/pos_provider.dart';
import 'package:niogu_app/features/pos/presentation/widgets/cart_bottom_sheet.dart';
import 'package:niogu_app/features/pos/presentation/widgets/decimal_quantity.dart';
import 'package:niogu_app/features/pos/presentation/widgets/insufficient_stock_dialog.dart';
import 'package:niogu_app/features/pos/presentation/widgets/pos_shimmer.dart';
import 'package:niogu_app/features/pos/presentation/widgets/product_card.dart';
import 'package:niogu_app/features/report/transaction/presentation/providers/transaction_report_provider.dart';
import 'package:sizer/sizer.dart';
class PosScreen extends ConsumerStatefulWidget {
const PosScreen({super.key});
@override
ConsumerState<PosScreen> createState() => _PosScreenState();
}
class _PosScreenState extends ConsumerState<PosScreen> {
final FocusNode _searchFocusNode = FocusNode();
Color _searchIconColor = Colors.grey;
final DraggableScrollableController _sheetController =
DraggableScrollableController();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _discountController = TextEditingController();
final TextEditingController _taxController = TextEditingController();
final TextEditingController _noteController = TextEditingController();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _phoneNumberController = TextEditingController();
final TextEditingController _addressController = TextEditingController();
final TextEditingController _payController = TextEditingController();
final List<String?> _imagePathTemps = [];
final ImagePicker _picker = ImagePicker();
Timer? _debounce;
bool _isOtherFormVisible = false;
bool _isCustomerFormVisible = false;
bool _validateChange = false;
double _changeAmount = 0.0;
String? _imagePath;
@override
void initState() {
// TODO: implement initState
super.initState();
_searchFocusNode.addListener(() {
setState(() {
_searchIconColor = _searchFocusNode.hasFocus
? Colors.black
: Colors.grey;
});
});
}
@override
void dispose() {
// TODO: implement dispose
_searchFocusNode.dispose();
_discountController.dispose();
_taxController.dispose();
_noteController.dispose();
_nameController.dispose();
_phoneNumberController.dispose();
_addressController.dispose();
_payController.dispose();
_debounce?.cancel();
super.dispose();
}
void _expandSheet() {
FocusManager.instance.primaryFocus?.unfocus();
Future.delayed(const Duration(milliseconds: 100), () {
if (_sheetController.isAttached) {
_sheetController.animateTo(
0.9,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} else {
LogMessage.log.i("Sheet controller is not attached");
}
});
}
void _onSearchChanged(String value) {
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 800), () {
ref.read(displayProductPosBySearchProvider.notifier).state = value;
});
}
int _calculateProductAmount(List<CartItems> cartItems) {
return cartItems.length;
}
double _calculateTotalQty(List<CartItems> cartItems) {
return cartItems.fold(0.0, (sum, cartItem) {
return sum + cartItem.quantity;
});
}
double _calculateShoppingAmount(List<CartItems> cartItems) {
return cartItems.fold(0.0, (sum, cartItem) {
return sum + cartItem.quantity * cartItem.sellingPrice;
});
}
void _discountOnChanged(String value) {
final currentOutletId = ref.read(currentOutletIdProvider);
final cartItemState = ref.read(cartItemsControllerProvider);
final cartItems = cartItemState.values.where((cartItem) {
return cartItem.currentOutletId == currentOutletId;
}).toList();
final double payAmount = double.tryParse(_payController.text.trim()) ?? 0.0;
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 400), () {
setState(() {
_validateChange = payAmount < _calculateTotalBill(cartItems);
_discountController.text = value;
});
});
if (_payController.text.trim().isNotEmpty) {
_calculateChange(_payController.text);
}
}
void _taxOnChanged(String value) {
final currentOutletId = ref.read(currentOutletIdProvider);
final cartItemState = ref.read(cartItemsControllerProvider);
final cartItems = cartItemState.values.where((cartItem) {
return cartItem.currentOutletId == currentOutletId;
}).toList();
final double payAmount = double.tryParse(_payController.text.trim()) ?? 0.0;
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 400), () {
setState(() {
_validateChange = payAmount < _calculateTotalBill(cartItems);
_taxController.text = value;
});
});
if (_payController.text.trim().isNotEmpty) {
_calculateChange(_payController.text);
}
}
void _calculateChange(String value) {
final currentOutletId = ref.read(currentOutletIdProvider);
final cartItemState = ref.read(cartItemsControllerProvider);
final cartItems = cartItemState.values.where((cartItem) {
return cartItem.currentOutletId == currentOutletId;
}).toList();
final double payAmount = double.tryParse(value) ?? 0.0;
final double totalBill = _calculateTotalBill(cartItems);
_validateChange = payAmount < totalBill;
_payController.text = value;
if (payAmount >= totalBill && cartItems.isNotEmpty) {
_changeAmount = payAmount - totalBill;
}
}
double _calculateTotalBill(List<CartItems> cartItems) {
final double discount =
double.tryParse(_discountController.text.trim()) ?? 0.0;
final double tax = double.tryParse(_taxController.text.trim()) ?? 0.0;
final double totalBiil = _calculateShoppingAmount(cartItems);
return totalBiil - discount + tax;
}
void _handleDecrement(DisplayProductPos product) {
final cartItemsNotifier = ref.read(cartItemsControllerProvider.notifier);
cartItemsNotifier.decrement(product.productVariantId);
if (_payController.text.trim().isNotEmpty) {
_calculateChange(_payController.text);
}
}
Future<void> _handleIncrement(DisplayProductPos product) async {
final cartItemsNotifier = ref.read(cartItemsControllerProvider.notifier);
if (product.stockType == StockType.fixed) {
if (product.remainingStock == 0) {
CustomSnackbar.showError(context, "Stok habis");
return;
}
if (cartItemsNotifier.quantityOf(product.productVariantId) >=
product.remainingStock) {
CustomSnackbar.showError(context, "Stok tidak cukup");
return;
}
}
await cartItemsNotifier.increment(product);
if (_payController.text.isNotEmpty) {
_calculateChange(_payController.text);
}
}
void _handleDeleteItem(CartItems cartItem) {
final cartItemsNotifier = ref.read(cartItemsControllerProvider.notifier);
cartItemsNotifier.delete(cartItem.id);
if (_payController.text.trim().isNotEmpty) {
_calculateChange(_payController.text);
}
}
void _clear() {
_discountController.clear();
_taxController.clear();
_noteController.clear();
_nameController.clear();
_phoneNumberController.clear();
_addressController.clear();
_payController.clear();
}
void _showInsufficientStockDialog({
required String id,
required String name,
required double currentStock,
required double totalNeeded,
required double missingAmount,
required String unit,
required String triggerProductName,
required int otherProductsCount,
}) {
showDialog(
context: context,
builder: (context) {
return InsufficientStockDialog(
materialName: name,
currentStock: currentStock.toStringWithoutTrailingZero(),
totalNeeded: totalNeeded.toStringWithoutTrailingZero(),
missingAmount: missingAmount.toStringWithoutTrailingZero(),
unit: unit,
triggerProductName: triggerProductName,
otherProductsCount: otherProductsCount,
onAddStockPressed: () {
context.pushNamed(AppRoute.stockInScreen);
},
onEditUsagePressed: () {
context.pushNamed(
AppRoute.editRawMaterialScreen,
pathParameters: {"id": id},
);
},
);
},
);
}
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) {
_imagePathTemps.add(imagePath);
setState(() {
_imagePath = imagePath;
});
}
}
} catch (e, st) {
LogMessage.log.w(e.toString(), error: e, stackTrace: st);
CustomSnackbar.showWarning(context, "Akses ditolak");
}
}
Future<void> _removeImage() async {
setState(() {
_imagePath = null;
});
}
Future<void> _cleanUpImages() async {
for (final path in _imagePathTemps) {
if (path != null) {
await ImageService.deleteLocalImage(path);
}
}
}
Future<void> _addSale() async {
final cartItemState = ref.read(cartItemsControllerProvider);
final currentOutletId = await SystemSetting.getCurrentOutletId();
final cartItems = cartItemState.values.where((cartItem) {
return cartItem.currentOutletId == currentOutletId;
}).toList();
if (cartItems.isEmpty) {
CustomSnackbar.showError(context, "Tambah minimal 1 produk");
return;
}
final Map<String, Set<String>> usedByProducts = {};
final Map<String, double> requiredMaterials = {};
final Map<String, String> materialNames = {};
final Map<String, double> currentStocks = {};
final Map<String, String> materialUnits = {};
for (final cartItem in cartItems) {
final rawMaterials = await ref
.read(posRepositoryProvider)
.getProductUseMaterials(cartItem.id, cartItem.quantity);
for (final rawMaterial in rawMaterials) {
if (!usedByProducts.containsKey(rawMaterial.rawMaterialId)) {
usedByProducts[rawMaterial.rawMaterialId] = {};
}
usedByProducts[rawMaterial.rawMaterialId]!.add(
cartItem.variantName ?? cartItem.name,
);
final double needForThisItem =
rawMaterial.productQuantity * rawMaterial.quantity;
if (requiredMaterials.containsKey(rawMaterial.rawMaterialId)) {
requiredMaterials[rawMaterial.rawMaterialId] =
requiredMaterials[rawMaterial.rawMaterialId]! + needForThisItem;
} else {
requiredMaterials[rawMaterial.rawMaterialId] = needForThisItem;
}
cartItem.variantName ?? cartItem.name;
materialNames[rawMaterial.rawMaterialId] = rawMaterial.name;
currentStocks[rawMaterial.rawMaterialId] = rawMaterial.stock;
materialUnits[rawMaterial.rawMaterialId] = rawMaterial.unit;
}
}
for (final entry in requiredMaterials.entries) {
final String id = entry.key;
final double totalNeeded = entry.value;
final double currentStock = currentStocks[id] ?? 0.0;
final String name = materialNames[id] ?? 'Bahan Baku';
final String unit = materialUnits[id] ?? '';
final products = usedByProducts[id]!.toList();
final String triggerProductName = products.first;
final int otherProductsCount = products.length - 1;
if (currentStock < totalNeeded) {
final double missingAmount = totalNeeded - currentStock;
_showInsufficientStockDialog(
id: id,
name: name,
currentStock: currentStock,
totalNeeded: totalNeeded,
missingAmount: missingAmount,
unit: unit,
triggerProductName: triggerProductName,
otherProductsCount: otherProductsCount,
);
return;
}
}
if (!_formKey.currentState!.validate()) return;
try {
if (_discountController.text.isNotEmpty) {
final double discount = double.parse(_discountController.text.trim());
if (discount <= 0) {
CustomSnackbar.showError(context, "Diskon harus lebih dari 0");
return;
}
}
} catch (e) {
CustomSnackbar.showError(context, "Nominal diskon tidak valid");
return;
}
try {
if (_taxController.text.isNotEmpty) {
final double tax = double.parse(_taxController.text.trim());
if (tax <= 0) {
CustomSnackbar.showError(context, "Pajak harus lebih dari 0");
return;
}
}
} catch (e) {
CustomSnackbar.showError(context, "Nominal pajak tidak valid");
return;
}
try {
if (_payController.text.isNotEmpty && _validateChange) {
final double payAmount = double.parse(_payController.text.trim());
if (payAmount <= 0) {
CustomSnackbar.showError(
context,
"Nominal pembayaran harus lebih dari 0",
);
return;
}
final double totalBill = _calculateTotalBill(cartItems);
if (totalBill > payAmount) {
CustomSnackbar.showError(
context,
"Kurang ${CurrencyFormat.formatToIdr((totalBill - payAmount), 0)}",
);
return;
}
}
} catch (e) {
CustomSnackbar.showError(context, "Nominal pembayaran tidak valid");
return;
}
CustomerInformation? customer;
final Map<String, dynamic> customerAddressSnapshot = {};
final bool customerInfoIsNotEmpty =
_nameController.text.isNotEmpty ||
_phoneNumberController.text.isNotEmpty ||
_addressController.text.isNotEmpty;
if (customerInfoIsNotEmpty) {
if (_nameController.text.isEmpty) {
CustomSnackbar.showError(context, "Nama pelanggan belum diisi");
return;
}
final name = _nameController.text.trim();
final phoneNumber = _phoneNumberController.text.trim();
final address = _addressController.text.trim();
customer = CustomerInformation(
name: name,
phoneNumber: phoneNumber,
address: address,
);
if (address.isNotEmpty) {
customerAddressSnapshot.putIfAbsent("full_address", () => address);
}
}
final selectedCustomerState = ref.read(selectedCustomerProvider);
SelectedCustomer? selectedCustomer;
if (selectedCustomerState?.outletId == currentOutletId) {
selectedCustomer = selectedCustomerState;
}
if (selectedCustomer != null && selectedCustomer.address.isNotEmpty) {
customerAddressSnapshot.putIfAbsent(
"full_address",
() => selectedCustomer!.address,
);
}
String? customerId;
String? customerName;
String? customerPhoneNumber;
if (selectedCustomer != null) {
customerId = selectedCustomer.id;
customerName = selectedCustomer.name;
customerPhoneNumber = selectedCustomer.phoneNumber;
} else if (customer != null) {
customerId = customer.id;
customerName = customer.name;
customerPhoneNumber = customer.phoneNumber;
}
final newSale = NewSale(
customerId: customerId,
customerNameSnapshot: customerName,
customerPhoneNumberSnapshot: customerPhoneNumber,
totalOrder: _calculateShoppingAmount(cartItems),
otherInformation: OtherInformation(
discount: double.tryParse(_discountController.text.trim()) ?? 0.0,
tax: double.tryParse(_taxController.text.trim()) ?? 0.0,
note: _noteController.text.trim(),
),
totalAmount: _calculateTotalBill(cartItems),
amountPaid: double.tryParse(_payController.text.trim()) ?? 0.0,
changeAmount: _changeAmount,
paymentProofPath: _imagePath,
customerAddressSnapshot: customerAddressSnapshot,
);
final itemSales = cartItems.map((item) {
return ItemSale(
orderId: newSale.localId,
outletInventoryId: item.outletInventoryId,
productVariantId: item.id,
stockType: item.stockType,
stock: item.remainingStock,
currentSold: item.currentSold,
quantity: item.quantity,
costPrice: item.costPrice,
productImageSnapshot: item.imagePath,
productNameSnapshot: item.name,
productVariantNameSnapshot: item.variantName,
sellingPriceSnapshot: item.sellingPrice,
subtotal: item.quantity * item.sellingPrice,
);
}).toList();
try {
await ref
.read(posControllerProvider.notifier)
.addSale(customer, newSale, itemSales);
if (!mounted) return;
CustomSnackbar.showSuccess(context, "Penjualan baru berhasil dibuat");
_clear();
for (final path in _imagePathTemps) {
if (path != null && _imagePath != null && path != _imagePath) {
await ImageService.deleteLocalImage(path);
}
}
setState(() {
_imagePath = null;
_imagePathTemps.clear();
});
ref.invalidate(selectedCustomerProvider);
ref.invalidate(cartItemsControllerProvider);
final orderDetail = await ref
.read(transactionReportRepositoryProvider)
.getOrderDetail(newSale.localId);
context.pushNamed(
AppRoute.transactionReportOrderDetailScreen,
extra: orderDetail,
);
} catch (e, st) {
LogMessage.log.e(e.toString(), error: e, stackTrace: st);
CustomSnackbar.showError(context, "Ups, terjadi kesalahan");
}
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final bool isTablet = 100.w >= 600;
final bool isLandscape =
MediaQuery.of(context).orientation == Orientation.landscape;
const greyColor = Color(0xFFF5F5F5);
final isLoggedIn = ref.watch(currentStatusLoginProvider);
final currentUserName = ref.watch(currentUserNameProvider);
final currentOutletId = ref.watch(currentOutletIdProvider);
final currentOutletName = ref.watch(currentOutletNameProvider);
final currentUserRole = ref.watch(currentUserRoleProvider);
final productState = isLoggedIn
? ref.watch(filteredDisplayProductPosProvider)
: AsyncValue<List<DisplayProductPos>>.data([]);
final productEmptyState = isLoggedIn
? ref.watch(displayProductPosEmptyProvider)
: ProductEmpty.empty_database;
final selectedCustomerState = ref.watch(selectedCustomerProvider);
final selectedCustomer =
selectedCustomerState?.outletId == currentOutletId
? selectedCustomerState
: null;
final posControllerState = ref.watch(posControllerProvider);
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
await _cleanUpImages();
context.goNamed(AppRoute.homeScreen);
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_imagePath = null;
});
});
},
child: SafeArea(
top: false,
bottom: true,
right: false,
left: false,
child: Scaffold(
backgroundColor: Colors.grey[50],
resizeToAvoidBottomInset: false,
appBar: AppBar(
backgroundColor: Colors.white,
surfaceTintColor: Colors.white,
elevation: 0,
toolbarHeight: 10.h,
titleSpacing: 0,
automaticallyImplyLeading: false,
title: Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: Row(
children: [
IconButton(
onPressed: () async {
await _cleanUpImages();
context.goNamed(AppRoute.homeScreen);
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_imagePath = null;
});
});
},
icon: Icon(
Icons.close_rounded,
color: AppColor.primaryColor,
size: 7.w,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
SizedBox(width: 3.w),
Expanded(
child: Container(
height: isTablet && isLandscape ? 12.h : 6.h,
padding: isTablet
? EdgeInsets.symmetric(
vertical: isLandscape ? 2.h : 1.h,
horizontal: 2.w,
)
: EdgeInsets.zero,
decoration: BoxDecoration(
color: greyColor,
borderRadius: BorderRadius.circular(2.5.w),
),
child: TextField(
focusNode: _searchFocusNode,
onChanged: (value) => _onSearchChanged(value),
textAlignVertical: TextAlignVertical.center,
style: TextStyle(
fontSize: isTablet
? AppFontSize.medium.sp
: AppFontSize.small.sp,
),
decoration: InputDecoration(
hintText: "Cari produk...",
hintStyle: TextStyle(
color: _searchIconColor,
fontSize: isTablet
? AppFontSize.medium.sp
: AppFontSize.small.sp,
),
prefixIcon: Icon(
Icons.search,
color: _searchIconColor,
size: 5.w,
),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
),
),
SizedBox(width: 3.w),
/**
if (isTablet) ...[
IconButton(
tooltip: isLandscape
? "Ubah ke Potrait"
: "Ubah ke Landscape",
icon: Icon(
isLandscape
? Icons.phone_iphone_rounded
: Icons.tablet_mac_rounded,
size: 5.w,
color: AppColor.primaryColor,
),
onPressed: () {
if (isLandscape) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
} else {
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
},
),
SizedBox(width: 3.w),
],
*/
/** Notification
Stack(
children: [
IconButton(
onPressed: () {},
icon: Icon(
Icons.notifications_outlined,
color: AppColor.primaryColor,
size: 7.w,
),
padding: EdgeInsets.zero,
),
Positioned(
right: isTablet
? 10
: 100.w > 360
? 12.5
: 15,
top: isTablet
? 10
: 100.w > 360
? 12.5
: 15,
child: Container(
padding: EdgeInsets.all(0.5.w),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: BoxConstraints(
minWidth: 2.5.w,
minHeight: 2.5.w,
),
),
),
],
),
*/
],
),
),
),
body: productState.when(
data: (products) {
final cartItemsNotifier = ref.watch(
cartItemsControllerProvider.notifier,
);
final cartItemState = ref.watch(cartItemsControllerProvider);
final cartItems = cartItemState.values.where((cartItem) {
return cartItem.currentOutletId == currentOutletId;
}).toList();
return Stack(
children: [
!isLoggedIn
? const CustomNotLogin()
: Padding(
padding: EdgeInsets.only(
left: 4.w,
right: 4.w,
top: 2.h,
bottom: 20.h,
),
child: switch (productEmptyState) {
ProductEmpty.loading => const SizedBox(),
ProductEmpty.empty_database =>
CustomEmptyScreen(
title: "Tidak Ada Produk",
body: "Kamu belum memiliki produk",
textButton: "Buat Produk Pertamamu",
onPressed: () => context.pushNamed(
AppRoute.addProductScreen,
),
),
ProductEmpty.empty_search_result =>
const CustomEmptyScreen(
body: "Produk Tidak Ditemukan",
),
ProductEmpty.has_data => GridView.builder(
itemCount: products.length,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.75,
crossAxisSpacing: 4.w,
mainAxisSpacing: 2.h,
),
itemBuilder: (context, index) {
final product = products[index];
final bool isInCart = cartItemsNotifier
.isInCart(product.productVariantId);
final double quantity = cartItemsNotifier
.quantityOf(product.productVariantId);
return ProductCard(
product: product,
isInCart: isInCart,
quantity: quantity,
onPressed: () async =>
await _handleIncrement(product),
onDecrementTap: () =>
_handleDecrement(product),
onIncrementTap: () async =>
await _handleIncrement(product),
);
},
),
},
),
CartBottomSheet(
isLoggedIn: isLoggedIn,
sheetController: _sheetController,
formKey: _formKey,
currentUserName: currentUserName,
currentOutletName: currentOutletName,
currentUserRole: currentUserRole,
cartItems: cartItems,
shoppingAmount: _calculateShoppingAmount(cartItems),
productAmount: _calculateProductAmount(cartItems),
totalQty: _calculateTotalQty(cartItems),
discountController: _discountController,
taxController: _taxController,
noteController: _noteController,
nameController: _nameController,
phoneNumberController: _phoneNumberController,
addressController: _addressController,
validateChange: _validateChange,
totalBill: _calculateTotalBill(cartItems),
onViewOrderPressed: _expandSheet,
onOtherFormTap: () {
setState(() {
_isOtherFormVisible = !_isOtherFormVisible;
});
},
onCustomerFormTap: () {
setState(() {
_isCustomerFormVisible = !_isCustomerFormVisible;
});
},
isOtherFormVisible: _isOtherFormVisible,
isCustomerFormVisible: _isCustomerFormVisible,
selectedCustomer: selectedCustomer,
onTapCloseSelectedCustomer: selectedCustomer == null
? null
: () {
ref
.read(selectedCustomerProvider.notifier)
.state =
null;
},
payController: _payController,
calculateChange: (value) {
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce = Timer(
const Duration(milliseconds: 400),
() {
final currentOutletId = ref.read(
currentOutletIdProvider,
);
final cartItemState = ref.read(
cartItemsControllerProvider,
);
final cartItems = cartItemState.values.where((
cartItem,
) {
return cartItem.currentOutletId ==
currentOutletId;
}).toList();
final double payAmount =
double.tryParse(value) ?? 0.0;
final double totalBill = _calculateTotalBill(
cartItems,
);
setState(() {
_validateChange = payAmount < totalBill;
_payController.text = value;
if (payAmount >= totalBill &&
cartItems.isNotEmpty) {
_changeAmount = payAmount - totalBill;
}
});
},
);
},
changeAmount: _changeAmount,
imagePath: _imagePath,
onDecrementTap: (cartItem) {
final product = products.firstWhere(
(p) => p.productVariantId == cartItem.id,
);
_handleDecrement(product);
},
onIncrementTap: (cartItem) async {
final product = products.firstWhere(
(p) => p.productVariantId == cartItem.id,
);
await _handleIncrement(product);
},
onEditPressed: (cartItem) {
final String name = cartItem.name;
final String variantName =
cartItem.variantName ?? '-';
final stockType = cartItem.stockType;
final String remainingStock = cartItem.remainingStock
.toStringWithoutTrailingZero();
final String unit = cartItem.unit;
final String initialQuantity = cartItem.quantity
.toStringWithoutTrailingZero();
final String sellingPrice =
CurrencyFormat.formatToIdr(
cartItem.sellingPrice,
0,
);
showDialog(
context: context,
builder: (_) => DecimalQuantity(
name: name,
variantName: variantName,
stockType: stockType,
remainingStock: remainingStock,
unit: unit,
initialQuantity: initialQuantity,
sellingPrice: sellingPrice,
onPressed: (value) {
try {
final double stock = double.parse(
remainingStock,
);
final double quantity = double.parse(value);
if (stockType == StockType.fixed &&
quantity > stock) {
CustomSnackbar.showError(
context,
"Stok tidak cukup",
);
return;
}
final cartItemsNotifier = ref.read(
cartItemsControllerProvider.notifier,
);
cartItemsNotifier.customQuantity(
cartItem.id,
quantity,
);
if (_payController.text.trim().isNotEmpty) {
_calculateChange(_payController.text);
}
} catch (e) {
CustomSnackbar.showError(
context,
"Stok tidak valid",
);
}
},
),
);
},
onDeletePressed: _handleDeleteItem,
discountOnChanged: _discountOnChanged,
taxOnChanged: _taxOnChanged,
onTapImageAdd: !isLoggedIn
? () => LoginRequired.showLoginRequired(context)
: _showPickerOptions,
onTapImageRemove: _removeImage,
onProccessPressed:
!isLoggedIn || posControllerState.isLoading
? null
: _addSale,
),
],
);
},
error: (error, stackTrace) {
return CustomErrorScreen(
message: "Ups, terjadi kesalahan",
onRefresh: () {},
);
},
loading: () => const PosShimmer(),
),
),
),
);
},
);
}
}