feat: enhance sales module with decimal formatting and input improvements

- Updated various components to utilize `FieldCurrencyInput` for quantity inputs, improving user experience in financial data entry.
- Introduced `formatDecimal` utility for consistent decimal formatting across the application, ensuring accurate display of numeric values.
- Refactored quantity handling in forms and tables to support decimal values, enhancing data integrity and usability.
- Added unit tests for new decimal formatting functionality to ensure reliability and correctness.

These changes improve the sales module's handling of numeric inputs and displays, providing a more user-friendly experience for managing financial data.
This commit is contained in:
shancheas
2026-08-31 16:27:55 +07:00
parent 9d0cde7252
commit 6808a681b7
11 changed files with 70 additions and 16 deletions
@@ -11,7 +11,7 @@ import {
Text,
} from '@repo/ui/components';
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
import { formatRupiah } from '@repo/utils';
import { formatDecimal, formatRupiah } from '@repo/utils';
import { salesInvoicesDataService } from '../../../../../sales/invoices/domain/factories';
import { packingSlipsModuleDataService } from '../../../../packing-slips/domain/factories';
import { relationLabel } from '../../../../shared/relation-label';
@@ -171,7 +171,7 @@ function DocumentProductsTable({
return (
<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">{formatDecimal(line.quantity) || '-'}</Table.Td>
<Table.Td ta="right">{line.price ? formatRupiah(line.price) : '-'}</Table.Td>
<Table.Td ta="right">{formatRupiah(lineTotal)}</Table.Td>
</Table.Tr>
@@ -1,9 +1,10 @@
import { Button, FieldTextInput, Group, Modal, Stack, Text } from '@repo/ui/components';
import { Button, FieldCurrencyInput, Group, Modal, Stack, Text } from '@repo/ui/components';
import { useEffect } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
import { relationLabel } from '../../field/shared/relation-label';
import type { SalesLineEntity } from './sales-document.entity';
import { toDecimalStringValue } from '../../../../../core/domain/decimal-string.schema';
export function CompletePackingModal({
opened,
@@ -17,7 +18,7 @@ export function CompletePackingModal({
onSubmit: (products: Array<{ productId: string; quantity: string }>) => Promise<void> | void;
}) {
const { t } = useEnterpriseModuleTranslationContext();
const form = useForm<{ products: Array<{ productId: string; quantity: string; label: string }> }>({
const form = useForm<{ products: Array<{ productId: string; quantity: string | number; label: string }> }>({
defaultValues: { products: [] },
});
const { fields } = useFieldArray({ control: form.control, name: 'products' });
@@ -37,7 +38,7 @@ export function CompletePackingModal({
await onSubmit(
values.products.map((line) => ({
productId: line.productId,
quantity: line.quantity,
quantity: toDecimalStringValue(line.quantity) ?? '',
})),
);
onClose();
@@ -49,10 +50,11 @@ export function CompletePackingModal({
<Stack gap="md">
<Text size="sm">{t('complete_packing_help')}</Text>
{fields.map((field, index) => (
<FieldTextInput
<FieldCurrencyInput
key={field.id}
control={form.control as any}
name={`products.${index}.quantity`}
prefix=""
label={`${t('delivered_quantity')}${form.getValues(`products.${index}.label`) || field.label}`}
required
/>
@@ -1,6 +1,6 @@
import { Box, Paper, Table, Text } from '@repo/ui/components';
import { useDetailPageContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
import { formatRupiah } from '@repo/utils';
import { formatDecimal, formatRupiah } from '@repo/utils';
import type { SalesDocumentEntity } from './sales-document.entity';
import { relationLabel } from '../../field/shared/relation-label';
@@ -47,7 +47,7 @@ export function DetailProducts() {
return (
<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">{formatDecimal(line.quantity) || '-'}</Table.Td>
<Table.Td ta="right">{line.price ? formatRupiah(line.price) : '-'}</Table.Td>
<Table.Td ta="right">{formatRupiah(total)}</Table.Td>
</Table.Tr>
@@ -4,7 +4,6 @@ import {
Button,
FieldAsyncSelect,
FieldCurrencyInput,
FieldTextInput,
Group,
Paper,
Table,
@@ -19,7 +18,7 @@ import { excludeProductIds, selectedProductIdsExceptLine } from './sales-line-op
import { relationLabel } from '../../field/shared/relation-label';
import type { ProductEntity } from '../../configuration/products/domain/entities';
function lineTotal(quantity?: string, price?: string | number) {
function lineTotal(quantity?: string | number, price?: string | number) {
const qty = Number(quantity);
const unitPrice = Number(price);
if (!Number.isFinite(qty) || !Number.isFinite(unitPrice)) return 0;
@@ -35,7 +34,8 @@ export function FormProducts() {
});
const products = useWatch({ control: formControl.control, name: 'products' }) ?? [];
const grandTotal = products.reduce(
(sum: number, line: { quantity?: string; price?: string | number }) => sum + lineTotal(line?.quantity, line.price),
(sum: number, line: { quantity?: string | number; price?: string | number }) =>
sum + lineTotal(line?.quantity, line.price),
0,
);
@@ -78,7 +78,12 @@ export function FormProducts() {
/>
</Table.Td>
<Table.Td miw={120}>
<FieldTextInput control={formControl.control} name={`products.${index}.quantity`} radius="md" />
<FieldCurrencyInput
control={formControl.control}
name={`products.${index}.quantity`}
prefix=""
radius="md"
/>
</Table.Td>
<Table.Td miw={140}>
<FieldCurrencyInput control={formControl.control} name={`products.${index}.price`} radius="md" />
@@ -127,7 +127,7 @@ export function toSalesWritePayload(
if (!productId) return null;
return omitEmptyFields({
productId,
quantity: line.quantity,
quantity: toDecimalStringValue(line.quantity),
price: toDecimalStringValue(line.price),
});
})
@@ -5,7 +5,11 @@ import {
optionalLatitudeSchema,
optionalLongitudeSchema,
} from '../../../../../core/domain/configuration-field-validators';
import { decimalStringSchema, optionalDecimalStringSchema } from '../../../../../core/domain/decimal-string.schema';
import {
decimalStringSchema,
optionalDecimalStringSchema,
PRICE_DECIMAL_SCALE,
} from '../../../../../core/domain/decimal-string.schema';
const NOTES_MAX = 1024;
const IMAGE_URL_MAX = 2048;
@@ -30,7 +34,7 @@ export function salesLineSchema(t: (key: string) => string) {
return z.object({
id: z.string().optional(),
product: relationSchema,
quantity: decimalStringSchema(t, 'common:fields.quantity'),
quantity: decimalStringSchema(t, 'common:fields.quantity', PRICE_DECIMAL_SCALE),
price: optionalDecimalStringSchema(t, 'common:fields.price'),
});
}