perubahan denomuretor on pos sale

This commit is contained in:
2026-04-04 23:44:16 +07:00
parent 517ed3bea3
commit d912b1f63a
20 changed files with 350 additions and 142 deletions

View File

@@ -55,6 +55,18 @@ class _AddPartyState extends State<AddParty> {
final GlobalKey<FormState> _formKay = GlobalKey();
FocusNode focusNode = FocusNode();
/// Normalizes Indonesian phone numbers to local format (0xx...)
/// Handles: +6289..., 6289..., 0896.2792.1934, spaces, etc.
String _normalizePhone(String input) {
// Remove all non-digit characters (dots, spaces, dashes, +)
String digits = input.replaceAll(RegExp(r'[^\d]'), '');
// Strip country code: +62 or 62 at the start
if (digits.startsWith('62')) {
digits = '0' + digits.substring(2);
}
return digits;
}
List<Country> _countries = [];
Country? _selectedBillingCountry;
Country? _selectedShippingCountry;
@@ -152,11 +164,39 @@ class _AddPartyState extends State<AddParty> {
TextFormField(
controller: phoneController,
keyboardType: TextInputType.phone,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) {
final normalized = _normalizePhone(value);
if (normalized != value) {
phoneController.value = TextEditingValue(
text: normalized,
selection: TextSelection.collapsed(offset: normalized.length),
);
}
},
validator: (value) {
if (value == null || value.isEmpty) {
return lang.S.of(context).pleaseEnterAValidPhoneNumber;
}
final digits = value.replaceAll(RegExp(r'[^\d]'), '');
if (digits.length < 11 || digits.length > 13) {
// Show popup after frame
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('Invalid Phone Number'),
content: Text('Phone number must be between 11 and 13 digits.'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text('OK'),
),
],
),
);
});
return 'Invalid length';
}
return null;
},
decoration: InputDecoration(
@@ -878,6 +918,28 @@ class _AddPartyState extends State<AddParty> {
return;
}
// Validate phone number length (11-13 digits)
final phoneDigits = phoneController.text.replaceAll(RegExp(r'[^\d]'), '');
if (phoneDigits.isNotEmpty && (phoneDigits.length < 11 || phoneDigits.length > 13)) {
await showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Invalid Number'),
content: const Text(
'Phone number must be between 11 and 13 digits.\n'
'Example: 08123456789 (11 digits) or 081234567890 (12 digits).',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('OK'),
),
],
),
);
return;
}
num parseOrZero(String? input) {
if (input == null || input.isEmpty) return 0;
return num.tryParse(input) ?? 0;

View File

@@ -364,6 +364,7 @@ class _DueCollectionScreenState extends State<DueCollectionScreen> {
showWalletOption: true, // Configure as needed
showChequeOption: (widget.customerModel.type != 'Supplier'), // Configure as needed
totalAmountController: paidText,
totalDueAmount: dueAmount,
onPaymentListChanged: () {},
),
const Divider(height: 20),

View File

@@ -1001,6 +1001,7 @@ class AddSalesScreenState extends ConsumerState<AddAndUpdatePurchaseScreen> {
key: paymentWidgetKey,
showWalletOption: true,
totalAmountController: recevedAmountController,
totalDueAmount: providerData.totalPayableAmount,
showChequeOption: false,
initialTransactions: widget.transitionModel?.transactions,
onPaymentListChanged: () {

View File

@@ -23,7 +23,6 @@ import '../../Const/api_config.dart';
import '../../GlobalComponents/glonal_popup.dart';
import '../../Repository/API/future_invoice.dart';
import '../../constant.dart';
import '../../currency.dart';
import '../../model/add_to_cart_model.dart';
import '../../model/sale_transaction_model.dart';
import '../../widgets/multipal payment mathods/multi_payment_widget.dart';
@@ -35,6 +34,16 @@ import '../invoice_details/sales_invoice_details_screen.dart';
import '../vat_&_tax/model/vat_model.dart';
import '../vat_&_tax/provider/text_repo.dart';
/// Format a number as Indonesian Rupiah for display only.
/// Example: 12500.75 → "Rp 12.500"
String _formatRupiah(num value) {
final intStr = value.round().toString().replaceAllMapped(
RegExp(r'(\d)(?=(\d{3})+(?!\d))'),
(m) => '${m[1]}.',
);
return 'Rp $intStr';
}
class AddSalesScreen extends ConsumerStatefulWidget {
AddSalesScreen({
super.key,
@@ -70,6 +79,11 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
void initState() {
super.initState();
// Pre-populate phone from selected customer
if (widget.customerModel?.phone != null) {
phoneController.text = widget.customerModel!.phone!;
}
// Listener for Received Amount Controller to calculate prices
recevedAmountController.addListener(() {
final cart = ref.read(cartNotifier);
@@ -296,7 +310,9 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
children: [
Text(lang.S.of(context).dueAmount),
Text(
widget.customerModel?.due == null ? '$currency 0' : '$currency${widget.customerModel?.due}',
widget.customerModel?.due == null
? _formatRupiah(0)
: _formatRupiah(widget.customerModel!.due ?? 0),
style: const TextStyle(color: Color(0xFFFF8C34)),
),
],
@@ -304,30 +320,34 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
const SizedBox(
height: 10,
),
AppTextField(
textFieldType: TextFieldType.NAME,
readOnly: true,
initialValue: widget.customerModel?.name ?? 'Guest',
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).customerName,
border: const OutlineInputBorder(),
),
),
Visibility(
visible: widget.customerModel == null,
child: Padding(
padding: const EdgeInsets.only(top: 20.0),
child: AppTextField(
controller: phoneController,
textFieldType: TextFieldType.PHONE,
decoration: kInputDecoration.copyWith(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).customerPhoneNumber,
hintText: lang.S.of(context).enterCustomerPhoneNumber,
Row(
children: [
Expanded(
child: AppTextField(
textFieldType: TextFieldType.NAME,
readOnly: true,
initialValue: widget.customerModel?.name ?? 'Guest',
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).customerName,
border: const OutlineInputBorder(),
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: AppTextField(
controller: phoneController,
textFieldType: TextFieldType.PHONE,
readOnly: widget.customerModel != null,
decoration: kInputDecoration.copyWith(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).customerPhoneNumber,
hintText: lang.S.of(context).enterCustomerPhoneNumber,
),
),
),
],
),
],
),
@@ -385,7 +405,7 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
),
),
Text(
'$currency${formatPointNumber(providerData.totalAmount)}',
_formatRupiah(providerData.totalAmount),
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
@@ -584,30 +604,17 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
const SizedBox(width: 10),*/
// VAT Amount Input Field
// VAT Amount Display (readOnly — show as Rupiah)
SizedBox(
height: 30,
width: 100,
child: TextFormField(
controller: providerData.vatAmountController,
style: _theme.textTheme.titleSmall,
readOnly: true,
onChanged: (value) => providerData.calculateDiscount(
value: value,
selectedTaxType: discountType.toString(),
child: Align(
alignment: Alignment.centerRight,
child: Text(
_formatRupiah(num.tryParse(providerData.vatAmountController.text) ?? 0),
style: _theme.textTheme.titleSmall,
textAlign: TextAlign.right,
),
textAlign: TextAlign.right,
decoration: InputDecoration(
hintText: '0',
hintStyle: _theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
),
border: const UnderlineInputBorder(borderSide: BorderSide(color: kBorder)),
enabledBorder: const UnderlineInputBorder(borderSide: BorderSide(color: kBorder)),
focusedBorder: const UnderlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8),
),
keyboardType: TextInputType.number,
),
),
],
@@ -663,7 +670,7 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
),
),
Text(
formatPointNumber(providerData.actualTotalAmount),
_formatRupiah(providerData.actualTotalAmount),
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
@@ -690,7 +697,7 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
),
),
Text(
formatPointNumber(providerData.roundingAmount),
_formatRupiah(providerData.roundingAmount),
style: _theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
),
@@ -710,7 +717,7 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
),
),
Text(
formatPointNumber(providerData.totalPayableAmount),
_formatRupiah(providerData.totalPayableAmount),
style: _theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
),
@@ -734,27 +741,45 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
color: kPeraColor,
),
),
SizedBox(
width: context.width() / 4,
height: 30,
child: TextFormField(
controller: recevedAmountController,
readOnly: (paymentWidgetKey.currentState?.getPaymentEntries().length ?? 1) > 1,
keyboardType: TextInputType.number,
textAlign: TextAlign.right,
style: _theme.textTheme.titleSmall,
decoration: InputDecoration(
hintText: '0',
hintStyle: _theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
// Received Amount — show Rupiah format when readOnly, numeric input when editable
Builder(builder: (ctx) {
final isReadOnly = (paymentWidgetKey.currentState?.getPaymentEntries().length ?? 1) > 1;
if (isReadOnly) {
return SizedBox(
width: context.width() / 4,
height: 30,
child: Align(
alignment: Alignment.centerRight,
child: Text(
_formatRupiah(num.tryParse(recevedAmountController.text) ?? 0),
style: _theme.textTheme.titleSmall,
textAlign: TextAlign.right,
),
),
);
}
return SizedBox(
width: context.width() / 4,
height: 30,
child: TextFormField(
controller: recevedAmountController,
readOnly: false,
keyboardType: TextInputType.number,
textAlign: TextAlign.right,
style: _theme.textTheme.titleSmall,
decoration: InputDecoration(
hintText: '0',
hintStyle: _theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
),
border: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)),
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)),
focusedBorder: UnderlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 8),
),
border: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)),
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)),
focusedBorder: UnderlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 8),
),
),
),
);
}),
],
),
),
@@ -774,7 +799,7 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
),
),
Text(
formatPointNumber(providerData.changeAmount),
_formatRupiah(providerData.changeAmount),
style: _theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
),
@@ -800,7 +825,7 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
),
),
Text(
formatPointNumber(providerData.dueAmount),
_formatRupiah(providerData.dueAmount),
style: _theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
),
@@ -820,6 +845,7 @@ class AddSalesScreenState extends ConsumerState<AddSalesScreen> {
key: paymentWidgetKey,
showWalletOption: true,
totalAmountController: recevedAmountController,
totalDueAmount: providerData.totalPayableAmount,
showChequeOption: true,
showCashDenomination: true,
initialTransactions: widget.transitionModel?.transactions,

View File

@@ -4,9 +4,18 @@ import 'package:hugeicons/hugeicons.dart';
import 'package:mobile_pos/Screens/Sales/provider/sales_cart_provider.dart';
import 'package:mobile_pos/Screens/Sales/sales_add_to_cart_sales_widget.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/currency.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
/// Format a number as Indonesian Rupiah for display only.
/// Example: 12500.75 → "Rp 12.500"
String _formatRupiah(num value) {
final intStr = value.round().toString().replaceAllMapped(
RegExp(r'(\d)(?=(\d{3})+(?!\d))'),
(m) => '${m[1]}.',
);
return 'Rp $intStr';
}
class SalesCartListWidget extends ConsumerWidget {
const SalesCartListWidget({super.key});
@@ -137,7 +146,7 @@ class SalesCartListWidget extends ConsumerWidget {
// 3. Qty Order
Text(
formatPointNumber(quantity),
formatPointNumber(quantity, addComma: true),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
@@ -158,7 +167,7 @@ class SalesCartListWidget extends ConsumerWidget {
// 5. Value Original
Text(
'$currency${formatPointNumber(originalTotal)}',
_formatRupiah(originalTotal),
style: TextStyle(
color: kPeraColor,
fontSize: 12,
@@ -169,7 +178,7 @@ class SalesCartListWidget extends ConsumerWidget {
// 6. Value VAT/Tax
if (itemVAT > 0) ...[
Text(
'VAT: $currency${formatPointNumber(itemVAT)}',
'VAT: ${_formatRupiah(itemVAT)}',
style: TextStyle(
color: kPeraColor,
fontSize: 10,
@@ -180,7 +189,7 @@ class SalesCartListWidget extends ConsumerWidget {
// 7. Sum (Value Original + VAT/Tax)
Text(
'$currency${formatPointNumber(finalTotal)}',
_formatRupiah(finalTotal),
style: _theme.textTheme.titleSmall?.copyWith(
color: kMainColor,
fontWeight: FontWeight.bold,

View File

@@ -317,7 +317,7 @@ class _ProductCardState extends State<ProductCard> {
),
// if (permissionService.hasPermission(Permit.salesPriceView.value))
Text(
'$currency${widget.productPrice}',
'$currency ${formatPointNumber(widget.productPrice, addComma: true)}',
style: theme.textTheme.titleMedium!.copyWith(fontSize: 18),
),
],

View File

@@ -16,7 +16,6 @@ import '../../Const/api_config.dart';
import '../../GlobalComponents/glonal_popup.dart';
import '../../constant.dart' as mainConstant;
import '../../constant.dart';
import '../../currency.dart';
import '../../model/business_info_model.dart' as binfo;
import '../../model/sale_transaction_model.dart';
import '../../thermal priting invoices/model/print_transaction_model.dart';
@@ -28,6 +27,16 @@ import '../language/language_provider.dart';
import '../../Provider/product_provider.dart';
import '../Home/home.dart';
/// Format a number as Indonesian Rupiah for display only.
/// Example: 12500.75 → "Rp 12.500"
String _formatRupiah(num value) {
final intStr = value.round().toString().replaceAllMapped(
RegExp(r'(\d)(?=(\d{3})+(?!\d))'),
(m) => '${m[1]}.',
);
return 'Rp $intStr';
}
class SalesInvoiceDetails extends StatefulWidget {
const SalesInvoiceDetails({
super.key,
@@ -654,7 +663,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
Expanded(
flex: 2,
child: Text(
'$currency${mainConstant.formatPointNumber(saleDetail.discount ?? 0, addComma: true)}',
_formatRupiah(saleDetail.discount ?? 0),
textAlign: TextAlign.center,
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w500,
@@ -678,7 +687,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
Expanded(
flex: 2,
child: Text(
'$currency${mainConstant.formatPointNumber(totalPrice, addComma: true)}',
_formatRupiah(totalPrice),
style: _theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
fontSize: fontSizeForPrinter(widget.businessInfo.data?.invoiceSize),
@@ -702,7 +711,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
text: '${lang.S.of(context).subTotal} : ',
children: [
TextSpan(
text: '$currency${mainConstant.formatPointNumber(getTotalForOldInvoice())}',
text: _formatRupiah(getTotalForOldInvoice()),
),
],
),
@@ -726,11 +735,11 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
),
children: [
TextSpan(
text: '$currency${mainConstant.formatPointNumber(
text: _formatRupiah(
(widget.saleTransaction.discountAmount ?? 0) +
getReturndDiscountAmount() +
getTotalItemDiscount(),
)}',
),
),
],
),
@@ -754,7 +763,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
),
children: [
TextSpan(
text: '$currency${mainConstant.formatPointNumber(widget.saleTransaction.vatAmount ?? 0)}',
text: _formatRupiah(widget.saleTransaction.vatAmount ?? 0),
),
],
),
@@ -778,8 +787,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
),
children: [
TextSpan(
text:
'$currency${mainConstant.formatPointNumber(widget.saleTransaction.shippingCharge ?? 0)}',
text: _formatRupiah(widget.saleTransaction.shippingCharge ?? 0),
),
],
),
@@ -808,8 +816,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
),
children: [
TextSpan(
text:
'$currency${mainConstant.formatPointNumber(widget.saleTransaction.actualTotalAmount ?? 0)}',
text: _formatRupiah(widget.saleTransaction.actualTotalAmount ?? 0),
),
],
),
@@ -834,7 +841,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
children: [
TextSpan(
text:
'$currency${!(widget.saleTransaction.roundingAmount?.isNegative ?? true) ? '+' : ''}${mainConstant.formatPointNumber(widget.saleTransaction.roundingAmount ?? 0)}',
'${!(widget.saleTransaction.roundingAmount?.isNegative ?? true) ? '+' : ''}${_formatRupiah(widget.saleTransaction.roundingAmount ?? 0)}',
),
],
),
@@ -857,8 +864,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
text: '${lang.S.of(context).totalAmount} : ',
children: [
TextSpan(
text:
'$currency${mainConstant.formatPointNumber(getTotalReturndAmount() + (widget.saleTransaction.totalAmount ?? 0))}',
text: _formatRupiah(getTotalReturndAmount() + (widget.saleTransaction.totalAmount ?? 0)),
),
],
),
@@ -1023,7 +1029,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
Expanded(
flex: 2,
child: Text(
'$currency${(widget.saleTransaction.salesReturns?[i].salesReturnDetails?[detailIndex].returnAmount ?? 0)}',
_formatRupiah(widget.saleTransaction.salesReturns?[i].salesReturnDetails?[detailIndex].returnAmount ?? 0),
textAlign: TextAlign.end,
style: _theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
@@ -1048,7 +1054,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
text: '${lang.S.of(context).totalReturnAmount} : ',
children: [
TextSpan(
text: '$currency${mainConstant.formatPointNumber(getTotalReturndAmount())}',
text: _formatRupiah(getTotalReturndAmount()),
),
],
),
@@ -1069,8 +1075,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
style: const TextStyle(fontWeight: FontWeight.w600),
children: [
TextSpan(
text:
'$currency${mainConstant.formatPointNumber(widget.saleTransaction.totalAmount ?? 0)}',
text: _formatRupiah(widget.saleTransaction.totalAmount ?? 0),
),
],
),
@@ -1090,8 +1095,11 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
text: '${lang.S.of(context).receivedAmount} : ',
children: [
TextSpan(
text:
'$currency${mainConstant.formatPointNumber(((widget.saleTransaction.totalAmount ?? 0) - (widget.saleTransaction.dueAmount ?? 0)) + (widget.saleTransaction.changeAmount ?? 0))}',
text: _formatRupiah(
((widget.saleTransaction.totalAmount ?? 0) -
(widget.saleTransaction.dueAmount ?? 0)) +
(widget.saleTransaction.changeAmount ?? 0),
),
),
],
),
@@ -1113,8 +1121,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
text: '${lang.S.of(context).due} : ',
children: [
TextSpan(
text:
'$currency${mainConstant.formatPointNumber(widget.saleTransaction.dueAmount ?? 0)}',
text: _formatRupiah(widget.saleTransaction.dueAmount ?? 0),
),
],
),
@@ -1136,8 +1143,7 @@ class _SalesInvoiceDetailsState extends State<SalesInvoiceDetails> {
text: '${_lang.changeAmount} : ',
children: [
TextSpan(
text:
'$currency${mainConstant.formatPointNumber(widget.saleTransaction.changeAmount ?? 0)}',
text: _formatRupiah(widget.saleTransaction.changeAmount ?? 0),
),
],
),

View File

@@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/currency.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../Const/api_config.dart';
@@ -33,6 +32,16 @@ import '../../model/business_info_model.dart';
import '../Home/home.dart';
import '../../Provider/profile_provider.dart';
/// Format a number as Indonesian Rupiah for display only.
/// Example: 12500.75 → "Rp 12.500"
String formatRupiah(num value) {
final intStr = value.round().toString().replaceAllMapped(
RegExp(r'(\d)(?=(\d{3})+(?!\d))'),
(m) => '${m[1]}.',
);
return 'Rp $intStr';
}
class PosSaleScreen extends ConsumerStatefulWidget {
const PosSaleScreen({super.key});
@@ -272,7 +281,7 @@ class _PosSaleScreenState extends ConsumerState<PosSaleScreen> {
),
const SizedBox(width: 8),
Text(
'(${providerData.cartItemList.length} Items): $currency${providerData.getTotalAmount()}',
'(${providerData.cartItemList.length} Items): ${formatRupiah(num.tryParse(providerData.getTotalAmount().toString()) ?? 0)}',
style: const TextStyle(color: Colors.white, fontSize: 14),
),
],
@@ -900,26 +909,26 @@ class _PosSaleScreenState extends ConsumerState<PosSaleScreen> {
),
Text(
() {
// Update: Price Display Logic
// Display-only: format as Indonesian Rupiah (Rp x.xxx.xxx,xx)
if (isCombo) {
return '$currency${product.productSalePrice ?? 0}';
return formatRupiah(product.productSalePrice ?? 0);
}
final customerType = selectedCustomer?.type ?? '';
final stock = product.stocks?.isNotEmpty == true ? product.stocks!.last : null;
if (stock == null) return '$currency${0.00}';
if (stock == null) return formatRupiah(0);
if (customerType.contains('Retailer')) {
return '$currency${stock.productSalePrice ?? 0}';
return formatRupiah(stock.productSalePrice ?? 0);
} else if (customerType.contains('Dealer')) {
return '$currency${stock.productDealerPrice ?? 0}';
return formatRupiah(stock.productDealerPrice ?? 0);
} else if (customerType.contains('Wholesaler')) {
return '$currency${stock.productWholeSalePrice ?? 0}';
return formatRupiah(stock.productWholeSalePrice ?? 0);
} else if (customerType.contains('Supplier')) {
return '$currency${stock.productPurchasePrice ?? 0}';
return formatRupiah(stock.productPurchasePrice ?? 0);
} else {
return '$currency${stock.productSalePrice ?? 0}';
return formatRupiah(stock.productSalePrice ?? 0);
}
}(),
style: _theme.textTheme.titleSmall?.copyWith(
@@ -1113,7 +1122,7 @@ class _PosSaleScreenState extends ConsumerState<PosSaleScreen> {
// 5. Value Original
Text(
'$currency${originalTotal.toStringAsFixed(2)}',
formatRupiah(originalTotal),
style: TextStyle(
color: kSubPeraColor,
fontSize: 12,
@@ -1124,7 +1133,7 @@ class _PosSaleScreenState extends ConsumerState<PosSaleScreen> {
// 6. Value VAT/Tax
if (itemVAT > 0) ...[
Text(
'VAT: $currency${itemVAT.toStringAsFixed(2)}',
'VAT: ${formatRupiah(itemVAT)}',
style: TextStyle(
color: kSubPeraColor,
fontSize: 10,
@@ -1135,7 +1144,7 @@ class _PosSaleScreenState extends ConsumerState<PosSaleScreen> {
// 7. Sum (Value Original + VAT/Tax)
Text(
'$currency${finalTotal.toStringAsFixed(2)}',
formatRupiah(finalTotal),
style: const TextStyle(
fontWeight: FontWeight.bold,
color: kMainColor,
@@ -1198,7 +1207,7 @@ class _PosSaleScreenState extends ConsumerState<PosSaleScreen> {
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
'$currency${providerData.getTotalAmount()}',
formatRupiah(num.tryParse(providerData.getTotalAmount().toString()) ?? 0),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,

View File

@@ -4,6 +4,7 @@ import 'package:intl/intl.dart';
import 'package:pdf/widgets.dart' as pw;
import 'generated/l10n.dart' as lang;
import 'currency.dart';
const kMainColor = Color(0xffC52127);
const kMainColor100 = Color(0xffFDE7F2);
@@ -49,7 +50,7 @@ const String onboard3 = 'images/onbord3.png';
const String logo = 'images/logokulakpos.png';
const String appsName = 'KULAKPOS';
const String companyWebsite = 'https://kulakpos.id';
const String companyName = 'KULAKPOS';
const String companyName = 'ONE TEKNOLOGI';
bool connected = false;
@@ -202,19 +203,41 @@ Map<String, String> languageMap = {
'Chinese': 'zh',
};
String getLocaleFromCurrency() {
final loweredName = currencyName.toLowerCase();
if (loweredName.contains('rupiah') || currency.toLowerCase() == 'rp' || currency == 'IDR') {
return 'id_ID';
} else if (loweredName.contains('rupee') || currency == '' || currency.toLowerCase() == 'rs' || currency == 'INR') {
return 'en_IN';
} else if (loweredName.contains('euro') || currency == '' || currency == 'EUR') {
return 'de_DE';
}
return 'en_US';
}
String formatPointNumber(num value, {bool addComma = false}) {
String formatted;
if (value % 1 == 0) {
formatted = value.toInt().toString();
} else {
formatted = value.toStringAsFixed(2);
}
// Always use 2 decimal places for consistent display
formatted = value.toStringAsFixed(2);
if (addComma) {
// NumberFormat from intl package is used here
final formatter = NumberFormat.decimalPattern();
formatted = formatter.format(num.parse(formatted));
try {
final formatter = NumberFormat.currency(locale: getLocaleFromCurrency(), symbol: '', decimalDigits: 2);
formatted = formatter.format(value).trim();
} catch (e) {
final fallback = NumberFormat.decimalPattern(getLocaleFromCurrency());
fallback.minimumFractionDigits = 2;
fallback.maximumFractionDigits = 2;
try {
formatted = fallback.format(value);
} catch (e2) {
final fallbackUs = NumberFormat.decimalPattern();
fallbackUs.minimumFractionDigits = 2;
fallbackUs.maximumFractionDigits = 2;
formatted = fallbackUs.format(value);
}
}
}
return formatted;

View File

@@ -6,6 +6,7 @@ import 'package:image/image.dart' as img;
import 'package:mobile_pos/Const/api_config.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:mobile_pos/thermal%20priting%20invoices/model/print_transaction_model.dart';
import 'package:mobile_pos/constant.dart';
import '../../thermer/thermer.dart' as thermer;
class DueThermalInvoiceTemplate {
@@ -31,11 +32,6 @@ class DueThermalInvoiceTemplate {
);
}
String formatPointNumber(num number, {bool addComma = false}) {
if (addComma) return NumberFormat("#,###.##", "en_US").format(number);
return number.toStringAsFixed(2);
}
// --- Main Generator ---
Future<List<int>> get template async {

View File

@@ -9,7 +9,7 @@ import 'package:mobile_pos/Screens/Products/add%20product/modle/create_product_m
import 'package:mobile_pos/Screens/Purchase/Model/purchase_transaction_model.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:mobile_pos/thermal%20priting%20invoices/model/print_transaction_model.dart';
import 'package:mobile_pos/constant.dart';
import '../../thermer/thermer.dart' as thermer;
class PurchaseThermalInvoiceTemplate {
@@ -37,11 +37,6 @@ class PurchaseThermalInvoiceTemplate {
);
}
String formatPointNumber(num number, {bool addComma = false}) {
if (addComma) return NumberFormat("#,###.##", "en_US").format(number);
return number.toStringAsFixed(2);
}
// --- Data Logic (Adapted from your provided code) ---
num _getProductPrice(num detailsId) {

View File

@@ -8,6 +8,7 @@ import 'package:mobile_pos/Const/api_config.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:mobile_pos/model/business_info_model.dart';
import 'package:mobile_pos/model/sale_transaction_model.dart';
import 'package:mobile_pos/constant.dart';
import '../../thermer/thermer.dart' as thermer;
class SaleThermalInvoiceTemplate {
@@ -36,11 +37,6 @@ class SaleThermalInvoiceTemplate {
);
}
String formatPointNumber(num number, {bool addComma = false}) {
if (addComma) return NumberFormat("#,###.##", "en_US").format(number);
return number.toStringAsFixed(2);
}
// --- Data Logic ---
String _getProductName(num detailsId) {

View File

@@ -41,6 +41,7 @@ class MultiPaymentWidget extends ConsumerStatefulWidget {
final bool hideAddButton;
final bool disableDropdown;
final bool showCashDenomination;
final num? totalDueAmount;
final VoidCallback? onPaymentListChanged;
final List<PaymentsTransaction>? initialTransactions;
@@ -53,6 +54,7 @@ class MultiPaymentWidget extends ConsumerStatefulWidget {
this.hideAddButton = false,
this.disableDropdown = false,
this.showCashDenomination = false,
this.totalDueAmount,
this.onPaymentListChanged,
this.initialTransactions,
});
@@ -135,6 +137,39 @@ class MultiPaymentWidgetState extends ConsumerState<MultiPaymentWidget> {
_isSyncing = false;
}
@override
void didUpdateWidget(MultiPaymentWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.totalDueAmount != oldWidget.totalDueAmount && widget.totalDueAmount != null && widget.totalDueAmount! > 0) {
if (_paymentEntries.length == 1) {
if (widget.totalDueAmount! % 1 == 0) {
_paymentEntries[0].amountController.text = widget.totalDueAmount!.toInt().toString();
} else {
_paymentEntries[0].amountController.text = widget.totalDueAmount!.toStringAsFixed(2);
}
_calculateTotalsFromPayments();
} else if (_paymentEntries.length > 1) {
// Adjust the last entry
double otherTotals = 0;
for (int i = 0; i < _paymentEntries.length - 1; i++) {
otherTotals += double.tryParse(_paymentEntries[i].amountController.text) ?? 0;
}
double remaining = widget.totalDueAmount!.toDouble() - otherTotals;
if (remaining > 0) {
if (remaining % 1 == 0) {
_paymentEntries.last.amountController.text = remaining.toInt().toString();
} else {
_paymentEntries.last.amountController.text = remaining.toStringAsFixed(2);
}
} else {
_paymentEntries.last.amountController.text = '0';
}
_calculateTotalsFromPayments();
}
}
}
// Listener for all payment amount fields
void _calculateTotalsFromPayments() {
if (_isSyncing) return;
@@ -290,6 +325,25 @@ class MultiPaymentWidgetState extends ConsumerState<MultiPaymentWidget> {
: (value) {
setState(() {
payment.type = value;
if (widget.totalDueAmount != null && widget.totalDueAmount! > 0) {
double otherTotals = 0;
for (var p in _paymentEntries) {
if (p != payment) {
otherTotals += double.tryParse(p.amountController.text) ?? 0;
}
}
double remaining = widget.totalDueAmount!.toDouble() - otherTotals;
if (remaining > 0) {
if (remaining % 1 == 0) {
payment.amountController.text = remaining.toInt().toString();
} else {
payment.amountController.text = remaining.toStringAsFixed(2);
}
} else {
payment.amountController.text = '0';
}
_calculateTotalsFromPayments();
}
});
},
validator: (value) {