fix(dashboard): fix currency input locale formatting (#12812)

* fix: refund forms and format currency util

* fix: claim form

* fix: return form

* fix: exchange form
This commit is contained in:
Frane Polić
2025-06-25 09:23:49 +02:00
committed by GitHub
parent 6ca755ede7
commit 9d61bb7e71
6 changed files with 183 additions and 76 deletions
@@ -1,5 +1,5 @@
export const formatCurrency = (amount: number, currency: string) => { export const formatCurrency = (amount: number, currency: string) => {
return new Intl.NumberFormat("en-US", { return new Intl.NumberFormat(undefined, {
style: "currency", style: "currency",
currency, currency,
signDisplay: "auto", signDisplay: "auto",
@@ -5,7 +5,6 @@ import {
clx, clx,
CurrencyInput, CurrencyInput,
Divider, Divider,
Input,
Label, Label,
RadioGroup, RadioGroup,
Select, Select,
@@ -15,8 +14,10 @@ import {
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { formatValue } from "react-currency-input-field" import { formatValue } from "react-currency-input-field"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useSearchParams } from "react-router-dom"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import { RouteDrawer, useRouteModal } from "../../../../../components/modals" import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form" import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
@@ -33,13 +34,19 @@ const OrderBalanceSettlementSchema = zod.object({
settlement_type: zod.enum(["credit_line", "refund"]), settlement_type: zod.enum(["credit_line", "refund"]),
refund: zod refund: zod
.object({ .object({
amount: zod.string().or(zod.number()).optional(), amount: zod.object({
value: zod.string().or(zod.number()).optional(),
float: zod.number().or(zod.null()),
}),
note: zod.string().optional(), note: zod.string().optional(),
}) })
.optional(), .optional(),
credit_line: zod credit_line: zod
.object({ .object({
amount: zod.string().or(zod.number()).optional(), amount: zod.object({
value: zod.string().or(zod.number()).optional(),
float: zod.number().or(zod.null()),
}),
note: zod.string().optional(), note: zod.string().optional(),
}) })
.optional(), .optional(),
@@ -51,19 +58,30 @@ export const OrderBalanceSettlementForm = ({
order: AdminOrder order: AdminOrder
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [searchParams] = useSearchParams()
const { handleSuccess } = useRouteModal() const { handleSuccess } = useRouteModal()
const [activePayment, setActivePayment] = useState<AdminPayment | null>(null) const paymentId = searchParams.get("paymentId")
const payments = getPaymentsFromOrder(order) const payments = getPaymentsFromOrder(order)
const pendingDifference = order.summary.pending_difference * -1 const pendingDifference = order.summary.pending_difference * -1
const [activePayment, setActivePayment] = useState<AdminPayment | null>(
paymentId ? payments.find((p) => p.id === paymentId) || null : null
)
const form = useForm<zod.infer<typeof OrderBalanceSettlementSchema>>({ const form = useForm<zod.infer<typeof OrderBalanceSettlementSchema>>({
defaultValues: { defaultValues: {
settlement_type: "refund", settlement_type: "refund",
refund: { refund: {
amount: 0, amount: {
value: "",
float: null,
},
}, },
credit_line: { credit_line: {
amount: 0, amount: {
value: "",
float: null,
},
}, },
}, },
resolver: zodResolver(OrderBalanceSettlementSchema), resolver: zodResolver(OrderBalanceSettlementSchema),
@@ -79,9 +97,14 @@ export const OrderBalanceSettlementForm = ({
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
if (data.settlement_type === "credit_line") { if (data.settlement_type === "credit_line") {
if (data.credit_line?.amount.float === null) {
return
}
await createCreditLine( await createCreditLine(
{ {
amount: parseFloat(data.credit_line!.amount! as string) * -1, amount: data.credit_line!.amount.float! * -1,
reference: "refund",
reference_id: order.id,
}, },
{ {
onSuccess: () => { onSuccess: () => {
@@ -97,9 +120,12 @@ export const OrderBalanceSettlementForm = ({
} }
if (data.settlement_type === "refund") { if (data.settlement_type === "refund") {
if (data.refund?.amount.float === null) {
return
}
await createRefund( await createRefund(
{ {
amount: parseFloat(data.refund!.amount! as string), amount: data.refund!.amount!.float!,
note: data.refund!.note, note: data.refund!.note,
}, },
{ {
@@ -107,7 +133,7 @@ export const OrderBalanceSettlementForm = ({
toast.success( toast.success(
t("orders.payment.refundPaymentSuccess", { t("orders.payment.refundPaymentSuccess", {
amount: formatCurrency( amount: formatCurrency(
parseFloat(data.refund!.amount! as string), data.refund!.amount!.float!,
order.currency_code! order.currency_code!
), ),
}) })
@@ -131,18 +157,23 @@ export const OrderBalanceSettlementForm = ({
useEffect(() => { useEffect(() => {
form.clearErrors() form.clearErrors()
const minimum = activePayment?.amount const _minimum = activePayment?.amount
? Math.min(pendingDifference, activePayment.amount) ? Math.min(pendingDifference, activePayment.amount)
: pendingDifference : pendingDifference
const minimum = {
value: _minimum.toFixed(currency.decimal_digits),
float: _minimum,
}
if (settlementType === "refund") { if (settlementType === "refund") {
form.setValue("refund.amount", activePayment ? minimum : 0) form.setValue("refund.amount", minimum)
} }
if (settlementType === "credit_line") { if (settlementType === "credit_line") {
form.setValue("credit_line.amount", minimum) form.setValue("credit_line.amount", minimum)
} }
}, [settlementType, activePayment, pendingDifference, form]) }, [settlementType, activePayment, pendingDifference, form, currency])
return ( return (
<RouteDrawer.Form form={form}> <RouteDrawer.Form form={form}>
@@ -194,6 +225,7 @@ export const OrderBalanceSettlementForm = ({
<> <>
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<Select <Select
defaultValue={activePayment?.id}
onValueChange={(value) => { onValueChange={(value) => {
setActivePayment(payments.find((p) => p.id === value)!) setActivePayment(payments.find((p) => p.id === value)!)
}} }}
@@ -260,9 +292,12 @@ export const OrderBalanceSettlementForm = ({
decimalScale={currency.decimal_digits} decimalScale={currency.decimal_digits}
symbol={currency.symbol_native} symbol={currency.symbol_native}
code={currency.code} code={currency.code}
value={field.value} value={field.value.value}
onValueChange={(_value, _name, values) => onValueChange={(_value, _name, values) =>
onChange(values?.value ? values?.value : "") onChange({
value: values?.value,
float: values?.float || null,
})
} }
autoFocus autoFocus
/> />
@@ -315,10 +350,13 @@ export const OrderBalanceSettlementForm = ({
decimalScale={currency.decimal_digits} decimalScale={currency.decimal_digits}
symbol={currency.symbol_native} symbol={currency.symbol_native}
code={currency.code} code={currency.code}
value={field.value} value={field.value.value}
onValueChange={(_value, _name, values) => onValueChange={(_value, _name, values) => {
onChange(values?.value ? values?.value : "") onChange({
} value: values?.value,
float: values?.float || null,
})
}}
autoFocus autoFocus
/> />
</Form.Control> </Form.Control>
@@ -87,10 +87,16 @@ export const ClaimCreateForm = ({
useState(false) useState(false)
const [customInboundShippingAmount, setCustomInboundShippingAmount] = const [customInboundShippingAmount, setCustomInboundShippingAmount] =
useState<number | string>(0) useState<{ value: string; float: number | null }>({
value: "0",
float: 0,
})
const [customOutboundShippingAmount, setCustomOutboundShippingAmount] = const [customOutboundShippingAmount, setCustomOutboundShippingAmount] =
useState<number | string>(0) useState<{ value: string; float: number | null }>({
value: "0",
float: 0,
})
const [inventoryMap, setInventoryMap] = useState< const [inventoryMap, setInventoryMap] = useState<
Record<string, InventoryLevelDTO[]> Record<string, InventoryLevelDTO[]>
@@ -263,13 +269,23 @@ export const ClaimCreateForm = ({
useEffect(() => { useEffect(() => {
if (inboundShipping) { if (inboundShipping) {
setCustomInboundShippingAmount(inboundShipping.total) setCustomInboundShippingAmount({
value: inboundShipping.total.toFixed(
currencies[order.currency_code.toUpperCase()].decimal_digits
),
float: inboundShipping.total,
})
} }
}, [inboundShipping]) }, [inboundShipping])
useEffect(() => { useEffect(() => {
if (outboundShipping) { if (outboundShipping) {
setCustomOutboundShippingAmount(outboundShipping.total) setCustomOutboundShippingAmount({
value: outboundShipping.total.toFixed(
currencies[order.currency_code.toUpperCase()].decimal_digits
),
float: outboundShipping.total,
})
} }
}, [outboundShipping]) }, [outboundShipping])
@@ -519,6 +535,7 @@ export const ClaimCreateForm = ({
).variants ).variants
variants.forEach((variant) => { variants.forEach((variant) => {
// TODO: fix this for inventory kits
ret[variant.id] = variant.inventory?.[0]?.location_levels || [] ret[variant.id] = variant.inventory?.[0]?.location_levels || []
}) })
@@ -560,6 +577,15 @@ export const ClaimCreateForm = ({
return (method?.total as number) || 0 return (method?.total as number) || 0
}, [preview.shipping_methods]) }, [preview.shipping_methods])
const outboundShippingTotal = useMemo(() => {
const method = preview.shipping_methods.find(
(sm) =>
!!sm.actions?.find((a) => a.action === "SHIPPING_ADD" && !a.return_id)
)
return (method?.total as number) || 0
}, [preview.shipping_methods])
return ( return (
<RouteFocusModal.Form form={form}> <RouteFocusModal.Form form={form}>
<KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col"> <KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col">
@@ -866,10 +892,7 @@ export const ClaimCreateForm = ({
} }
}) })
const customPrice = const customPrice = customInboundShippingAmount.float
customInboundShippingAmount === ""
? null
: parseFloat(customInboundShippingAmount)
if (actionId) { if (actionId) {
updateInboundShipping( updateInboundShipping(
@@ -891,8 +914,13 @@ export const ClaimCreateForm = ({
.symbol_native .symbol_native
} }
code={order.currency_code} code={order.currency_code}
onValueChange={setCustomInboundShippingAmount} onValueChange={(value, _name, values) => {
value={customInboundShippingAmount} setCustomInboundShippingAmount({
value: values?.value || "",
float: values?.float || null,
})
}}
value={customInboundShippingAmount.value}
disabled={showInboundItemsPlaceholder} disabled={showInboundItemsPlaceholder}
/> />
) : ( ) : (
@@ -937,10 +965,7 @@ export const ClaimCreateForm = ({
} }
}) })
const customPrice = const customPrice = customOutboundShippingAmount.float
customOutboundShippingAmount === ""
? null
: parseFloat(customOutboundShippingAmount)
if (actionId) { if (actionId) {
updateOutboundShipping( updateOutboundShipping(
@@ -962,13 +987,18 @@ export const ClaimCreateForm = ({
.symbol_native .symbol_native
} }
code={order.currency_code} code={order.currency_code}
onValueChange={setCustomOutboundShippingAmount} onValueChange={(value, _name, values) => {
value={customOutboundShippingAmount} setCustomOutboundShippingAmount({
value: values?.value || "",
float: values?.float || null,
})
}}
value={customOutboundShippingAmount.value}
disabled={showOutboundItemsPlaceholder} disabled={showOutboundItemsPlaceholder}
/> />
) : ( ) : (
getStylizedAmount( getStylizedAmount(
outboundShipping?.amount ?? 0, outboundShippingTotal,
order.currency_code order.currency_code
) )
)} )}
@@ -60,10 +60,18 @@ export const ExchangeCreateForm = ({
useState(false) useState(false)
const [isOutboundShippingPriceEdit, setIsOutboundShippingPriceEdit] = const [isOutboundShippingPriceEdit, setIsOutboundShippingPriceEdit] =
useState(false) useState(false)
const [customInboundShippingAmount, setCustomInboundShippingAmount] = const [customInboundShippingAmount, setCustomInboundShippingAmount] =
useState<number | string>(0) useState<{ value: string; float: number | null }>({
value: "0",
float: 0,
})
const [customOutboundShippingAmount, setCustomOutboundShippingAmount] = const [customOutboundShippingAmount, setCustomOutboundShippingAmount] =
useState<number | string>(0) useState<{ value: string; float: number | null }>({
value: "0",
float: 0,
})
/** /**
* MUTATIONS * MUTATIONS
@@ -252,6 +260,15 @@ export const ExchangeCreateForm = ({
return (method?.total as number) || 0 return (method?.total as number) || 0
}, [preview.shipping_methods]) }, [preview.shipping_methods])
const outboundShippingTotal = useMemo(() => {
const method = preview.shipping_methods.find(
(sm) =>
!!sm.actions?.find((a) => a.action === "SHIPPING_ADD" && !a.return_id)
)
return (method?.total as number) || 0
}, [preview.shipping_methods])
return ( return (
<RouteFocusModal.Form form={form}> <RouteFocusModal.Form form={form}>
<KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col"> <KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col">
@@ -356,10 +373,7 @@ export const ExchangeCreateForm = ({
} }
}) })
const customPrice = const customPrice = customInboundShippingAmount.float
customInboundShippingAmount === ""
? null
: parseFloat(customInboundShippingAmount)
if (actionId) { if (actionId) {
updateInboundShipping( updateInboundShipping(
@@ -381,8 +395,13 @@ export const ExchangeCreateForm = ({
.symbol_native .symbol_native
} }
code={order.currency_code} code={order.currency_code}
onValueChange={setCustomInboundShippingAmount} onValueChange={(value, name, values) =>
value={customInboundShippingAmount} setCustomInboundShippingAmount({
value: values?.value || "",
float: values?.float || null,
})
}
value={customInboundShippingAmount.value}
disabled={!inboundPreviewItems?.length} disabled={!inboundPreviewItems?.length}
/> />
) : ( ) : (
@@ -427,10 +446,7 @@ export const ExchangeCreateForm = ({
} }
}) })
const customPrice = const customPrice = customOutboundShippingAmount.float
customOutboundShippingAmount === ""
? null
: parseFloat(customOutboundShippingAmount)
if (actionId) { if (actionId) {
updateOutboundShipping( updateOutboundShipping(
@@ -452,13 +468,18 @@ export const ExchangeCreateForm = ({
.symbol_native .symbol_native
} }
code={order.currency_code} code={order.currency_code}
onValueChange={setCustomOutboundShippingAmount} onValueChange={(value, name, values) =>
value={customOutboundShippingAmount} setCustomOutboundShippingAmount({
value: values?.value || "",
float: values?.float || null,
})
}
value={customOutboundShippingAmount.value}
disabled={!outboundPreviewItems?.length} disabled={!outboundPreviewItems?.length}
/> />
) : ( ) : (
getStylizedAmount( getStylizedAmount(
outboundShipping?.amount ?? 0, outboundShippingTotal,
order.currency_code order.currency_code
) )
)} )}
@@ -8,11 +8,11 @@ import {
Textarea, Textarea,
toast, toast,
} from "@medusajs/ui" } from "@medusajs/ui"
import { useEffect, useMemo } from "react" import { useEffect, useMemo, useState } from "react"
import { formatValue } from "react-currency-input-field" import { formatValue } from "react-currency-input-field"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useNavigate, useSearchParams } from "react-router-dom" import { useSearchParams } from "react-router-dom"
import * as zod from "zod" import * as zod from "zod"
import { Form } from "../../../../../components/common/form" import { Form } from "../../../../../components/common/form"
import { RouteDrawer, useRouteModal } from "../../../../../components/modals" import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
@@ -29,16 +29,21 @@ type CreateRefundFormProps = {
} }
const CreateRefundSchema = zod.object({ const CreateRefundSchema = zod.object({
amount: zod.string().or(zod.number()), amount: zod.object({
value: zod.string().or(zod.number()),
float: zod.number().or(zod.null()),
}),
note: zod.string().optional(), note: zod.string().optional(),
}) })
export const CreateRefundForm = ({ order }: CreateRefundFormProps) => { export const CreateRefundForm = ({ order }: CreateRefundFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal() const { handleSuccess } = useRouteModal()
const navigate = useNavigate()
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const paymentId = searchParams.get("paymentId") const [paymentId, setPaymentId] = useState<string | undefined>(
searchParams.get("paymentId") || undefined
)
const payments = getPaymentsFromOrder(order) const payments = getPaymentsFromOrder(order)
const payment = payments.find((p) => p.id === paymentId)! const payment = payments.find((p) => p.id === paymentId)!
const paymentAmount = payment?.amount || 0 const paymentAmount = payment?.amount || 0
@@ -50,7 +55,10 @@ export const CreateRefundForm = ({ order }: CreateRefundFormProps) => {
const form = useForm<zod.infer<typeof CreateRefundSchema>>({ const form = useForm<zod.infer<typeof CreateRefundSchema>>({
defaultValues: { defaultValues: {
amount: paymentAmount, amount: {
value: paymentAmount.toFixed(currency.decimal_digits),
float: paymentAmount,
},
note: "", note: "",
}, },
resolver: zodResolver(CreateRefundSchema), resolver: zodResolver(CreateRefundSchema),
@@ -61,21 +69,24 @@ export const CreateRefundForm = ({ order }: CreateRefundFormProps) => {
const paymentAmount = (payment?.amount || 0) as number const paymentAmount = (payment?.amount || 0) as number
const pendingAmount = const pendingAmount =
pendingDifference < 0 pendingDifference < 0
? Math.min(pendingDifference, paymentAmount) ? Math.min(Math.abs(pendingDifference), paymentAmount)
: paymentAmount : paymentAmount
const normalizedAmount = const normalizedAmount =
pendingAmount < 0 ? pendingAmount * -1 : pendingAmount pendingAmount < 0 ? pendingAmount * -1 : pendingAmount
form.setValue("amount", normalizedAmount as number) form.setValue("amount", {
}, [payment]) value: normalizedAmount.toFixed(currency.decimal_digits),
float: normalizedAmount,
})
}, [payment?.id || ""])
const { mutateAsync, isPending } = useRefundPayment(order.id, payment?.id!) const { mutateAsync, isPending } = useRefundPayment(order.id, payment?.id!)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync( await mutateAsync(
{ {
amount: parseFloat(data.amount as string), amount: data.amount.float!,
note: data.note, note: data.note,
}, },
{ {
@@ -83,7 +94,7 @@ export const CreateRefundForm = ({ order }: CreateRefundFormProps) => {
toast.success( toast.success(
t("orders.payment.refundPaymentSuccess", { t("orders.payment.refundPaymentSuccess", {
amount: formatCurrency( amount: formatCurrency(
data.amount as number, data.amount.float!,
payment?.currency_code! payment?.currency_code!
), ),
}) })
@@ -107,11 +118,9 @@ export const CreateRefundForm = ({ order }: CreateRefundFormProps) => {
<RouteDrawer.Body className="flex-1 overflow-auto"> <RouteDrawer.Body className="flex-1 overflow-auto">
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<Select <Select
value={payment?.id} value={paymentId}
onValueChange={(value) => { onValueChange={(value) => {
navigate(`/orders/${order.id}/refund?paymentId=${value}`, { setPaymentId(value)
replace: true,
})
}} }}
> >
<Label className="txt-compact-small mb-[-6px] font-sans font-medium"> <Label className="txt-compact-small mb-[-6px] font-sans font-medium">
@@ -179,9 +188,12 @@ export const CreateRefundForm = ({ order }: CreateRefundFormProps) => {
decimalScale={currency.decimal_digits} decimalScale={currency.decimal_digits}
symbol={currency.symbol_native} symbol={currency.symbol_native}
code={currency.code} code={currency.code}
value={field.value} value={field.value.value}
onValueChange={(_value, _name, values) => onValueChange={(_value, _name, values) =>
onChange(values?.value ? values?.value : "") onChange({
value: values?.value,
float: values?.float || null,
})
} }
autoFocus autoFocus
/> />
@@ -94,7 +94,13 @@ export const ReturnCreateForm = ({
*/ */
const { setIsOpen } = useStackedModal() const { setIsOpen } = useStackedModal()
const [isShippingPriceEdit, setIsShippingPriceEdit] = useState(false) const [isShippingPriceEdit, setIsShippingPriceEdit] = useState(false)
const [customShippingAmount, setCustomShippingAmount] = useState(0) const [customShippingAmount, setCustomShippingAmount] = useState<{
value: string
float: number | null
}>({
value: "0",
float: 0,
})
const [inventoryMap, setInventoryMap] = useState< const [inventoryMap, setInventoryMap] = useState<
Record<string, InventoryLevelDTO[]> Record<string, InventoryLevelDTO[]>
>({}) >({})
@@ -671,10 +677,7 @@ export const ReturnCreateForm = ({
if (actionId) { if (actionId) {
updateReturnShipping({ updateReturnShipping({
actionId, actionId,
custom_amount: custom_amount: customShippingAmount.float,
typeof customShippingAmount === "string"
? null
: customShippingAmount,
}) })
} }
setIsShippingPriceEdit(false) setIsShippingPriceEdit(false)
@@ -684,10 +687,13 @@ export const ReturnCreateForm = ({
.symbol_native .symbol_native
} }
code={order.currency_code} code={order.currency_code}
onValueChange={(value) => onValueChange={(value, name, values) =>
setCustomShippingAmount(value ? parseFloat(value) : "") setCustomShippingAmount({
value: values?.value || "",
float: values?.float || null,
})
} }
value={customShippingAmount} value={customShippingAmount.value}
disabled={showPlaceholder} disabled={showPlaceholder}
/> />
) : ( ) : (