8d5a05a419
- Implemented smoke tests for UOM directionality, ensuring conversions are one-directional and correctly validated. - Added tests for receiving and selling items in different UOMs, verifying correct quantity handling and error responses. - Created a UOM conversions panel in the frontend to allow users to manage UOM conversions for items. - Introduced hooks for allowed UOMs to optimize fetching and caching of UOM data for document line forms. - Developed utility functions for consistent UOM formatting and conversion handling across the application.
74 lines
2.4 KiB
TypeScript
74 lines
2.4 KiB
TypeScript
"use client"
|
|
|
|
// Per-item allowed-UOM lookup for document line forms.
|
|
//
|
|
// Every line on a document can reference a different item, so this caches by itemId and
|
|
// fetches each one once. A naive refetch on every line edit would cost one request per line
|
|
// on a 20-line invoice; the cache keeps it to one request per distinct item.
|
|
import { useCallback, useEffect, useRef, useState } from "react"
|
|
|
|
import { itemsApi } from "@/lib/api/items"
|
|
import { AllowedUom } from "@/types/master-data"
|
|
|
|
export interface AllowedUomLookup {
|
|
/** Allowed units for an item, or `undefined` while it is still loading / unknown. */
|
|
get: (itemId: number | null | undefined) => AllowedUom[] | undefined
|
|
/** Ensure an item's units are loaded. Safe to call repeatedly; in-flight requests are shared. */
|
|
load: (itemId: number | null | undefined) => void
|
|
}
|
|
|
|
export function useAllowedUoms(): AllowedUomLookup {
|
|
const [cache, setCache] = useState<Record<number, AllowedUom[]>>({})
|
|
// Tracks in-flight and failed ids so a repeated render never re-issues the same request.
|
|
const pending = useRef<Set<number>>(new Set())
|
|
|
|
const load = useCallback((itemId: number | null | undefined) => {
|
|
if (!itemId || pending.current.has(itemId)) return
|
|
pending.current.add(itemId)
|
|
itemsApi
|
|
.allowedUoms(itemId)
|
|
.then((uoms) => setCache((prev) => ({ ...prev, [itemId]: uoms })))
|
|
.catch(() => {
|
|
// Leave the id marked so we don't hammer a failing endpoint. The picker falls back
|
|
// to the global UOM list (see `pickerOptions`), and the server still rejects an
|
|
// invalid unit on save — this is a degraded experience, not a correctness hole.
|
|
})
|
|
}, [])
|
|
|
|
const get = useCallback(
|
|
(itemId: number | null | undefined) => (itemId ? cache[itemId] : undefined),
|
|
[cache],
|
|
)
|
|
|
|
return { get, load }
|
|
}
|
|
|
|
/**
|
|
* Single-item variant for screens with one item in scope (the conversion editor, a stock
|
|
* enquiry filtered to one item).
|
|
*/
|
|
export function useItemAllowedUoms(itemId: number | null | undefined): AllowedUom[] {
|
|
const [uoms, setUoms] = useState<AllowedUom[]>([])
|
|
|
|
useEffect(() => {
|
|
if (!itemId) {
|
|
setUoms([])
|
|
return
|
|
}
|
|
let cancelled = false
|
|
itemsApi
|
|
.allowedUoms(itemId)
|
|
.then((res) => {
|
|
if (!cancelled) setUoms(res)
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setUoms([])
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [itemId])
|
|
|
|
return uoms
|
|
}
|