perubahan denomuretor on pos sale
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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: () {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user