@@ -62,6 +62,7 @@ export function ChangeInventoryPage() {
const [ dateInput , setDateInput ] = useState < string > ( ( ) = > new Date ( ) . toISOString ( ) ) ;
const [ specsChips , setSpecsChips ] = useState < string [ ] > ( [ ] ) ;
const [ note , setNote ] = useState ( "" ) ;
const [ deliveredTo , setDeliveredTo ] = useState ( "" ) ;
const [ rows , setRows ] = useState < RowDraft [ ] > ( [ ] ) ;
const [ busy , setBusy ] = useState ( false ) ;
@@ -86,10 +87,20 @@ export function ChangeInventoryPage() {
) ;
const formRecords = useMemo ( ( ) = > {
const txt = matTxt . trim ( ) ;
const txt = matTxt . trim ( ) . toLowerCase ( ) ;
const wid = type === "TRANSFER" ? fromId : whId ;
return data . inventory . filter ( ( r ) = > {
if ( r . warehouse ? . id !== wid ) return false ;
if ( type === "WITHDRAWAL" ) {
if ( r . quantity_number <= 0 ) return false ;
if ( txt ) {
const mMatch = r . material . name . toLowerCase ( ) . includes ( txt ) ;
const bMatch = r . brand ? . name ? . toLowerCase ( ) . includes ( txt ) ? ? false ;
const sMatch = Object . values ( r . specs || { } ) . some ( ( v ) = > String ( v ) . toLowerCase ( ) . includes ( txt ) ) ;
if ( ! mMatch && ! bMatch && ! sMatch ) return false ;
}
return true ;
}
if ( txt && ! r . material . name . includes ( txt ) ) return false ;
return true ;
} ) ;
@@ -186,6 +197,7 @@ export function ChangeInventoryPage() {
setQty ( "" ) ;
setSpecsChips ( [ ] ) ;
setNote ( "" ) ;
setDeliveredTo ( "" ) ;
await data . refresh ( ) ;
} catch ( err ) {
toast . error ( err instanceof Error ? err . message : "خطا در ثبت" ) ;
@@ -235,6 +247,9 @@ export function ChangeInventoryPage() {
} ;
const submitList = async ( t : TransactionType ) = > {
if ( t === "WITHDRAWAL" && ! deliveredTo . trim ( ) ) {
return toast . error ( "نام تحویلگیرنده اقلام الزامی است" ) ;
}
const ops : Record < string , unknown > [ ] = rows
. map ( ( r , i ) : Record < string , unknown > | null = > {
const n = parseNum ( r . qty ) ;
@@ -244,7 +259,18 @@ export function ChangeInventoryPage() {
if ( t === "STOCK_COUNT" ) {
return { transaction_type : "STOCK_COUNT" , warehouse_id : whId , material_id : rec.material.id , brand_id : rec.brand?.id || null , physical_qty : n , note : r.note , transaction_date : toServerIso ( ) } ;
}
return { transaction_type : t , warehouse_id : whId , material_id : rec.material.id , brand_id : rec.brand?.id || null , quantity : n , unit_id : rec.unit?.id ? ? null , specs : rec.specs || { } , note : r.note , transaction_date : toServerIso ( ) } ;
return {
transaction_type : t ,
warehouse_id : whId ,
material_id : rec.material.id ,
brand_id : rec.brand?.id || null ,
quantity : n ,
unit_id : rec.unit?.id ? ? null ,
specs : rec.specs || { } ,
note : r.note ,
delivered_to : t === "WITHDRAWAL" ? deliveredTo . trim ( ) : undefined ,
transaction_date : toServerIso ( ) ,
} ;
} )
. filter ( ( x ) : x is Record < string , unknown > = > x !== null ) ;
if ( ! ops . length ) return toast . error ( "هیچ ردیفی پر نشده است" ) ;
@@ -370,6 +396,9 @@ export function ChangeInventoryPage() {
if ( type === "PURCHASE" ) {
return ! ! ( whId && matTxt . trim ( ) && brandTxt . trim ( ) && unitTxt . trim ( ) && parseNum ( qty ) > 0 && specsComplete ) ;
}
if ( type === "WITHDRAWAL" ) {
return ! ! ( whId && deliveredTo . trim ( ) && rows . some ( ( r ) = > parseNum ( r . qty ) > 0 ) ) ;
}
if ( type === "TRANSFER" ) return ! ! ( fromId && toId && matTxt . trim ( ) && rows . some ( ( r ) = > parseNum ( r . qty ) > 0 ) ) ;
if ( type === "RETURN" ) return ! ! ( whId && matTxt . trim ( ) && rows . some ( ( r ) = > parseNum ( r . qty ) > 0 ) ) ;
if ( type === "FOUND" ) {
@@ -486,7 +515,36 @@ export function ChangeInventoryPage() {
< / div >
< / div >
{ ( type === "WITHDRAWAL" || type === "DAMAGE" || type === "WASTE" || type === "LOSS" || type === "STOCK_COUNT" || type === "RETURN" || type === "FOUND" ) && (
{ type === "WITHDRAWAL" && (
< div className = "grid gap-3 sm:grid-cols-2" >
< div className = "field" >
< label className = "text-xs font-semibold text-muted" > ج س ت ج و د ر م ت ر ی ا ل ه ا ( ا خ ت ی ا ر ی ) < / label >
< input
list = "dl-mat"
className = "h-9 w-full rounded-lg border border-line bg-panel px-3 text-xs sm:text-sm"
placeholder = "فیلتر بر اساس نام متریال، برند یا مشخصه (خالی = نمایش همه)…"
value = { matTxt }
onChange = { ( e ) = > { setMatTxt ( e . target . value ) ; setRows ( [ ] ) ; } }
/ >
< datalist id = "dl-mat" >
{ data . lookup . materials . map ( ( m ) = > < option key = { m . id } value = { m . name } / > ) }
< / datalist >
< / div >
< div className = "field" >
< label className = "text-xs font-semibold text-muted" >
ت ح و ی ل ب ه ( ت ح و ی ل گ ی ر ن د ه ) < span className = "text-rose-500 font-bold" > * < / span >
< / label >
< input
className = "h-9 w-full rounded-lg border border-line bg-panel px-3 text-xs sm:text-sm"
placeholder = "نام فرد، اکیپ یا پیمانکار تحویلگیرنده اقلام…"
value = { deliveredTo }
onChange = { ( e ) = > setDeliveredTo ( e . target . value ) }
/ >
< / div >
< / div >
) }
{ ( type === "DAMAGE" || type === "WASTE" || type === "LOSS" || type === "STOCK_COUNT" || type === "RETURN" || type === "FOUND" ) && (
< div className = "field" >
< label className = "text-xs font-semibold text-muted" > { type === "FOUND" ? "متریال (خالی = همه مفقودیها)" : "متریال *" } < / label >
< input
@@ -653,10 +711,11 @@ export function ChangeInventoryPage() {
setRows = { setRows }
records = { formRecords }
unitNameFn = { ( id ) = > unitName ( data . lookup . units , id ) }
showMaterial = { type === "WITHDRAWAL" || ! matTxt . trim ( ) }
emptyMsg = {
! whId ? "برای نمایش رکوردها، ابتدا انبار را انتخاب کنید."
: ! matTxt . trim ( ) ? "نام متریال را تایپ کنید تا همه مدلهای آن (با برند و مشخصه متفاوت) نمایش داده شود."
: ! formRecords . length ? "برای این نام متریال در انبار انتخابی رکوردی وجود ندارد. ابتدا «موجودی اولیه» ثبت کنی د."
: ( type !== "WITHDRAWAL" && ! matTxt . trim ( ) ) ? "نام متریال را تایپ کنید تا همه مدلهای آن (با برند و مشخصه متفاوت) نمایش داده شود."
: ! formRecords . length ? ( matTxt . trim ( ) ? "برای این عبارت در انبار انتخابی رکوردی یافت نشد." : "هیچ کالای دارای موجودی در این انبار یافت نش د.")
: ""
}
/ >
@@ -673,6 +732,7 @@ export function ChangeInventoryPage() {
setRows = { setRows }
records = { formRecords }
unitNameFn = { ( id ) = > unitName ( data . lookup . units , id ) }
showMaterial = { ! matTxt . trim ( ) }
emptyMsg = {
! fromId || ! toId ? "برای نمایش موجودیها، انبار مبدأ، مقصد و نام متریال را وارد کنید."
: ! matTxt . trim ( ) ? "نام متریال را تایپ کنید…"
@@ -778,57 +838,69 @@ export function ChangeInventoryPage() {
) ;
}
function RowTable ( { isCount , rows , setRows , records , unitNameFn , emptyMsg } : {
function RowTable ( { isCount , rows , setRows , records , unitNameFn , emptyMsg , showMaterial = true } : {
isCount : boolean ;
rows : RowDraft [ ] ;
setRows : ( r : RowDraft [ ] ) = > void ;
records : { id : string ; brand : { id : string ; name : string } | null ; unit : { id : number ; code : string ; name_fa : string | null } | null ; quantity_number : number ; specs : Record < string , string > } [ ] ;
records : { id : string ; material ? : { id : string ; name : string } ; brand : { id : string ; name : string } | null ; unit : { id : number ; code : string ; name_fa : string | null } | null ; quantity_number : number ; specs : Record < string , string > } [ ] ;
unitNameFn : ( id : number | null | undefined ) = > string ;
emptyMsg : string ;
showMaterial? : boolean ;
} ) {
if ( emptyMsg ) return < div className = "rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted" > { emptyMsg } < / div > ;
return (
< div className = "overflow-x-auto" >
< table className = "w-full text-[12.5px] min-w-[500px] " >
< thead >
< tr className = "border-b border-line text-right text-muted " >
< th className = "py-2 pe-3" > م ش خ ص ه ه ا < / th >
< th className = "py-2 pe -3" > ب ر ن د < / th >
< th className = "py-2 pe -3" > و ا ح د < / th >
< th className = "py-2 pe -3" > م و ج و د ی ف ع ل ی < / th >
< th className = "py-2 pe -3" > { isCount ? "شمارششده" : "تعداد" } < / th >
< th className = "py-2" > ی ا د د ا ش ت < / th >
< / tr >
< / thead >
< tbody >
{ records . map ( ( r , i ) = > (
< tr key = { r . id } className = "border-b border-line/50" >
< td className = "py-2 pe-3 text-muted" > { Object . entries ( r . specs || { } ) . map ( ( [ k , v ] ) = > ` ${ k } : ${ v } ` ) . join ( " · " ) || "—" } < / td >
< td className = "py-2 pe-3" > { r . brand ? . name || "—" } < / td >
< td className = "py-2 pe-3" > { unitNameFn ( r . unit ? . id ) } < / td >
< td className = "py-2 pe-3 font-bold" > { faNum ( r . quantity_number ) } < / td >
< td className = "py-2 pe-3" >
< input
inputMode = "decimal"
className = "h-8 w-24 rounded-lg border border-line bg-panel px-2 text-sm ltr"
placeholder = { isCount ? "شمارششده" : "۰ " }
value = { isCount && rows [ i ] ? . qty === "" ? String ( r . quantity_number ) : ( rows [ i ] ? . qty || "" ) }
onChange = { ( e ) = > setRows ( rows . map ( ( x , xi ) = > ( xi === i ? { . . . x , qty : e.target.value } : x ) ) ) }
/ >
< / td >
< td className = "py-2" >
< input
className = "h-8 w-32 rounded-lg border border-line bg-panel px-2 text-sm"
placeholder = "توضیح (اختیاری)"
value = { rows [ i ] ? . note || "" }
onChange = { ( e ) = > setRows ( rows . map ( ( x , xi ) = > ( xi === i ? { . . . x , note : e.target.value } : x ) ) ) }
/ >
< / td >
< div >
< div className = "overflow-x-auto max-h-[420px] overflow-y-auto border border-line rounded-lg " >
< table className = "w-full text-[12.5px] min-w-[560px]" >
< thead className = "sticky top-0 bg-panel z-10 shadow-2xs border-b border-line " >
< tr className = "text-right text-muted" >
{ showMaterial && < th className = "py-2.5 px -3" > م ت ر ی ا ل < / th > }
< th className = "py-2.5 px -3" > م ش خ ص ه ه ا < / th >
< th className = "py-2.5 px -3" > ب ر ن د < / th >
< th className = "py-2.5 px -3" > و ا ح د < / th >
< th className = "py-2.5 px-3" > م و ج و د ی ف ع ل ی < / th >
< th className = "py-2.5 px-3" > { isCount ? "شمارششده" : "تعداد" } < / th >
< th className = "py-2.5 px-3" > ی ا د د ا ش ت < / th >
< / tr >
) ) }
< / tbody >
< / table >
< div className = "mt-2 text-[11px] text-muted" > س ت و ن « م و ج و د ی ف ع ل ی » م ق د ا ر م و ج و د ه ر ر ک و ر د ا س ت . ب ر ا ی ه ر ر د ی ف ت ع د ا د و ی ا د د ا ش ت و ا ر د ک ن ی د . < / div >
< / thead >
< tbody className = "divide-y divide-line/40" >
{ records . map ( ( r , i ) = > (
< tr key = { r . id } className = "hover:bg-bg/40 transition-colors" >
{ showMaterial && (
< td className = "py-2 px-3 font-bold text-ink" >
{ r . material ? . name || "—" }
< / td >
) }
< td className = "py-2 px-3 text-muted" > { Object . entries ( r . specs || { } ) . map ( ( [ k , v ] ) = > ` ${ k } : ${ v } ` ) . join ( " · " ) || "—" } < / td >
< td className = "py-2 px-3" > { r . brand ? . name || "—" } < / td >
< td className = "py-2 px-3" > { unitNameFn ( r . unit ? . id ) } < / td >
< td className = "py-2 px-3 font-bold text-ink" > { faNum ( r . quantity_number ) } < / td >
< td className = "py-2 px-3" >
< input
inputMode = "decimal"
className = "h-8 w-24 rounded-lg border border-line bg-panel px-2 text-sm ltr focus:border-accent"
placeholder = { isCount ? "شمارششده" : "۰ " }
value = { isCount && rows [ i ] ? . qty === "" ? String ( r . quantity_number ) : ( rows [ i ] ? . qty || "" ) }
onChange = { ( e ) = > setRows ( rows . map ( ( x , xi ) = > ( xi === i ? { . . . x , qty : e.target.value } : x ) ) ) }
/ >
< / td >
< td className = "py-2 px-3" >
< input
className = "h-8 w-32 rounded-lg border border-line bg-panel px-2 text-sm focus:border-accent"
placeholder = "توضیح (اختیاری)"
value = { rows [ i ] ? . note || "" }
onChange = { ( e ) = > setRows ( rows . map ( ( x , xi ) = > ( xi === i ? { . . . x , note : e.target.value } : x ) ) ) }
/ >
< / td >
< / tr >
) ) }
< / tbody >
< / table >
< / div >
< div className = "mt-2 flex items-center justify-between text-[11px] text-muted" >
< span > س ت و ن « م و ج و د ی ف ع ل ی » م ق د ا ر م و ج و د ه ر ر ک و ر د ا س ت . ب ر ا ی ه ر ر د ی ف ت ع د ا د و ی ا د د ا ش ت و ا ر د ک ن ی د . < / span >
< span > { faNum ( records . length ) } ق ل م ک ا ل ا < / span >
< / div >
< / div >
) ;
}