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.