diff --git a/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/form-documents-preview.tsx b/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/form-documents-preview.tsx
index 6382be5..80feed6 100644
--- a/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/form-documents-preview.tsx
+++ b/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/form-documents-preview.tsx
@@ -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 (
{relationLabel(line.product) || line.productId}
- {line.quantity}
+ {formatDecimal(line.quantity) || '-'}
{line.price ? formatRupiah(line.price) : '-'}
{formatRupiah(lineTotal)}
diff --git a/apps/web/src/apps/main/modules/sales/shared/complete-packing-modal.tsx b/apps/web/src/apps/main/modules/sales/shared/complete-packing-modal.tsx
index f340616..8307fac 100644
--- a/apps/web/src/apps/main/modules/sales/shared/complete-packing-modal.tsx
+++ b/apps/web/src/apps/main/modules/sales/shared/complete-packing-modal.tsx
@@ -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;
}) {
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({
{t('complete_packing_help')}
{fields.map((field, index) => (
-
diff --git a/apps/web/src/apps/main/modules/sales/shared/detail-products.tsx b/apps/web/src/apps/main/modules/sales/shared/detail-products.tsx
index ce4f1de..55ee62f 100644
--- a/apps/web/src/apps/main/modules/sales/shared/detail-products.tsx
+++ b/apps/web/src/apps/main/modules/sales/shared/detail-products.tsx
@@ -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 (
{relationLabel(line.product) || line.productId}
- {line.quantity}
+ {formatDecimal(line.quantity) || '-'}
{line.price ? formatRupiah(line.price) : '-'}
{formatRupiah(total)}
diff --git a/apps/web/src/apps/main/modules/sales/shared/form-products.tsx b/apps/web/src/apps/main/modules/sales/shared/form-products.tsx
index 47f9252..94c8199 100644
--- a/apps/web/src/apps/main/modules/sales/shared/form-products.tsx
+++ b/apps/web/src/apps/main/modules/sales/shared/form-products.tsx
@@ -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() {
/>
-
+
diff --git a/apps/web/src/apps/main/modules/sales/shared/sales-document.mapper.ts b/apps/web/src/apps/main/modules/sales/shared/sales-document.mapper.ts
index aee74c8..56fb56a 100644
--- a/apps/web/src/apps/main/modules/sales/shared/sales-document.mapper.ts
+++ b/apps/web/src/apps/main/modules/sales/shared/sales-document.mapper.ts
@@ -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),
});
})
diff --git a/apps/web/src/apps/main/modules/sales/shared/sales-document.validator.ts b/apps/web/src/apps/main/modules/sales/shared/sales-document.validator.ts
index 6b2d60a..a6cb571 100644
--- a/apps/web/src/apps/main/modules/sales/shared/sales-document.validator.ts
+++ b/apps/web/src/apps/main/modules/sales/shared/sales-document.validator.ts
@@ -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'),
});
}
diff --git a/packages/ui/src/components/field-value/index.ts b/packages/ui/src/components/field-value/index.ts
index 554a705..a658cb9 100644
--- a/packages/ui/src/components/field-value/index.ts
+++ b/packages/ui/src/components/field-value/index.ts
@@ -1,3 +1,4 @@
export * from './field-value';
export * from './render-date';
export * from './render-currency';
+export * from './render-decimal';
diff --git a/packages/ui/src/components/field-value/render-decimal.test.tsx b/packages/ui/src/components/field-value/render-decimal.test.tsx
new file mode 100644
index 0000000..3c429a9
--- /dev/null
+++ b/packages/ui/src/components/field-value/render-decimal.test.tsx
@@ -0,0 +1,15 @@
+import { describe, expect, it } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { RenderDecimal } from './render-decimal';
+
+describe('RenderDecimal', () => {
+ it('renders two display decimals without a currency prefix', () => {
+ render();
+ expect(screen.getByText('2,00')).toBeInTheDocument();
+ });
+
+ it('renders the fallback when empty', () => {
+ render();
+ expect(screen.getByText('-')).toBeInTheDocument();
+ });
+});
diff --git a/packages/ui/src/components/field-value/render-decimal.tsx b/packages/ui/src/components/field-value/render-decimal.tsx
new file mode 100644
index 0000000..7c099a7
--- /dev/null
+++ b/packages/ui/src/components/field-value/render-decimal.tsx
@@ -0,0 +1,11 @@
+import { formatDecimal } from '@repo/utils';
+
+export interface RenderDecimalProps {
+ value?: string | number | null;
+ fallback?: string;
+}
+
+export function RenderDecimal({ value, fallback = '-' }: RenderDecimalProps) {
+ const formatted = formatDecimal(value);
+ return <>{formatted || fallback}>;
+}
diff --git a/packages/utils/src/currency/currency.utils.test.ts b/packages/utils/src/currency/currency.utils.test.ts
index 9383fa1..5f205ca 100644
--- a/packages/utils/src/currency/currency.utils.test.ts
+++ b/packages/utils/src/currency/currency.utils.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
-import { CurrencyUtils, formatRupiah, toCurrencyNumber } from './currency.utils';
+import { CurrencyUtils, formatDecimal, formatRupiah, toCurrencyNumber } from './currency.utils';
describe('CurrencyUtils', () => {
let currency: CurrencyUtils;
@@ -140,6 +140,15 @@ describe('CurrencyUtils', () => {
});
});
+ describe('formatDecimal', () => {
+ it('should format with two display decimals and no currency prefix', () => {
+ const stored = '2.0000';
+ expect(formatDecimal(stored)).toBe('2,00');
+ expect(formatDecimal('1234.5678')).toBe('1.234,57');
+ expect(stored).toBe('2.0000');
+ });
+ });
+
describe('toCurrencyNumber', () => {
it('should keep numeric values as numbers', () => {
expect(toCurrencyNumber(12500.12345)).toBe(12500.12345);
diff --git a/packages/utils/src/currency/currency.utils.ts b/packages/utils/src/currency/currency.utils.ts
index 682d6ef..f5b9e33 100644
--- a/packages/utils/src/currency/currency.utils.ts
+++ b/packages/utils/src/currency/currency.utils.ts
@@ -192,6 +192,13 @@ export function formatRupiah(value: CurrencyInput): string {
return displayCurrency.format(value);
}
+const displayDecimal = new CurrencyUtils({ prefix: '', decimalScale: CURRENCY_DISPLAY_SCALE });
+
+/** Format a numeric value with two display decimals and no currency prefix. */
+export function formatDecimal(value: CurrencyInput): string {
+ return displayDecimal.format(value);
+}
+
/**
* Coerce a stored price (number or decimal string) into a NumberInput value.
* Empty / invalid inputs stay as '' so the form field can remain blank.