mirror of
https://github.com/we-promise/sure.git
synced 2026-04-13 09:07:25 +00:00
Merge pull request #1176 from we-promise/claude/fix-issue-1138-uRWb6
Fix decimal separator handling in money input fields
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import parseLocaleFloat from "utils/parse_locale_float"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["customWrapper", "customField", "tickerSelect", "qtyField", "priceField", "priceWarning", "priceWarningMessage"]
|
||||
@@ -42,8 +43,8 @@ export default class extends Controller {
|
||||
|
||||
// Calculate the implied/entered price
|
||||
let enteredPriceCents = null
|
||||
const qty = Number.parseFloat(this.qtyFieldTarget?.value)
|
||||
const enteredPrice = Number.parseFloat(this.priceFieldTarget?.value)
|
||||
const qty = parseLocaleFloat(this.qtyFieldTarget?.value)
|
||||
const enteredPrice = parseLocaleFloat(this.priceFieldTarget?.value)
|
||||
|
||||
if (enteredPrice && enteredPrice > 0) {
|
||||
// User entered a price directly
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import parseLocaleFloat from "utils/parse_locale_float"
|
||||
|
||||
// Handles bidirectional conversion between total cost basis and per-share cost
|
||||
// in the manual cost basis entry form.
|
||||
@@ -9,7 +10,7 @@ export default class extends Controller {
|
||||
// Called when user types in the total cost basis field
|
||||
// Updates the per-share display and input to show the calculated value
|
||||
updatePerShare() {
|
||||
const total = Number.parseFloat(this.totalTarget.value) || 0
|
||||
const total = parseLocaleFloat(this.totalTarget.value)
|
||||
const qty = this.qtyValue || 1
|
||||
const perShare = qty > 0 ? (total / qty).toFixed(2) : "0.00"
|
||||
this.perShareValueTarget.textContent = perShare
|
||||
@@ -21,7 +22,7 @@ export default class extends Controller {
|
||||
// Called when user types in the per-share field
|
||||
// Updates the total cost basis field with the calculated value
|
||||
updateTotal() {
|
||||
const perShare = Number.parseFloat(this.perShareTarget.value) || 0
|
||||
const perShare = parseLocaleFloat(this.perShareTarget.value)
|
||||
const qty = this.qtyValue || 1
|
||||
const total = (perShare * qty).toFixed(2)
|
||||
this.totalTarget.value = total
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import parseLocaleFloat from "utils/parse_locale_float"
|
||||
|
||||
// Handles the inline cost basis editor in the holding drawer.
|
||||
// Shows/hides the form and handles bidirectional total <-> per-share conversion.
|
||||
@@ -13,7 +14,7 @@ export default class extends Controller {
|
||||
|
||||
// Called when user types in total cost basis field
|
||||
updatePerShare() {
|
||||
const total = Number.parseFloat(this.totalTarget.value) || 0
|
||||
const total = parseLocaleFloat(this.totalTarget.value)
|
||||
const qty = this.qtyValue || 1
|
||||
const perShare = qty > 0 ? (total / qty).toFixed(2) : "0.00"
|
||||
this.perShareValueTarget.textContent = perShare
|
||||
@@ -24,7 +25,7 @@ export default class extends Controller {
|
||||
|
||||
// Called when user types in per-share field
|
||||
updateTotal() {
|
||||
const perShare = Number.parseFloat(this.perShareTarget.value) || 0
|
||||
const perShare = parseLocaleFloat(this.perShareTarget.value)
|
||||
const qty = this.qtyValue || 1
|
||||
const total = (perShare * qty).toFixed(2)
|
||||
this.totalTarget.value = total
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
import parseLocaleFloat from "utils/parse_locale_float";
|
||||
import { CurrenciesService } from "services/currencies_service";
|
||||
|
||||
// Connects to data-controller="money-field"
|
||||
@@ -15,10 +16,12 @@ export default class extends Controller {
|
||||
new CurrenciesService().get(currency).then((currency) => {
|
||||
this.amountTarget.step = currency.step;
|
||||
|
||||
if (Number.isFinite(this.amountTarget.value)) {
|
||||
this.amountTarget.value = Number.parseFloat(
|
||||
this.amountTarget.value,
|
||||
).toFixed(currency.default_precision);
|
||||
const rawValue = this.amountTarget.value.trim();
|
||||
if (rawValue !== "") {
|
||||
const parsedAmount = parseLocaleFloat(rawValue);
|
||||
if (Number.isFinite(parsedAmount)) {
|
||||
this.amountTarget.value = parsedAmount.toFixed(currency.default_precision);
|
||||
}
|
||||
}
|
||||
|
||||
this.symbolTarget.innerText = currency.symbol;
|
||||
|
||||
24
app/javascript/utils/parse_locale_float.js
Normal file
24
app/javascript/utils/parse_locale_float.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// Parses a float from a string that may use either commas or dots as decimal separators.
|
||||
// Handles formats like "1,234.56" (English) and "1.234,56" (French/European).
|
||||
export default function parseLocaleFloat(value) {
|
||||
if (typeof value !== "string") return Number.parseFloat(value) || 0
|
||||
|
||||
const cleaned = value.replace(/\s/g, "")
|
||||
const lastComma = cleaned.lastIndexOf(",")
|
||||
const lastDot = cleaned.lastIndexOf(".")
|
||||
|
||||
if (lastComma > lastDot) {
|
||||
// When there's no dot present and exactly 3 digits follow the last comma,
|
||||
// treat comma as a thousands separator (e.g., "1,234" → 1234, "12,345" → 12345)
|
||||
const digitsAfterComma = cleaned.length - lastComma - 1
|
||||
if (lastDot === -1 && digitsAfterComma === 3) {
|
||||
return Number.parseFloat(cleaned.replace(/,/g, "")) || 0
|
||||
}
|
||||
|
||||
// Comma is the decimal separator (e.g., "1.234,56" or "256,54")
|
||||
return Number.parseFloat(cleaned.replace(/\./g, "").replace(",", ".")) || 0
|
||||
}
|
||||
|
||||
// Dot is the decimal separator (e.g., "1,234.56" or "256.54")
|
||||
return Number.parseFloat(cleaned.replace(/,/g, "")) || 0
|
||||
}
|
||||
Reference in New Issue
Block a user