feat: add currency input field and enhance currency formatting utilities
- Introduced `FieldCurrencyInput` component for handling currency input in forms, supporting formatted display while maintaining numeric values. - Implemented `RenderCurrency` component for displaying currency values with proper formatting. - Enhanced `CurrencyUtils` to include parsing and formatting functions for Rupiah, ensuring accurate representation in UI. - Updated various components and forms to utilize the new currency input and rendering capabilities, improving user experience in financial data entry. - Added unit tests for currency input and formatting functionalities to ensure reliability and correctness. These changes enhance the application's handling of currency inputs and displays, providing a more user-friendly experience for financial transactions.
This commit is contained in:
+9
@@ -64,4 +64,13 @@ describe('ProductsRemoteDataTransformer', () => {
|
||||
expect(payload).not.toHaveProperty('status');
|
||||
expect(payload).not.toHaveProperty('id');
|
||||
});
|
||||
|
||||
it('stringifies a numeric price on write without rounding', () => {
|
||||
const payload = transformer.transformCreatePayload({
|
||||
code: 'SKU_003',
|
||||
name: 'Priced Widget',
|
||||
price: 12500.12345 as unknown as string,
|
||||
});
|
||||
expect(payload.price).toBe('12500.12345');
|
||||
});
|
||||
});
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
import { BaseDataTransformer } from '@repo/core-api/data-services';
|
||||
import { emptyToNull, omitEmptyFields } from '../../../../../../../core/domain/configuration-field-validators';
|
||||
import { toDecimalStringValue } from '../../../../../../../core/domain/decimal-string.schema';
|
||||
import type { ProductDto, ProductEntity } from '../entities';
|
||||
|
||||
export class ProductsRemoteDataTransformer extends BaseDataTransformer<ProductEntity> {
|
||||
@@ -28,7 +29,7 @@ export class ProductsRemoteDataTransformer extends BaseDataTransformer<ProductEn
|
||||
code: entity.code,
|
||||
name: entity.name,
|
||||
unit: entity.unit,
|
||||
price: entity.price,
|
||||
price: toDecimalStringValue(entity.price),
|
||||
brand: entity.brand,
|
||||
});
|
||||
}
|
||||
@@ -38,7 +39,7 @@ export class ProductsRemoteDataTransformer extends BaseDataTransformer<ProductEn
|
||||
code: entity.code,
|
||||
name: entity.name,
|
||||
unit: emptyToNull(entity.unit) as string | null,
|
||||
price: emptyToNull(entity.price) as string | null,
|
||||
price: emptyToNull(toDecimalStringValue(entity.price)) as string | null,
|
||||
brand: emptyToNull(entity.brand) as string | null,
|
||||
};
|
||||
}
|
||||
|
||||
+6
-1
@@ -29,6 +29,11 @@ describe('createProductSchema', () => {
|
||||
});
|
||||
|
||||
it('rejects a non-decimal price', () => {
|
||||
expect(schema.safeParse({ ...valid, price: '12.34567' }).success).toBe(false);
|
||||
expect(schema.safeParse({ ...valid, price: 'abc' }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts a numeric price with five decimal places', () => {
|
||||
expect(schema.safeParse({ ...valid, price: 12.34567 }).success).toBe(true);
|
||||
expect(schema.safeParse({ ...valid, price: '12.34567' }).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+6
-2
@@ -1,4 +1,4 @@
|
||||
import { Box, Paper, SimpleGrid, FieldValue, RenderDate, Text, StatusBadge } from '@repo/ui/components';
|
||||
import { Box, Paper, SimpleGrid, FieldValue, RenderCurrency, RenderDate, Text, StatusBadge } from '@repo/ui/components';
|
||||
import { useDetailPageContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||
import type { ProductEntity } from '../../../domain/entities';
|
||||
|
||||
@@ -17,7 +17,11 @@ export function DetailGeneral() {
|
||||
<FieldValue label={t('common:fields.code')} value={data?.code} />
|
||||
<FieldValue label={t('common:fields.name')} value={data?.name} />
|
||||
<FieldValue label={t('common:fields.unit')} value={data?.unit} />
|
||||
<FieldValue label={t('common:fields.price')} value={data?.price} />
|
||||
<FieldValue
|
||||
label={t('common:fields.price')}
|
||||
value={data?.price}
|
||||
render={(val) => <RenderCurrency value={val as string | number | null} />}
|
||||
/>
|
||||
<FieldValue label={t('common:fields.brand')} value={data?.brand} />
|
||||
<FieldValue
|
||||
label={t('common:fields.status')}
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { Box, FieldTextInput, Paper, SimpleGrid, Text } from '@repo/ui/components';
|
||||
import { Box, FieldCurrencyInput, FieldTextInput, Paper, SimpleGrid, Text } from '@repo/ui/components';
|
||||
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
|
||||
|
||||
export function FormGeneral() {
|
||||
@@ -35,11 +35,11 @@ export function FormGeneral() {
|
||||
placeholder="e.g. PCS"
|
||||
radius="md"
|
||||
/>
|
||||
<FieldTextInput
|
||||
<FieldCurrencyInput
|
||||
control={formControl.control}
|
||||
name="price"
|
||||
label={t('common:fields.price')}
|
||||
placeholder="12500.0000"
|
||||
placeholder="Rp 12.500,00"
|
||||
radius="md"
|
||||
/>
|
||||
<FieldTextInput
|
||||
|
||||
+7
-1
@@ -7,6 +7,7 @@ import {
|
||||
import { ColDef, Text } from '@repo/ui/components';
|
||||
import { Trans } from '@repo/core-i18n';
|
||||
import { Package } from 'lucide-react';
|
||||
import { formatRupiah } from '@repo/utils';
|
||||
import { FilterFormContent } from '../components/index-component/filter-content';
|
||||
import type { ProductEntity } from '../../domain/entities';
|
||||
|
||||
@@ -18,7 +19,12 @@ export default function ProductPageIndex() {
|
||||
{ field: 'code', headerName: t('common:fields.code'), minWidth: 140 },
|
||||
{ field: 'name', headerName: t('common:fields.name'), minWidth: 180 },
|
||||
{ field: 'unit', headerName: t('common:fields.unit'), minWidth: 100 },
|
||||
{ field: 'price', headerName: t('common:fields.price'), minWidth: 140 },
|
||||
{
|
||||
field: 'price',
|
||||
headerName: t('common:fields.price'),
|
||||
minWidth: 140,
|
||||
valueFormatter: ({ value }) => formatRupiah(value) || '-',
|
||||
},
|
||||
{ field: 'brand', headerName: t('common:fields.brand'), minWidth: 140 },
|
||||
];
|
||||
}, [t]);
|
||||
|
||||
+7
-1
@@ -7,6 +7,7 @@ import {
|
||||
import { ColDef, Text } from '@repo/ui/components';
|
||||
import { Trans } from '@repo/core-i18n';
|
||||
import { Receipt } from 'lucide-react';
|
||||
import { formatRupiah } from '@repo/utils';
|
||||
import { SalesFilterFormContent } from '../../../shared/filter-content';
|
||||
import { useSalesDocumentActions } from '../../../shared/use-sales-document-actions';
|
||||
import { relationLabel } from '../../../../field/shared/relation-label';
|
||||
@@ -38,7 +39,12 @@ export default function SalesInvoicePageIndex() {
|
||||
minWidth: 160,
|
||||
valueGetter: ({ data }) => relationLabel(data?.branch) || data?.branchId,
|
||||
},
|
||||
{ field: 'balance', headerName: t('common:fields.balance'), minWidth: 140 },
|
||||
{
|
||||
field: 'balance',
|
||||
headerName: t('common:fields.balance'),
|
||||
minWidth: 140,
|
||||
valueFormatter: ({ value }) => formatRupiah(value) || '-',
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { BaseDataTransformer } from '@repo/core-api/data-services';
|
||||
import { formatDateValue, parseDateValue } from '@repo/ui/form';
|
||||
import { emptyToNull, omitEmptyFields } from '../../../../../../../core/domain/configuration-field-validators';
|
||||
import { toDecimalStringValue } from '../../../../../../../core/domain/decimal-string.schema';
|
||||
import { mapSalesImagesFromDto, relationId, toLookup } from '../../../shared/sales-document.mapper';
|
||||
import type { SalesPaymentDto, SalesPaymentEntity } from '../entities';
|
||||
|
||||
@@ -51,7 +52,7 @@ export class SalesPaymentsRemoteDataTransformer extends BaseDataTransformer<Sale
|
||||
.map((row) => {
|
||||
const invoiceId = relationId(row.invoice) ?? row.invoiceId;
|
||||
if (!invoiceId) return null;
|
||||
return omitEmptyFields({ invoiceId, amount: row.amount });
|
||||
return omitEmptyFields({ invoiceId, amount: toDecimalStringValue(row.amount) });
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { compose, maxLength, required } from '@repo/ui/validators';
|
||||
import { decimalStringSchema } from '../../../../../../../core/domain/decimal-string.schema';
|
||||
import { decimalStringSchema, PRICE_DECIMAL_SCALE } from '../../../../../../../core/domain/decimal-string.schema';
|
||||
import { salesImageSchema } from '../../../shared/sales-document.validator';
|
||||
|
||||
const NOTES_MAX = 1024;
|
||||
@@ -40,7 +40,7 @@ export function createSalesPaymentSchema(t: (key: string) => string) {
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
invoice: relationSchema,
|
||||
amount: decimalStringSchema(t, 'common:fields.amount'),
|
||||
amount: decimalStringSchema(t, 'common:fields.amount', PRICE_DECIMAL_SCALE),
|
||||
}),
|
||||
)
|
||||
.min(1),
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { Box, Paper, Table, Text } from '@repo/ui/components';
|
||||
import { useDetailPageContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||
import { formatRupiah } from '@repo/utils';
|
||||
import type { SalesPaymentEntity } from '../../../domain/entities';
|
||||
import { relationLabel } from '../../../../../field/shared/relation-label';
|
||||
|
||||
@@ -34,7 +35,7 @@ export function DetailAllocations() {
|
||||
rows.map((row, index) => (
|
||||
<Table.Tr key={row.id ?? `${row.invoiceId}-${index}`}>
|
||||
<Table.Td>{relationLabel(row.invoice) || row.invoiceId}</Table.Td>
|
||||
<Table.Td ta="right">{row.amount}</Table.Td>
|
||||
<Table.Td ta="right">{formatRupiah(row.amount)}</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
)}
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
FieldAsyncSelect,
|
||||
FieldTextInput,
|
||||
FieldCurrencyInput,
|
||||
Group,
|
||||
Paper,
|
||||
Table,
|
||||
@@ -59,7 +59,7 @@ export function FormAllocations() {
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td miw={140}>
|
||||
<FieldTextInput control={formControl.control} name={`invoices.${index}.amount`} radius="md" />
|
||||
<FieldCurrencyInput control={formControl.control} name={`invoices.${index}.amount`} radius="md" />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { Anchor, Box, Paper, SimpleGrid, FieldValue, RenderDate, Text, StatusBadge } from '@repo/ui/components';
|
||||
import {
|
||||
Anchor,
|
||||
Box,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
FieldValue,
|
||||
RenderCurrency,
|
||||
RenderDate,
|
||||
Text,
|
||||
StatusBadge,
|
||||
} from '@repo/ui/components';
|
||||
import { useDetailPageContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||
import type { SalesDocumentEntity } from './sales-document.entity';
|
||||
import { relationLabel } from '../../field/shared/relation-label';
|
||||
@@ -83,7 +93,13 @@ export function DetailGeneral({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showBalance ? <FieldValue label={t('common:fields.balance')} value={data?.balance} /> : null}
|
||||
{showBalance ? (
|
||||
<FieldValue
|
||||
label={t('common:fields.balance')}
|
||||
value={data?.balance}
|
||||
render={(val) => <RenderCurrency value={val as string | number | null} />}
|
||||
/>
|
||||
) : null}
|
||||
{salesRequestHref && (
|
||||
<FieldValue
|
||||
label={t('common:fields.salesRequest')}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Box, Paper, Table, Text } from '@repo/ui/components';
|
||||
import { useDetailPageContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||
import { CurrencyUtils } from '@repo/utils';
|
||||
import { formatRupiah } from '@repo/utils';
|
||||
import type { SalesDocumentEntity } from './sales-document.entity';
|
||||
import { relationLabel } from '../../field/shared/relation-label';
|
||||
|
||||
const currency = new CurrencyUtils({ decimalScale: 4 });
|
||||
|
||||
export function DetailProducts() {
|
||||
const { detailData } = useDetailPageContext<SalesDocumentEntity>();
|
||||
const { t } = useEnterpriseModuleTranslationContext();
|
||||
@@ -50,8 +48,8 @@ export function DetailProducts() {
|
||||
<Table.Tr key={line.id ?? `${line.productId}-${index}`}>
|
||||
<Table.Td>{relationLabel(line.product) || line.productId}</Table.Td>
|
||||
<Table.Td ta="right">{line.quantity}</Table.Td>
|
||||
<Table.Td ta="right">{line.price ? currency.format(line.price) : '-'}</Table.Td>
|
||||
<Table.Td ta="right">{currency.format(total)}</Table.Td>
|
||||
<Table.Td ta="right">{line.price ? formatRupiah(line.price) : '-'}</Table.Td>
|
||||
<Table.Td ta="right">{formatRupiah(total)}</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
})
|
||||
@@ -60,7 +58,7 @@ export function DetailProducts() {
|
||||
</Table>
|
||||
</Box>
|
||||
<Text fw={600} ta="right" mt="md">
|
||||
{t('common:fields.total')}: {currency.format(grandTotal)}
|
||||
{t('common:fields.total')}: {formatRupiah(grandTotal)}
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
FieldAsyncSelect,
|
||||
FieldCurrencyInput,
|
||||
FieldTextInput,
|
||||
Group,
|
||||
Paper,
|
||||
@@ -12,15 +13,13 @@ import {
|
||||
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
|
||||
import { useFieldArray, useWatch } from '@repo/ui/form';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { CurrencyUtils } from '@repo/utils';
|
||||
import { formatRupiah } from '@repo/utils';
|
||||
import { loadProductOptions } from './load-product-options';
|
||||
import { excludeProductIds, selectedProductIdsExceptLine } from './sales-line-options';
|
||||
import { relationLabel } from '../../field/shared/relation-label';
|
||||
import type { ProductEntity } from '../../configuration/products/domain/entities';
|
||||
|
||||
const currency = new CurrencyUtils({ decimalScale: 4 });
|
||||
|
||||
function lineTotal(quantity?: string, price?: string) {
|
||||
function lineTotal(quantity?: string, price?: string | number) {
|
||||
const qty = Number(quantity);
|
||||
const unitPrice = Number(price);
|
||||
if (!Number.isFinite(qty) || !Number.isFinite(unitPrice)) return 0;
|
||||
@@ -36,7 +35,7 @@ export function FormProducts() {
|
||||
});
|
||||
const products = useWatch({ control: formControl.control, name: 'products' }) ?? [];
|
||||
const grandTotal = products.reduce(
|
||||
(sum: number, line: { quantity?: string; price?: string }) => sum + lineTotal(line?.quantity, line.price),
|
||||
(sum: number, line: { quantity?: string; price?: string | number }) => sum + lineTotal(line?.quantity, line.price),
|
||||
0,
|
||||
);
|
||||
|
||||
@@ -82,9 +81,9 @@ export function FormProducts() {
|
||||
<FieldTextInput control={formControl.control} name={`products.${index}.quantity`} radius="md" />
|
||||
</Table.Td>
|
||||
<Table.Td miw={140}>
|
||||
<FieldTextInput control={formControl.control} name={`products.${index}.price`} radius="md" />
|
||||
<FieldCurrencyInput control={formControl.control} name={`products.${index}.price`} radius="md" />
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">{currency.format(lineTotal(line?.quantity, line?.price))}</Table.Td>
|
||||
<Table.Td ta="right">{formatRupiah(lineTotal(line?.quantity, line?.price))}</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
@@ -111,7 +110,7 @@ export function FormProducts() {
|
||||
{t('add_line')}
|
||||
</Button>
|
||||
<Text fw={600}>
|
||||
{t('common:fields.total')}: {currency.format(grandTotal)}
|
||||
{t('common:fields.total')}: {formatRupiah(grandTotal)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
export const PAYABLE_INVOICE_STATUSES = ['processed', 'partial'] as const;
|
||||
|
||||
export function isPayableInvoice(invoice: {
|
||||
status?: string | null;
|
||||
balance?: string | number | null;
|
||||
}): boolean {
|
||||
export function isPayableInvoice(invoice: { status?: string | null; balance?: string | number | null }): boolean {
|
||||
const payable: readonly string[] = PAYABLE_INVOICE_STATUSES;
|
||||
if (!invoice.status || !payable.includes(invoice.status)) {
|
||||
return false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { formatDateValue, parseDateValue } from '@repo/ui/form';
|
||||
import { emptyToNull, omitEmptyFields } from '../../../../../core/domain/configuration-field-validators';
|
||||
import { toDecimalStringValue } from '../../../../../core/domain/decimal-string.schema';
|
||||
import type {
|
||||
LookupStub,
|
||||
SalesDocumentDto,
|
||||
@@ -127,7 +128,7 @@ export function toSalesWritePayload(
|
||||
return omitEmptyFields({
|
||||
productId,
|
||||
quantity: line.quantity,
|
||||
price: line.price,
|
||||
price: toDecimalStringValue(line.price),
|
||||
});
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
optionalLatitudeSchema,
|
||||
optionalLongitudeSchema,
|
||||
} from '../../../../../core/domain/configuration-field-validators';
|
||||
import { decimalStringSchema } from '../../../../../core/domain/decimal-string.schema';
|
||||
import { decimalStringSchema, optionalDecimalStringSchema } from '../../../../../core/domain/decimal-string.schema';
|
||||
|
||||
const NOTES_MAX = 1024;
|
||||
const IMAGE_URL_MAX = 2048;
|
||||
@@ -31,7 +31,7 @@ export function salesLineSchema(t: (key: string) => string) {
|
||||
id: z.string().optional(),
|
||||
product: relationSchema,
|
||||
quantity: decimalStringSchema(t, 'common:fields.quantity'),
|
||||
price: z.preprocess(emptyToUndefined, decimalStringSchema(t, 'common:fields.price').optional()),
|
||||
price: optionalDecimalStringSchema(t, 'common:fields.price'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,7 @@ import { describe, expect, it } from 'vitest';
|
||||
import { excludeProductIds, selectedProductIdsExceptLine } from './sales-line-options';
|
||||
|
||||
describe('sales line product options', () => {
|
||||
const lines = [
|
||||
{ product: { id: 'prd-1' } },
|
||||
{ product: { id: 'prd-2' } },
|
||||
{ product: null },
|
||||
];
|
||||
const lines = [{ product: { id: 'prd-1' } }, { product: { id: 'prd-2' } }, { product: null }];
|
||||
|
||||
it('collects product ids from other lines only', () => {
|
||||
expect(selectedProductIdsExceptLine(lines, 0)).toEqual(['prd-2']);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { decimalStringSchema, optionalDecimalStringSchema, toDecimalStringValue } from './decimal-string.schema';
|
||||
|
||||
const t = (key: string) => key;
|
||||
|
||||
describe('decimalStringSchema', () => {
|
||||
const schema = decimalStringSchema(t, 'common:fields.quantity');
|
||||
|
||||
it('accepts a decimal string within four places', () => {
|
||||
expect(schema.safeParse('2.0000').success).toBe(true);
|
||||
});
|
||||
|
||||
it('coerces a number into a decimal string', () => {
|
||||
const result = schema.safeParse(12.5);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data).toBe('12.5');
|
||||
});
|
||||
|
||||
it('rejects more than four decimal places', () => {
|
||||
expect(schema.safeParse('12.34567').success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('optionalDecimalStringSchema', () => {
|
||||
const schema = optionalDecimalStringSchema(t, 'common:fields.price');
|
||||
|
||||
it('accepts five decimal places and numbers', () => {
|
||||
expect(schema.safeParse('12.34567').success).toBe(true);
|
||||
expect(schema.safeParse(12500.12345).success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts empty values', () => {
|
||||
expect(schema.safeParse('').success).toBe(true);
|
||||
expect(schema.safeParse(undefined).success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects six decimal places', () => {
|
||||
expect(schema.safeParse('12.345678').success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toDecimalStringValue', () => {
|
||||
it('keeps five decimal places when stringifying a number', () => {
|
||||
expect(toDecimalStringValue(12500.12345)).toBe('12500.12345');
|
||||
expect(toDecimalStringValue('12500.12345')).toBe('12500.12345');
|
||||
expect(toDecimalStringValue('')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,53 @@
|
||||
import { z } from 'zod';
|
||||
import { CURRENCY_DATA_SCALE } from '@repo/utils';
|
||||
|
||||
const DECIMAL_PATTERN = /^\d+(\.\d{1,4})?$/;
|
||||
export const PRICE_DECIMAL_SCALE = CURRENCY_DATA_SCALE;
|
||||
export const QUANTITY_DECIMAL_SCALE = 4;
|
||||
|
||||
function emptyToUndefined(value: unknown) {
|
||||
function coerceDecimalInput(value: unknown) {
|
||||
if (value === '' || value === null || value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) ? String(value) : value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function decimalStringSchema(t: (key: string) => string, fieldKey = 'common:fields.price') {
|
||||
return z.string().regex(DECIMAL_PATTERN, {
|
||||
message: JSON.stringify({ key: 'validation:invalid_format', values: { field: t(fieldKey) } }),
|
||||
});
|
||||
export function toDecimalStringValue(value: unknown): string | undefined {
|
||||
const next = coerceDecimalInput(value);
|
||||
return typeof next === 'string' ? next : undefined;
|
||||
}
|
||||
|
||||
export function optionalDecimalStringSchema(t: (key: string) => string, fieldKey = 'common:fields.price') {
|
||||
return z.preprocess(emptyToUndefined, decimalStringSchema(t, fieldKey).optional());
|
||||
function decimalPattern(maxDecimals: number) {
|
||||
return new RegExp(`^\\d+(\\.\\d{1,${maxDecimals}})?$`);
|
||||
}
|
||||
|
||||
export function decimalStringSchema(
|
||||
t: (key: string) => string,
|
||||
fieldKey = 'common:fields.price',
|
||||
maxDecimals = QUANTITY_DECIMAL_SCALE,
|
||||
) {
|
||||
return z.preprocess(
|
||||
coerceDecimalInput,
|
||||
z.string().regex(decimalPattern(maxDecimals), {
|
||||
message: JSON.stringify({ key: 'validation:invalid_format', values: { field: t(fieldKey) } }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function optionalDecimalStringSchema(
|
||||
t: (key: string) => string,
|
||||
fieldKey = 'common:fields.price',
|
||||
maxDecimals = PRICE_DECIMAL_SCALE,
|
||||
) {
|
||||
return z.preprocess(
|
||||
coerceDecimalInput,
|
||||
z
|
||||
.string()
|
||||
.regex(decimalPattern(maxDecimals), {
|
||||
message: JSON.stringify({ key: 'validation:invalid_format', values: { field: t(fieldKey) } }),
|
||||
})
|
||||
.optional(),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user