FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

GoodPickExecutiondetail.tsx 130 KiB

4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
1週間前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
1週間前
4ヶ月前
3ヶ月前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
3ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
1週間前
4ヶ月前
4ヶ月前
3ヶ月前
1週間前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
1週間前
4ヶ月前
1週間前
1週間前
4ヶ月前
1週間前
1週間前
4ヶ月前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
1ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
1週間前
4ヶ月前
1週間前
1週間前
1週間前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
1週間前
1週間前
4ヶ月前
4ヶ月前
4ヶ月前
1週間前
1週間前
1週間前
1週間前
1週間前
2ヶ月前
1週間前
2ヶ月前
1週間前
1週間前
2ヶ月前
2ヶ月前
1週間前
1週間前
2ヶ月前
1週間前
2ヶ月前
1週間前
1週間前
1週間前
2ヶ月前
1週間前
1週間前
2ヶ月前
1週間前
2ヶ月前
1週間前
2ヶ月前
1週間前
2ヶ月前
4ヶ月前
1週間前
1週間前
4ヶ月前
4ヶ月前
1週間前
4ヶ月前
1週間前
1週間前
4ヶ月前
1週間前
1週間前
4ヶ月前
4ヶ月前
4ヶ月前
1週間前
4ヶ月前
1週間前
1週間前
4ヶ月前
1週間前
1週間前
4ヶ月前
1週間前
4ヶ月前
1週間前
1週間前
4ヶ月前
1週間前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
1週間前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1週間前
4ヶ月前
3ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
1週間前
4ヶ月前
3ヶ月前
4ヶ月前
1週間前
4ヶ月前
4ヶ月前
1週間前
4ヶ月前
1週間前
4ヶ月前
3ヶ月前
1週間前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
1週間前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
1週間前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
1週間前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1週間前
3ヶ月前
1週間前
3ヶ月前
1週間前
3ヶ月前
1週間前
3ヶ月前
1週間前
3ヶ月前
1週間前
3ヶ月前
1週間前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
1週間前
3ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1週間前
3ヶ月前
1週間前
3ヶ月前
3ヶ月前
1週間前
3ヶ月前
1週間前
3ヶ月前
3ヶ月前
1週間前
1週間前
1週間前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400
  1. "use client";
  2. import {
  3. Box,
  4. Button,
  5. Stack,
  6. TextField,
  7. Typography,
  8. Alert,
  9. CircularProgress,
  10. Table,
  11. TableBody,
  12. TableCell,
  13. TableContainer,
  14. TableHead,
  15. TableRow,
  16. Paper,
  17. Checkbox,
  18. TablePagination,
  19. Modal,
  20. Chip,
  21. } from "@mui/material";
  22. import dayjs from 'dayjs';
  23. import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider';
  24. import { fetchLotDetail } from "@/app/api/inventory/actions";
  25. import React, { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react";
  26. import { useTranslation } from "react-i18next";
  27. import { useRouter } from "next/navigation";
  28. import {
  29. updateStockOutLineStatus,
  30. createStockOutLine,
  31. updateStockOutLine,
  32. recordPickExecutionIssue,
  33. fetchFGPickOrders, // Add this import
  34. FGPickOrderResponse,
  35. stockReponse,
  36. PickExecutionIssueData,
  37. checkPickOrderCompletion,
  38. fetchAllPickOrderLotsHierarchical,
  39. PickOrderCompletionResponse,
  40. checkAndCompletePickOrderByConsoCode,
  41. updateSuggestedLotLineId,
  42. updateStockOutLineStatusByQRCodeAndLotNo,
  43. confirmLotSubstitution,
  44. fetchDoPickOrderDetail, // 必须添加
  45. DoPickOrderDetail, // 必须添加
  46. fetchFGPickOrdersByUserId ,
  47. batchQrSubmit,
  48. batchSubmitList, // 添加:导入 batchSubmitList
  49. batchSubmitListRequest, // 添加:导入类型
  50. batchSubmitListLineRequest,
  51. batchScan,
  52. BatchScanRequest,
  53. BatchScanLineRequest,
  54. } from "@/app/api/pickOrder/actions";
  55. import FGPickOrderInfoCard from "./FGPickOrderInfoCard";
  56. import LotConfirmationModal from "./LotConfirmationModal";
  57. //import { fetchItem } from "@/app/api/settings/item";
  58. import { updateInventoryLotLineStatus, analyzeQrCode } from "@/app/api/inventory/actions";
  59. import { fetchNameList, NameList } from "@/app/api/user/actions";
  60. import {
  61. FormProvider,
  62. useForm,
  63. } from "react-hook-form";
  64. import SearchBox, { Criterion } from "../SearchBox";
  65. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  66. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  67. import QrCodeIcon from '@mui/icons-material/QrCode';
  68. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  69. import { useSession } from "next-auth/react";
  70. import { SessionWithTokens } from "@/config/authConfig";
  71. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  72. import GoodPickExecutionForm from "./GoodPickExecutionForm";
  73. import FGPickOrderCard from "./FGPickOrderCard";
  74. import LinearProgressWithLabel from "../common/LinearProgressWithLabel";
  75. import ScanStatusAlert from "../common/ScanStatusAlert";
  76. interface Props {
  77. filterArgs: Record<string, any>;
  78. onSwitchToRecordTab?: () => void;
  79. onRefreshReleasedOrderCount?: () => void;
  80. }
  81. // QR Code Modal Component (from LotTable)
  82. const QrCodeModal: React.FC<{
  83. open: boolean;
  84. onClose: () => void;
  85. lot: any | null;
  86. onQrCodeSubmit: (lotNo: string) => void;
  87. combinedLotData: any[]; // Add this prop
  88. lotConfirmationOpen: boolean;
  89. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData,lotConfirmationOpen = false }) => {
  90. const { t } = useTranslation("pickOrder");
  91. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  92. const [manualInput, setManualInput] = useState<string>('');
  93. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  94. const [manualInputError, setManualInputError] = useState<boolean>(false);
  95. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  96. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  97. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  98. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  99. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  100. const [fgPickOrder, setFgPickOrder] = useState<FGPickOrderResponse | null>(null);
  101. const fetchingRef = useRef<Set<number>>(new Set());
  102. // Process scanned QR codes
  103. useEffect(() => {
  104. // ✅ Don't process if modal is not open
  105. if (!open) {
  106. return;
  107. }
  108. // ✅ Don't process if lot confirmation modal is open
  109. if (lotConfirmationOpen) {
  110. console.log("Lot confirmation modal is open, skipping QrCodeModal processing...");
  111. return;
  112. }
  113. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  114. const latestQr = qrValues[qrValues.length - 1];
  115. if (processedQrCodes.has(latestQr)) {
  116. console.log("QR code already processed, skipping...");
  117. return;
  118. }
  119. try {
  120. const qrData = JSON.parse(latestQr);
  121. if (qrData.stockInLineId && qrData.itemId) {
  122. // ✅ Check if we're already fetching this stockInLineId
  123. if (fetchingRef.current.has(qrData.stockInLineId)) {
  124. console.log(`⏱️ [QR MODAL] Already fetching stockInLineId: ${qrData.stockInLineId}, skipping duplicate call`);
  125. return;
  126. }
  127. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  128. setIsProcessingQr(true);
  129. setQrScanFailed(false);
  130. // ✅ Mark as fetching
  131. fetchingRef.current.add(qrData.stockInLineId);
  132. const fetchStartTime = performance.now();
  133. console.log(`⏱️ [QR MODAL] Starting fetchStockInLineInfo for stockInLineId: ${qrData.stockInLineId}`);
  134. fetchStockInLineInfo(qrData.stockInLineId)
  135. .then((stockInLineInfo) => {
  136. // ✅ Remove from fetching set
  137. fetchingRef.current.delete(qrData.stockInLineId);
  138. // ✅ Check again if modal is still open and lot confirmation is not open
  139. if (!open || lotConfirmationOpen) {
  140. console.log("Modal state changed, skipping result processing");
  141. return;
  142. }
  143. const fetchTime = performance.now() - fetchStartTime;
  144. console.log(`⏱️ [QR MODAL] fetchStockInLineInfo time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`);
  145. console.log("Stock in line info:", stockInLineInfo);
  146. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  147. if (stockInLineInfo.lotNo === lot.lotNo) {
  148. console.log(` QR Code verified for lot: ${lot.lotNo}`);
  149. setQrScanSuccess(true);
  150. onQrCodeSubmit(lot.lotNo);
  151. onClose();
  152. resetScan();
  153. } else {
  154. console.log(` QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  155. setQrScanFailed(true);
  156. setManualInputError(true);
  157. setManualInputSubmitted(true);
  158. }
  159. })
  160. .catch((error) => {
  161. // ✅ Remove from fetching set
  162. fetchingRef.current.delete(qrData.stockInLineId);
  163. // ✅ Check again if modal is still open
  164. if (!open || lotConfirmationOpen) {
  165. console.log("Modal state changed, skipping error handling");
  166. return;
  167. }
  168. const fetchTime = performance.now() - fetchStartTime;
  169. console.error(`❌ [QR MODAL] fetchStockInLineInfo failed after ${fetchTime.toFixed(2)}ms:`, error);
  170. setScannedQrResult('Error fetching data');
  171. setQrScanFailed(true);
  172. setManualInputError(true);
  173. setManualInputSubmitted(true);
  174. })
  175. .finally(() => {
  176. setIsProcessingQr(false);
  177. });
  178. } else {
  179. const qrContent = latestQr.replace(/[{}]/g, '');
  180. setScannedQrResult(qrContent);
  181. if (qrContent === lot.lotNo) {
  182. setQrScanSuccess(true);
  183. onQrCodeSubmit(lot.lotNo);
  184. onClose();
  185. resetScan();
  186. } else {
  187. setQrScanFailed(true);
  188. setManualInputError(true);
  189. setManualInputSubmitted(true);
  190. }
  191. }
  192. } catch (error) {
  193. console.log("QR code is not JSON format, trying direct comparison");
  194. const qrContent = latestQr.replace(/[{}]/g, '');
  195. setScannedQrResult(qrContent);
  196. if (qrContent === lot.lotNo) {
  197. setQrScanSuccess(true);
  198. onQrCodeSubmit(lot.lotNo);
  199. onClose();
  200. resetScan();
  201. } else {
  202. setQrScanFailed(true);
  203. setManualInputError(true);
  204. setManualInputSubmitted(true);
  205. }
  206. }
  207. }
  208. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes, lotConfirmationOpen, open]);
  209. // Clear states when modal opens
  210. useEffect(() => {
  211. if (open) {
  212. setManualInput('');
  213. setManualInputSubmitted(false);
  214. setManualInputError(false);
  215. setIsProcessingQr(false);
  216. setQrScanFailed(false);
  217. setQrScanSuccess(false);
  218. setScannedQrResult('');
  219. setProcessedQrCodes(new Set());
  220. }
  221. }, [open]);
  222. useEffect(() => {
  223. if (lot) {
  224. setManualInput('');
  225. setManualInputSubmitted(false);
  226. setManualInputError(false);
  227. setIsProcessingQr(false);
  228. setQrScanFailed(false);
  229. setQrScanSuccess(false);
  230. setScannedQrResult('');
  231. setProcessedQrCodes(new Set());
  232. }
  233. }, [lot]);
  234. // Auto-submit manual input when it matches
  235. useEffect(() => {
  236. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  237. console.log(' Auto-submitting manual input:', manualInput.trim());
  238. const timer = setTimeout(() => {
  239. setQrScanSuccess(true);
  240. onQrCodeSubmit(lot.lotNo);
  241. onClose();
  242. setManualInput('');
  243. setManualInputError(false);
  244. setManualInputSubmitted(false);
  245. }, 200);
  246. return () => clearTimeout(timer);
  247. }
  248. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  249. const handleManualSubmit = () => {
  250. if (manualInput.trim() === lot?.lotNo) {
  251. setQrScanSuccess(true);
  252. onQrCodeSubmit(lot.lotNo);
  253. onClose();
  254. setManualInput('');
  255. } else {
  256. setQrScanFailed(true);
  257. setManualInputError(true);
  258. setManualInputSubmitted(true);
  259. }
  260. };
  261. useEffect(() => {
  262. if (open) {
  263. startScan();
  264. }
  265. }, [open, startScan]);
  266. return (
  267. <Modal open={open} onClose={onClose}>
  268. <Box sx={{
  269. position: 'absolute',
  270. top: '50%',
  271. left: '50%',
  272. transform: 'translate(-50%, -50%)',
  273. bgcolor: 'background.paper',
  274. p: 3,
  275. borderRadius: 2,
  276. minWidth: 400,
  277. }}>
  278. <Typography variant="h6" gutterBottom>
  279. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  280. </Typography>
  281. {isProcessingQr && (
  282. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  283. <Typography variant="body2" color="primary">
  284. {t("Processing QR code...")}
  285. </Typography>
  286. </Box>
  287. )}
  288. <Box sx={{ mb: 2 }}>
  289. <Typography variant="body2" gutterBottom>
  290. <strong>{t("Manual Input")}:</strong>
  291. </Typography>
  292. <TextField
  293. fullWidth
  294. size="small"
  295. value={manualInput}
  296. onChange={(e) => {
  297. setManualInput(e.target.value);
  298. if (qrScanFailed || manualInputError) {
  299. setQrScanFailed(false);
  300. setManualInputError(false);
  301. setManualInputSubmitted(false);
  302. }
  303. }}
  304. sx={{ mb: 1 }}
  305. error={manualInputSubmitted && manualInputError}
  306. helperText={
  307. manualInputSubmitted && manualInputError
  308. ? `${t("The input is not the same as the expected lot number.")}`
  309. : ''
  310. }
  311. />
  312. <Button
  313. variant="contained"
  314. onClick={handleManualSubmit}
  315. disabled={!manualInput.trim()}
  316. size="small"
  317. color="primary"
  318. >
  319. {t("Submit")}
  320. </Button>
  321. </Box>
  322. {qrValues.length > 0 && (
  323. <Box sx={{
  324. mb: 2,
  325. p: 2,
  326. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  327. borderRadius: 1
  328. }}>
  329. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  330. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  331. </Typography>
  332. {qrScanSuccess && (
  333. <Typography variant="caption" color="success" display="block">
  334. {t("Verified successfully!")}
  335. </Typography>
  336. )}
  337. </Box>
  338. )}
  339. <Box sx={{ mt: 2, textAlign: 'right' }}>
  340. <Button onClick={onClose} variant="outlined">
  341. {t("Cancel")}
  342. </Button>
  343. </Box>
  344. </Box>
  345. </Modal>
  346. );
  347. };
  348. const ManualLotConfirmationModal: React.FC<{
  349. open: boolean;
  350. onClose: () => void;
  351. onConfirm: (expectedLotNo: string, scannedLotNo: string) => void;
  352. expectedLot: {
  353. lotNo: string;
  354. itemCode: string;
  355. itemName: string;
  356. } | null;
  357. scannedLot: {
  358. lotNo: string;
  359. itemCode: string;
  360. itemName: string;
  361. } | null;
  362. isLoading?: boolean;
  363. }> = ({ open, onClose, onConfirm, expectedLot, scannedLot, isLoading = false }) => {
  364. const { t } = useTranslation("pickOrder");
  365. const [expectedLotInput, setExpectedLotInput] = useState<string>('');
  366. const [scannedLotInput, setScannedLotInput] = useState<string>('');
  367. const [error, setError] = useState<string>('');
  368. // 当模态框打开时,预填充输入框
  369. useEffect(() => {
  370. if (open) {
  371. setExpectedLotInput(expectedLot?.lotNo || '');
  372. setScannedLotInput(scannedLot?.lotNo || '');
  373. setError('');
  374. }
  375. }, [open, expectedLot, scannedLot]);
  376. const handleConfirm = () => {
  377. if (!expectedLotInput.trim() || !scannedLotInput.trim()) {
  378. setError(t("Please enter both expected and scanned lot numbers."));
  379. return;
  380. }
  381. if (expectedLotInput.trim() === scannedLotInput.trim()) {
  382. setError(t("Expected and scanned lot numbers cannot be the same."));
  383. return;
  384. }
  385. onConfirm(expectedLotInput.trim(), scannedLotInput.trim());
  386. };
  387. return (
  388. <Modal open={open} onClose={onClose}>
  389. <Box sx={{
  390. position: 'absolute',
  391. top: '50%',
  392. left: '50%',
  393. transform: 'translate(-50%, -50%)',
  394. bgcolor: 'background.paper',
  395. p: 3,
  396. borderRadius: 2,
  397. minWidth: 500,
  398. }}>
  399. <Typography variant="h6" gutterBottom color="warning.main">
  400. {t("Manual Lot Confirmation")}
  401. </Typography>
  402. <Box sx={{ mb: 2 }}>
  403. <Typography variant="body2" gutterBottom>
  404. <strong>{t("Expected Lot Number")}:</strong>
  405. </Typography>
  406. <TextField
  407. fullWidth
  408. size="small"
  409. value={expectedLotInput}
  410. onChange={(e) => {
  411. setExpectedLotInput(e.target.value);
  412. setError('');
  413. }}
  414. placeholder={expectedLot?.lotNo || t("Enter expected lot number")}
  415. sx={{ mb: 2 }}
  416. error={!!error && !expectedLotInput.trim()}
  417. />
  418. </Box>
  419. <Box sx={{ mb: 2 }}>
  420. <Typography variant="body2" gutterBottom>
  421. <strong>{t("Scanned Lot Number")}:</strong>
  422. </Typography>
  423. <TextField
  424. fullWidth
  425. size="small"
  426. value={scannedLotInput}
  427. onChange={(e) => {
  428. setScannedLotInput(e.target.value);
  429. setError('');
  430. }}
  431. placeholder={scannedLot?.lotNo || t("Enter scanned lot number")}
  432. sx={{ mb: 2 }}
  433. error={!!error && !scannedLotInput.trim()}
  434. />
  435. </Box>
  436. {error && (
  437. <Box sx={{ mb: 2, p: 1, backgroundColor: '#ffebee', borderRadius: 1 }}>
  438. <Typography variant="body2" color="error">
  439. {error}
  440. </Typography>
  441. </Box>
  442. )}
  443. <Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
  444. <Button onClick={onClose} variant="outlined" disabled={isLoading}>
  445. {t("Cancel")}
  446. </Button>
  447. <Button
  448. onClick={handleConfirm}
  449. variant="contained"
  450. color="warning"
  451. disabled={isLoading || !expectedLotInput.trim() || !scannedLotInput.trim()}
  452. >
  453. {isLoading ? t("Processing...") : t("Confirm")}
  454. </Button>
  455. </Box>
  456. </Box>
  457. </Modal>
  458. );
  459. };
  460. const PickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab, onRefreshReleasedOrderCount }) => {
  461. const { t } = useTranslation("pickOrder");
  462. const router = useRouter();
  463. const { data: session } = useSession() as { data: SessionWithTokens | null };
  464. const [doPickOrderDetail, setDoPickOrderDetail] = useState<DoPickOrderDetail | null>(null);
  465. const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(null);
  466. const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
  467. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  468. const [allLotsCompleted, setAllLotsCompleted] = useState(false);
  469. const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
  470. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  471. const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
  472. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  473. const [qrScanInput, setQrScanInput] = useState<string>('');
  474. const [qrScanError, setQrScanError] = useState<boolean>(false);
  475. const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>('');
  476. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  477. const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false);
  478. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  479. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  480. const [paginationController, setPaginationController] = useState({
  481. pageNum: 0,
  482. pageSize: -1,
  483. });
  484. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  485. const initializationRef = useRef(false);
  486. const autoAssignRef = useRef(false);
  487. const formProps = useForm();
  488. const errors = formProps.formState.errors;
  489. // QR scanner states (always-on, no modal)
  490. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  491. const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
  492. const [expectedLotData, setExpectedLotData] = useState<any>(null);
  493. const [scannedLotData, setScannedLotData] = useState<any>(null);
  494. const [isConfirmingLot, setIsConfirmingLot] = useState(false);
  495. // Add GoodPickExecutionForm states
  496. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  497. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  498. const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
  499. const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
  500. // Add these missing state variables after line 352
  501. const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
  502. // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling
  503. const [processedQrCombinations, setProcessedQrCombinations] = useState<Map<number, Set<number>>>(new Map());
  504. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  505. const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
  506. const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
  507. const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
  508. // Cache for fetchStockInLineInfo API calls to avoid redundant requests
  509. const stockInLineInfoCache = useRef<Map<number, { lotNo: string | null; timestamp: number }>>(new Map());
  510. const CACHE_TTL = 60000; // 60 seconds cache TTL
  511. const abortControllerRef = useRef<AbortController | null>(null);
  512. const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  513. // Use refs for processed QR tracking to avoid useEffect dependency issues and delays
  514. const processedQrCodesRef = useRef<Set<string>>(new Set());
  515. const lastProcessedQrRef = useRef<string>('');
  516. // Store callbacks in refs to avoid useEffect dependency issues
  517. const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null);
  518. const resetScanRef = useRef<(() => void) | null>(null);
  519. // Handle QR code button click
  520. const handleQrCodeClick = (pickOrderId: number) => {
  521. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  522. // TODO: Implement QR code functionality
  523. };
  524. const progress = useMemo(() => {
  525. if (combinedLotData.length === 0) {
  526. return { completed: 0, total: 0 };
  527. }
  528. const nonPendingCount = combinedLotData.filter(lot => {
  529. const status = lot.stockOutLineStatus?.toLowerCase();
  530. return status !== 'pending';
  531. }).length;
  532. return {
  533. completed: nonPendingCount,
  534. total: combinedLotData.length
  535. };
  536. }, [combinedLotData]);
  537. // Cached version of fetchStockInLineInfo to avoid redundant API calls
  538. const fetchStockInLineInfoCached = useCallback(async (stockInLineId: number): Promise<{ lotNo: string | null }> => {
  539. const now = Date.now();
  540. const cached = stockInLineInfoCache.current.get(stockInLineId);
  541. // Return cached value if still valid
  542. if (cached && (now - cached.timestamp) < CACHE_TTL) {
  543. console.log(`✅ [CACHE HIT] Using cached stockInLineInfo for ${stockInLineId}`);
  544. return { lotNo: cached.lotNo };
  545. }
  546. // Cancel previous request if exists
  547. if (abortControllerRef.current) {
  548. abortControllerRef.current.abort();
  549. }
  550. // Create new abort controller for this request
  551. const abortController = new AbortController();
  552. abortControllerRef.current = abortController;
  553. try {
  554. console.log(`⏱️ [CACHE MISS] Fetching stockInLineInfo for ${stockInLineId}`);
  555. const stockInLineInfo = await fetchStockInLineInfo(stockInLineId);
  556. // Store in cache
  557. stockInLineInfoCache.current.set(stockInLineId, {
  558. lotNo: stockInLineInfo.lotNo || null,
  559. timestamp: now
  560. });
  561. // Limit cache size to prevent memory leaks
  562. if (stockInLineInfoCache.current.size > 100) {
  563. const firstKey = stockInLineInfoCache.current.keys().next().value;
  564. if (firstKey !== undefined) {
  565. stockInLineInfoCache.current.delete(firstKey);
  566. }
  567. }
  568. return { lotNo: stockInLineInfo.lotNo || null };
  569. } catch (error: any) {
  570. if (error.name === 'AbortError') {
  571. console.log(`⏱️ [CACHE] Request aborted for ${stockInLineId}`);
  572. throw error;
  573. }
  574. console.error(`❌ [CACHE] Error fetching stockInLineInfo for ${stockInLineId}:`, error);
  575. throw error;
  576. }
  577. }, []);
  578. const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => {
  579. const mismatchStartTime = performance.now();
  580. console.log(`⏱️ [HANDLE LOT MISMATCH START]`);
  581. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  582. console.log("Lot mismatch detected:", { expectedLot, scannedLot });
  583. // ✅ Use setTimeout to avoid flushSync warning - schedule modal update in next tick
  584. const setTimeoutStartTime = performance.now();
  585. console.time('setLotConfirmationOpen');
  586. setTimeout(() => {
  587. const setStateStartTime = performance.now();
  588. setExpectedLotData(expectedLot);
  589. setScannedLotData({
  590. ...scannedLot,
  591. lotNo: scannedLot.lotNo || null,
  592. });
  593. setLotConfirmationOpen(true);
  594. const setStateTime = performance.now() - setStateStartTime;
  595. console.timeEnd('setLotConfirmationOpen');
  596. console.log(`⏱️ [HANDLE LOT MISMATCH] Modal state set to open (setState time: ${setStateTime.toFixed(2)}ms)`);
  597. console.log(`✅ [HANDLE LOT MISMATCH] Modal state set to open`);
  598. }, 0);
  599. const setTimeoutTime = performance.now() - setTimeoutStartTime;
  600. console.log(`⏱️ [PERF] setTimeout scheduling time: ${setTimeoutTime.toFixed(2)}ms`);
  601. // ✅ Fetch lotNo in background ONLY for display purposes (using cached version)
  602. if (!scannedLot.lotNo && scannedLot.stockInLineId) {
  603. const stockInLineId = scannedLot.stockInLineId;
  604. if (typeof stockInLineId !== 'number') {
  605. console.warn(`⏱️ [HANDLE LOT MISMATCH] Invalid stockInLineId: ${stockInLineId}`);
  606. return;
  607. }
  608. console.log(`⏱️ [HANDLE LOT MISMATCH] Fetching lotNo in background (stockInLineId: ${stockInLineId})`);
  609. const fetchStartTime = performance.now();
  610. fetchStockInLineInfoCached(stockInLineId)
  611. .then((stockInLineInfo) => {
  612. const fetchTime = performance.now() - fetchStartTime;
  613. console.log(`⏱️ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`);
  614. const updateStateStartTime = performance.now();
  615. startTransition(() => {
  616. setScannedLotData((prev: any) => ({
  617. ...prev,
  618. lotNo: stockInLineInfo.lotNo || null,
  619. }));
  620. });
  621. const updateStateTime = performance.now() - updateStateStartTime;
  622. console.log(`⏱️ [PERF] Update scanned lot data time: ${updateStateTime.toFixed(2)}ms`);
  623. const totalTime = performance.now() - mismatchStartTime;
  624. console.log(`⏱️ [HANDLE LOT MISMATCH] Background fetch completed: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  625. })
  626. .catch((error) => {
  627. if (error.name !== 'AbortError') {
  628. const fetchTime = performance.now() - fetchStartTime;
  629. console.error(`❌ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached failed after ${fetchTime.toFixed(2)}ms:`, error);
  630. }
  631. });
  632. } else {
  633. const totalTime = performance.now() - mismatchStartTime;
  634. console.log(`⏱️ [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  635. }
  636. }, [fetchStockInLineInfoCached]);
  637. const checkAllLotsCompleted = useCallback((lotData: any[]) => {
  638. if (lotData.length === 0) {
  639. setAllLotsCompleted(false);
  640. return false;
  641. }
  642. // Filter out rejected lots
  643. const nonRejectedLots = lotData.filter(lot =>
  644. lot.lotAvailability !== 'rejected' &&
  645. lot.stockOutLineStatus !== 'rejected'
  646. );
  647. if (nonRejectedLots.length === 0) {
  648. setAllLotsCompleted(false);
  649. return false;
  650. }
  651. // Check if all non-rejected lots are completed
  652. const allCompleted = nonRejectedLots.every(lot =>
  653. lot.stockOutLineStatus === 'completed'
  654. );
  655. setAllLotsCompleted(allCompleted);
  656. return allCompleted;
  657. }, []);
  658. // 在 fetchAllCombinedLotData 函数中(约 446-684 行)
  659. const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdOverride?: number) => {
  660. setCombinedDataLoading(true);
  661. try {
  662. const userIdToUse = userId || currentUserId;
  663. console.log(" fetchAllCombinedLotData called with userId:", userIdToUse);
  664. if (!userIdToUse) {
  665. console.warn("⚠️ No userId available, skipping API call");
  666. setCombinedLotData([]);
  667. setOriginalCombinedData([]);
  668. setAllLotsCompleted(false);
  669. return;
  670. }
  671. // 获取新结构的层级数据
  672. const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse);
  673. console.log(" Hierarchical data (new structure):", hierarchicalData);
  674. // 检查数据结构
  675. if (!hierarchicalData.fgInfo || !hierarchicalData.pickOrders || hierarchicalData.pickOrders.length === 0) {
  676. console.warn("⚠️ No FG info or pick orders found");
  677. setCombinedLotData([]);
  678. setOriginalCombinedData([]);
  679. setAllLotsCompleted(false);
  680. return;
  681. }
  682. // 使用合并后的 pick order 对象(现在只有一个对象,包含所有数据)
  683. const mergedPickOrder = hierarchicalData.pickOrders[0];
  684. // 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片)
  685. // 修改第 478-509 行的 fgOrder 构建逻辑:
  686. const fgOrder: FGPickOrderResponse = {
  687. doPickOrderId: hierarchicalData.fgInfo.doPickOrderId,
  688. ticketNo: hierarchicalData.fgInfo.ticketNo,
  689. storeId: hierarchicalData.fgInfo.storeId,
  690. shopCode: hierarchicalData.fgInfo.shopCode,
  691. shopName: hierarchicalData.fgInfo.shopName,
  692. truckLanceCode: hierarchicalData.fgInfo.truckLanceCode,
  693. DepartureTime: hierarchicalData.fgInfo.departureTime,
  694. shopAddress: "",
  695. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  696. // 兼容字段
  697. pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0,
  698. pickOrderConsoCode: mergedPickOrder.consoCode || "",
  699. pickOrderTargetDate: mergedPickOrder.targetDate || "",
  700. pickOrderStatus: mergedPickOrder.status || "",
  701. deliveryOrderId: mergedPickOrder.doOrderIds?.[0] || 0,
  702. deliveryNo: mergedPickOrder.deliveryOrderCodes?.[0] || "",
  703. deliveryDate: "",
  704. shopId: 0,
  705. shopPoNo: "",
  706. numberOfCartons: mergedPickOrder.pickOrderLines?.length || 0,
  707. qrCodeData: hierarchicalData.fgInfo.doPickOrderId,
  708. // 新增:多个 pick orders 信息 - 保持数组格式,不要 join
  709. numberOfPickOrders: mergedPickOrder.pickOrderIds?.length || 0,
  710. pickOrderIds: mergedPickOrder.pickOrderIds || [],
  711. pickOrderCodes: Array.isArray(mergedPickOrder.pickOrderCodes)
  712. ? mergedPickOrder.pickOrderCodes
  713. : [], // 改:保持数组
  714. deliveryOrderIds: mergedPickOrder.doOrderIds || [],
  715. deliveryNos: Array.isArray(mergedPickOrder.deliveryOrderCodes)
  716. ? mergedPickOrder.deliveryOrderCodes
  717. : [], // 改:保持数组
  718. lineCountsPerPickOrder: Array.isArray(mergedPickOrder.lineCountsPerPickOrder)
  719. ? mergedPickOrder.lineCountsPerPickOrder
  720. : []
  721. };
  722. setFgPickOrders([fgOrder]);
  723. console.log(" DEBUG fgOrder.lineCountsPerPickOrder:", fgOrder.lineCountsPerPickOrder);
  724. console.log(" DEBUG fgOrder.pickOrderCodes:", fgOrder.pickOrderCodes);
  725. console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
  726. // 移除:不需要 doPickOrderDetail 和 switcher 逻辑
  727. // if (hierarchicalData.pickOrders.length > 1) { ... }
  728. // 直接使用合并后的 pickOrderLines
  729. console.log("🎯 Displaying merged pick order lines");
  730. // 将层级数据转换为平铺格式(用于表格显示)
  731. const flatLotData: any[] = [];
  732. mergedPickOrder.pickOrderLines.forEach((line: any) => {
  733. // ✅ FIXED: 处理 lots(如果有)
  734. if (line.lots && line.lots.length > 0) {
  735. // 修复:先对 lots 按 lotId 去重并合并 requiredQty
  736. const lotMap = new Map<number, any>();
  737. line.lots.forEach((lot: any) => {
  738. const lotId = lot.id;
  739. if (lotMap.has(lotId)) {
  740. // 如果已存在,合并 requiredQty
  741. const existingLot = lotMap.get(lotId);
  742. existingLot.requiredQty = (existingLot.requiredQty || 0) + (lot.requiredQty || 0);
  743. // 保留其他字段(使用第一个遇到的 lot 的字段)
  744. } else {
  745. // 首次遇到,添加到 map
  746. lotMap.set(lotId, { ...lot });
  747. }
  748. });
  749. // 遍历去重后的 lots
  750. lotMap.forEach((lot: any) => {
  751. flatLotData.push({
  752. // 使用合并后的数据
  753. pickOrderConsoCode: mergedPickOrder.consoCode,
  754. pickOrderTargetDate: mergedPickOrder.targetDate,
  755. pickOrderStatus: mergedPickOrder.status,
  756. pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0, // 使用第一个 pickOrderId
  757. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  758. pickOrderLineId: line.id,
  759. pickOrderLineRequiredQty: line.requiredQty,
  760. pickOrderLineStatus: line.status,
  761. itemId: line.item.id,
  762. itemCode: line.item.code,
  763. itemName: line.item.name,
  764. uomDesc: line.item.uomDesc,
  765. uomShortDesc: line.item.uomShortDesc,
  766. lotId: lot.id,
  767. lotNo: lot.lotNo,
  768. expiryDate: lot.expiryDate,
  769. location: lot.location,
  770. stockUnit: lot.stockUnit,
  771. availableQty: lot.availableQty,
  772. requiredQty: lot.requiredQty, // 使用合并后的 requiredQty
  773. actualPickQty: lot.actualPickQty,
  774. inQty: lot.inQty,
  775. outQty: lot.outQty,
  776. holdQty: lot.holdQty,
  777. lotStatus: lot.lotStatus,
  778. lotAvailability: lot.lotAvailability,
  779. processingStatus: lot.processingStatus,
  780. suggestedPickLotId: lot.suggestedPickLotId,
  781. stockOutLineId: lot.stockOutLineId,
  782. stockOutLineStatus: lot.stockOutLineStatus,
  783. stockOutLineQty: lot.stockOutLineQty,
  784. stockInLineId: lot.stockInLineId,
  785. routerId: lot.router?.id,
  786. routerIndex: lot.router?.index,
  787. routerRoute: lot.router?.route,
  788. routerArea: lot.router?.area,
  789. noLot: false,
  790. });
  791. });
  792. }
  793. // ✅ FIXED: 同时处理 stockouts(无论是否有 lots)
  794. if (line.stockouts && line.stockouts.length > 0) {
  795. // ✅ FIXED: 处理所有 stockouts,而不仅仅是第一个
  796. line.stockouts.forEach((stockout: any) => {
  797. flatLotData.push({
  798. pickOrderConsoCode: mergedPickOrder.consoCodes?.[0] || "",
  799. pickOrderTargetDate: mergedPickOrder.targetDate,
  800. pickOrderStatus: mergedPickOrder.status,
  801. pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0,
  802. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  803. pickOrderLineId: line.id,
  804. pickOrderLineRequiredQty: line.requiredQty,
  805. pickOrderLineStatus: line.status,
  806. itemId: line.item.id,
  807. itemCode: line.item.code,
  808. itemName: line.item.name,
  809. uomDesc: line.item.uomDesc,
  810. uomShortDesc: line.item.uomShortDesc,
  811. // Null stock 字段 - 从 stockouts 数组中获取
  812. lotId: stockout.lotId || null,
  813. lotNo: stockout.lotNo || null,
  814. expiryDate: null,
  815. location: stockout.location || null,
  816. stockUnit: line.item.uomDesc,
  817. availableQty: stockout.availableQty || 0,
  818. requiredQty: line.requiredQty,
  819. actualPickQty: stockout.qty || 0,
  820. inQty: 0,
  821. outQty: 0,
  822. holdQty: 0,
  823. lotStatus: 'unavailable',
  824. lotAvailability: 'insufficient_stock',
  825. processingStatus: stockout.status || 'pending',
  826. suggestedPickLotId: null,
  827. stockOutLineId: stockout.id || null, // 使用 stockouts 数组中的 id
  828. stockOutLineStatus: stockout.status || null,
  829. stockOutLineQty: stockout.qty || 0,
  830. routerId: null,
  831. routerIndex: 999999,
  832. routerRoute: null,
  833. routerArea: null,
  834. noLot: true,
  835. });
  836. });
  837. }
  838. });
  839. console.log(" Transformed flat lot data:", flatLotData);
  840. console.log(" Total items (including null stock):", flatLotData.length);
  841. setCombinedLotData(flatLotData);
  842. setOriginalCombinedData(flatLotData);
  843. checkAllLotsCompleted(flatLotData);
  844. } catch (error) {
  845. console.error(" Error fetching combined lot data:", error);
  846. setCombinedLotData([]);
  847. setOriginalCombinedData([]);
  848. setAllLotsCompleted(false);
  849. } finally {
  850. setCombinedDataLoading(false);
  851. }
  852. }, [currentUserId, checkAllLotsCompleted]); // 移除 selectedPickOrderId 依赖
  853. // Add effect to check completion when lot data changes
  854. const handleManualLotConfirmation = useCallback(async (currentLotNo: string, newLotNo: string) => {
  855. console.log(` Manual lot confirmation: Current=${currentLotNo}, New=${newLotNo}`);
  856. // 使用第一个输入框的 lot number 查找当前数据
  857. const currentLot = combinedLotData.find(lot =>
  858. lot.lotNo && lot.lotNo === currentLotNo
  859. );
  860. if (!currentLot) {
  861. console.error(`❌ Current lot not found: ${currentLotNo}`);
  862. alert(t("Current lot number not found. Please verify and try again."));
  863. return;
  864. }
  865. if (!currentLot.stockOutLineId) {
  866. console.error("❌ No stockOutLineId found for current lot");
  867. alert(t("No stock out line found for current lot. Please contact administrator."));
  868. return;
  869. }
  870. setIsConfirmingLot(true);
  871. try {
  872. // 调用 updateStockOutLineStatusByQRCodeAndLotNo API
  873. // 第一个 lot 用于获取 pickOrderLineId, stockOutLineId, itemId
  874. // 第二个 lot 作为 inventoryLotNo
  875. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  876. pickOrderLineId: currentLot.pickOrderLineId,
  877. inventoryLotNo: newLotNo, // 第二个输入框的值
  878. stockOutLineId: currentLot.stockOutLineId,
  879. itemId: currentLot.itemId,
  880. status: "checked",
  881. });
  882. console.log("📥 updateStockOutLineStatusByQRCodeAndLotNo result:", res);
  883. if (res.code === "checked" || res.code === "SUCCESS") {
  884. // ✅ 更新本地状态
  885. const entity = res.entity as any;
  886. setCombinedLotData(prev => prev.map(lot => {
  887. if (lot.stockOutLineId === currentLot.stockOutLineId &&
  888. lot.pickOrderLineId === currentLot.pickOrderLineId) {
  889. return {
  890. ...lot,
  891. stockOutLineStatus: 'checked',
  892. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  893. };
  894. }
  895. return lot;
  896. }));
  897. setOriginalCombinedData(prev => prev.map(lot => {
  898. if (lot.stockOutLineId === currentLot.stockOutLineId &&
  899. lot.pickOrderLineId === currentLot.pickOrderLineId) {
  900. return {
  901. ...lot,
  902. stockOutLineStatus: 'checked',
  903. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  904. };
  905. }
  906. return lot;
  907. }));
  908. console.log("✅ Lot substitution completed successfully");
  909. setQrScanSuccess(true);
  910. setQrScanError(false);
  911. // 关闭手动输入模态框
  912. setManualLotConfirmationOpen(false);
  913. // 刷新数据
  914. await fetchAllCombinedLotData();
  915. } else if (res.code === "LOT_NUMBER_MISMATCH") {
  916. console.warn("⚠️ Backend reported LOT_NUMBER_MISMATCH:", res.message);
  917. // ✅ 打开 lot confirmation modal 而不是显示 alert
  918. // 从响应消息中提取 expected lot number(如果可能)
  919. // 或者使用 currentLotNo 作为 expected lot
  920. const expectedLotNo = currentLotNo; // 当前 lot 是期望的
  921. // 查找新 lot 的信息(如果存在于 combinedLotData 中)
  922. const newLot = combinedLotData.find(lot =>
  923. lot.lotNo && lot.lotNo === newLotNo
  924. );
  925. // 设置 expected lot data
  926. setExpectedLotData({
  927. lotNo: expectedLotNo,
  928. itemCode: currentLot.itemCode || '',
  929. itemName: currentLot.itemName || ''
  930. });
  931. // 设置 scanned lot data
  932. setScannedLotData({
  933. lotNo: newLotNo,
  934. itemCode: newLot?.itemCode || currentLot.itemCode || '',
  935. itemName: newLot?.itemName || currentLot.itemName || '',
  936. inventoryLotLineId: newLot?.lotId || null,
  937. stockInLineId: null // 手动输入时可能没有 stockInLineId
  938. });
  939. // 设置 selectedLotForQr 为当前 lot
  940. setSelectedLotForQr(currentLot);
  941. // 关闭手动输入模态框
  942. setManualLotConfirmationOpen(false);
  943. // 打开 lot confirmation modal
  944. setLotConfirmationOpen(true);
  945. setQrScanError(false); // 不显示错误,因为会打开确认模态框
  946. setQrScanSuccess(false);
  947. } else if (res.code === "ITEM_MISMATCH") {
  948. console.warn("⚠️ Backend reported ITEM_MISMATCH:", res.message);
  949. alert(t("Item mismatch: {message}", { message: res.message || "" }));
  950. setQrScanError(true);
  951. setQrScanSuccess(false);
  952. // 关闭手动输入模态框
  953. setManualLotConfirmationOpen(false);
  954. } else {
  955. console.warn("⚠️ Unexpected response code:", res.code);
  956. alert(t("Failed to update lot status. Response: {code}", { code: res.code }));
  957. setQrScanError(true);
  958. setQrScanSuccess(false);
  959. // 关闭手动输入模态框
  960. setManualLotConfirmationOpen(false);
  961. }
  962. } catch (error) {
  963. console.error("❌ Error in manual lot confirmation:", error);
  964. alert(t("Failed to confirm lot substitution. Please try again."));
  965. setQrScanError(true);
  966. setQrScanSuccess(false);
  967. // 关闭手动输入模态框
  968. setManualLotConfirmationOpen(false);
  969. } finally {
  970. setIsConfirmingLot(false);
  971. }
  972. }, [combinedLotData, fetchAllCombinedLotData, t]);
  973. useEffect(() => {
  974. if (combinedLotData.length > 0) {
  975. checkAllLotsCompleted(combinedLotData);
  976. }
  977. }, [combinedLotData, checkAllLotsCompleted]);
  978. // Add function to expose completion status to parent
  979. const getCompletionStatus = useCallback(() => {
  980. return allLotsCompleted;
  981. }, [allLotsCompleted]);
  982. // Expose completion status to parent component
  983. useEffect(() => {
  984. // Dispatch custom event with completion status
  985. const event = new CustomEvent('pickOrderCompletionStatus', {
  986. detail: {
  987. allLotsCompleted,
  988. tabIndex: 1 // 明确指定这是来自标签页 1 的事件
  989. }
  990. });
  991. window.dispatchEvent(event);
  992. }, [allLotsCompleted]);
  993. const handleLotConfirmation = useCallback(async () => {
  994. if (!expectedLotData || !scannedLotData || !selectedLotForQr) return;
  995. setIsConfirmingLot(true);
  996. try {
  997. const newLotNo = scannedLotData?.lotNo;
  998. const newStockInLineId = scannedLotData?.stockInLineId;
  999. await confirmLotSubstitution({
  1000. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  1001. stockOutLineId: selectedLotForQr.stockOutLineId,
  1002. originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId,
  1003. newInventoryLotNo: "",
  1004. newStockInLineId: newStockInLineId
  1005. });
  1006. setQrScanError(false);
  1007. setQrScanSuccess(false);
  1008. setQrScanInput('');
  1009. // ✅ 修复:在确认后重置扫描状态,避免重复处理
  1010. resetScan();
  1011. // ✅ 修复:不要清空 processedQrCodes,而是保留当前 QR code 的标记
  1012. // 或者如果确实需要清空,应该在重置扫描后再清空
  1013. // setProcessedQrCodes(new Set());
  1014. // setLastProcessedQr('');
  1015. setPickExecutionFormOpen(false);
  1016. if(selectedLotForQr?.stockOutLineId){
  1017. const stockOutLineUpdate = await updateStockOutLineStatus({
  1018. id: selectedLotForQr.stockOutLineId,
  1019. status: 'checked',
  1020. qty: 0
  1021. });
  1022. }
  1023. // ✅ 修复:先关闭 modal 和清空状态,再刷新数据
  1024. setLotConfirmationOpen(false);
  1025. setExpectedLotData(null);
  1026. setScannedLotData(null);
  1027. setSelectedLotForQr(null);
  1028. // ✅ 修复:刷新数据前设置刷新标志,避免在刷新期间处理新的 QR code
  1029. setIsRefreshingData(true);
  1030. await fetchAllCombinedLotData();
  1031. setIsRefreshingData(false);
  1032. } catch (error) {
  1033. console.error("Error confirming lot substitution:", error);
  1034. } finally {
  1035. setIsConfirmingLot(false);
  1036. }
  1037. }, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData, resetScan]);
  1038. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  1039. console.log(` Processing QR Code for lot: ${lotNo}`);
  1040. // 检查 lotNo 是否为 null 或 undefined(包括字符串 "null")
  1041. if (!lotNo || lotNo === 'null' || lotNo.trim() === '') {
  1042. console.error(" Invalid lotNo: null, undefined, or empty");
  1043. return;
  1044. }
  1045. // Use current data without refreshing to avoid infinite loop
  1046. const currentLotData = combinedLotData;
  1047. console.log(` Available lots:`, currentLotData.map(lot => lot.lotNo));
  1048. // 修复:在比较前确保 lotNo 不为 null
  1049. const lotNoLower = lotNo.toLowerCase();
  1050. const matchingLots = currentLotData.filter(lot => {
  1051. if (!lot.lotNo) return false; // 跳过 null lotNo
  1052. return lot.lotNo === lotNo || lot.lotNo.toLowerCase() === lotNoLower;
  1053. });
  1054. if (matchingLots.length === 0) {
  1055. console.error(` Lot not found: ${lotNo}`);
  1056. setQrScanError(true);
  1057. setQrScanSuccess(false);
  1058. const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', ');
  1059. console.log(` QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`);
  1060. return;
  1061. }
  1062. console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
  1063. setQrScanError(false);
  1064. try {
  1065. let successCount = 0;
  1066. let errorCount = 0;
  1067. for (const matchingLot of matchingLots) {
  1068. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  1069. if (matchingLot.stockOutLineId) {
  1070. const stockOutLineUpdate = await updateStockOutLineStatus({
  1071. id: matchingLot.stockOutLineId,
  1072. status: 'checked',
  1073. qty: 0
  1074. });
  1075. console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
  1076. // Treat multiple backend shapes as success (type-safe via any)
  1077. const r: any = stockOutLineUpdate as any;
  1078. const updateOk =
  1079. r?.code === 'SUCCESS' ||
  1080. typeof r?.id === 'number' ||
  1081. r?.type === 'checked' ||
  1082. r?.status === 'checked' ||
  1083. typeof r?.entity?.id === 'number' ||
  1084. r?.entity?.status === 'checked';
  1085. if (updateOk) {
  1086. successCount++;
  1087. } else {
  1088. errorCount++;
  1089. }
  1090. } else {
  1091. const createStockOutLineData = {
  1092. consoCode: matchingLot.pickOrderConsoCode,
  1093. pickOrderLineId: matchingLot.pickOrderLineId,
  1094. inventoryLotLineId: matchingLot.lotId,
  1095. qty: 0
  1096. };
  1097. const createResult = await createStockOutLine(createStockOutLineData);
  1098. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult);
  1099. if (createResult && createResult.code === "SUCCESS") {
  1100. // Immediately set status to checked for new line
  1101. let newSolId: number | undefined;
  1102. const anyRes: any = createResult as any;
  1103. if (typeof anyRes?.id === 'number') {
  1104. newSolId = anyRes.id;
  1105. } else if (anyRes?.entity) {
  1106. newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id;
  1107. }
  1108. if (newSolId) {
  1109. const setChecked = await updateStockOutLineStatus({
  1110. id: newSolId,
  1111. status: 'checked',
  1112. qty: 0
  1113. });
  1114. if (setChecked && setChecked.code === "SUCCESS") {
  1115. successCount++;
  1116. } else {
  1117. errorCount++;
  1118. }
  1119. } else {
  1120. console.warn("Created stock out line but no ID returned; cannot set to checked");
  1121. errorCount++;
  1122. }
  1123. } else {
  1124. errorCount++;
  1125. }
  1126. }
  1127. }
  1128. // FIXED: Set refresh flag before refreshing data
  1129. setIsRefreshingData(true);
  1130. console.log("🔄 Refreshing data after QR code processing...");
  1131. await fetchAllCombinedLotData();
  1132. if (successCount > 0) {
  1133. console.log(` QR Code processing completed: ${successCount} updated/created`);
  1134. setQrScanSuccess(true);
  1135. setQrScanError(false);
  1136. setQrScanInput(''); // Clear input after successful processing
  1137. //setIsManualScanning(false);
  1138. // stopScan();
  1139. // resetScan();
  1140. // Clear success state after a delay
  1141. //setTimeout(() => {
  1142. //setQrScanSuccess(false);
  1143. //}, 2000);
  1144. } else {
  1145. console.error(` QR Code processing failed: ${errorCount} errors`);
  1146. setQrScanError(true);
  1147. setQrScanSuccess(false);
  1148. // Clear error state after a delay
  1149. // setTimeout(() => {
  1150. // setQrScanError(false);
  1151. //}, 3000);
  1152. }
  1153. } catch (error) {
  1154. console.error(" Error processing QR code:", error);
  1155. setQrScanError(true);
  1156. setQrScanSuccess(false);
  1157. // Clear error state after a delay
  1158. setTimeout(() => {
  1159. setQrScanError(false);
  1160. }, 3000);
  1161. } finally {
  1162. // Clear refresh flag after a short delay
  1163. setTimeout(() => {
  1164. setIsRefreshingData(false);
  1165. }, 1000);
  1166. }
  1167. }, [combinedLotData]);
  1168. const handleFastQrScan = useCallback(async (lotNo: string) => {
  1169. const startTime = performance.now();
  1170. console.log(`⏱️ [FAST SCAN START] Lot: ${lotNo}`);
  1171. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  1172. // 从 combinedLotData 中找到对应的 lot
  1173. const findStartTime = performance.now();
  1174. const matchingLot = combinedLotData.find(lot =>
  1175. lot.lotNo && lot.lotNo === lotNo
  1176. );
  1177. const findTime = performance.now() - findStartTime;
  1178. console.log(`⏱️ Find lot time: ${findTime.toFixed(2)}ms`);
  1179. if (!matchingLot || !matchingLot.stockOutLineId) {
  1180. const totalTime = performance.now() - startTime;
  1181. console.warn(`⚠️ Fast scan: Lot ${lotNo} not found or no stockOutLineId`);
  1182. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
  1183. return;
  1184. }
  1185. try {
  1186. // ✅ 使用快速 API
  1187. const apiStartTime = performance.now();
  1188. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1189. pickOrderLineId: matchingLot.pickOrderLineId,
  1190. inventoryLotNo: lotNo,
  1191. stockOutLineId: matchingLot.stockOutLineId,
  1192. itemId: matchingLot.itemId,
  1193. status: "checked",
  1194. });
  1195. const apiTime = performance.now() - apiStartTime;
  1196. console.log(`⏱️ API call time: ${apiTime.toFixed(2)}ms`);
  1197. if (res.code === "checked" || res.code === "SUCCESS") {
  1198. // ✅ 只更新本地状态,不调用 fetchAllCombinedLotData
  1199. const updateStartTime = performance.now();
  1200. const entity = res.entity as any;
  1201. setCombinedLotData(prev => prev.map(lot => {
  1202. if (lot.stockOutLineId === matchingLot.stockOutLineId &&
  1203. lot.pickOrderLineId === matchingLot.pickOrderLineId) {
  1204. return {
  1205. ...lot,
  1206. stockOutLineStatus: 'checked',
  1207. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  1208. };
  1209. }
  1210. return lot;
  1211. }));
  1212. setOriginalCombinedData(prev => prev.map(lot => {
  1213. if (lot.stockOutLineId === matchingLot.stockOutLineId &&
  1214. lot.pickOrderLineId === matchingLot.pickOrderLineId) {
  1215. return {
  1216. ...lot,
  1217. stockOutLineStatus: 'checked',
  1218. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  1219. };
  1220. }
  1221. return lot;
  1222. }));
  1223. const updateTime = performance.now() - updateStartTime;
  1224. console.log(`⏱️ State update time: ${updateTime.toFixed(2)}ms`);
  1225. const totalTime = performance.now() - startTime;
  1226. console.log(`✅ [FAST SCAN END] Lot: ${lotNo}`);
  1227. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  1228. console.log(`⏰ End time: ${new Date().toISOString()}`);
  1229. } else {
  1230. const totalTime = performance.now() - startTime;
  1231. console.warn(`⚠️ Fast scan failed for ${lotNo}:`, res.code);
  1232. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
  1233. }
  1234. } catch (error) {
  1235. const totalTime = performance.now() - startTime;
  1236. console.error(` Fast scan error for ${lotNo}:`, error);
  1237. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
  1238. }
  1239. }, [combinedLotData, updateStockOutLineStatusByQRCodeAndLotNo]);
  1240. // Enhanced lotDataIndexes with cached active lots for better performance
  1241. const lotDataIndexes = useMemo(() => {
  1242. const indexStartTime = performance.now();
  1243. console.log(`⏱️ [PERF] lotDataIndexes calculation START, data length: ${combinedLotData.length}`);
  1244. const byItemId = new Map<number, any[]>();
  1245. const byItemCode = new Map<string, any[]>();
  1246. const byLotId = new Map<number, any>();
  1247. const byLotNo = new Map<string, any[]>();
  1248. const byStockInLineId = new Map<number, any[]>();
  1249. // Cache active lots separately to avoid filtering on every scan
  1250. const activeLotsByItemId = new Map<number, any[]>();
  1251. const rejectedStatuses = new Set(['rejected']);
  1252. // ✅ Use for loop instead of forEach for better performance on tablets
  1253. for (let i = 0; i < combinedLotData.length; i++) {
  1254. const lot = combinedLotData[i];
  1255. const isActive = !rejectedStatuses.has(lot.lotAvailability) &&
  1256. !rejectedStatuses.has(lot.stockOutLineStatus) &&
  1257. !rejectedStatuses.has(lot.processingStatus);
  1258. if (lot.itemId) {
  1259. if (!byItemId.has(lot.itemId)) {
  1260. byItemId.set(lot.itemId, []);
  1261. activeLotsByItemId.set(lot.itemId, []);
  1262. }
  1263. byItemId.get(lot.itemId)!.push(lot);
  1264. if (isActive) {
  1265. activeLotsByItemId.get(lot.itemId)!.push(lot);
  1266. }
  1267. }
  1268. if (lot.itemCode) {
  1269. if (!byItemCode.has(lot.itemCode)) {
  1270. byItemCode.set(lot.itemCode, []);
  1271. }
  1272. byItemCode.get(lot.itemCode)!.push(lot);
  1273. }
  1274. if (lot.lotId) {
  1275. byLotId.set(lot.lotId, lot);
  1276. }
  1277. if (lot.lotNo) {
  1278. if (!byLotNo.has(lot.lotNo)) {
  1279. byLotNo.set(lot.lotNo, []);
  1280. }
  1281. byLotNo.get(lot.lotNo)!.push(lot);
  1282. }
  1283. if (lot.stockInLineId) {
  1284. if (!byStockInLineId.has(lot.stockInLineId)) {
  1285. byStockInLineId.set(lot.stockInLineId, []);
  1286. }
  1287. byStockInLineId.get(lot.stockInLineId)!.push(lot);
  1288. }
  1289. }
  1290. const indexTime = performance.now() - indexStartTime;
  1291. if (indexTime > 10) {
  1292. console.log(`⏱️ [PERF] lotDataIndexes calculation END: ${indexTime.toFixed(2)}ms (${(indexTime / 1000).toFixed(3)}s)`);
  1293. }
  1294. return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId };
  1295. }, [combinedLotData.length, combinedLotData]);
  1296. // Store resetScan in ref for immediate access (update on every render)
  1297. resetScanRef.current = resetScan;
  1298. const processOutsideQrCode = useCallback(async (latestQr: string) => {
  1299. const totalStartTime = performance.now();
  1300. console.log(`⏱️ [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`);
  1301. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  1302. // ✅ Measure index access time
  1303. const indexAccessStart = performance.now();
  1304. const indexes = lotDataIndexes; // Access the memoized indexes
  1305. const indexAccessTime = performance.now() - indexAccessStart;
  1306. console.log(`⏱️ [PERF] Index access time: ${indexAccessTime.toFixed(2)}ms`);
  1307. // 1) Parse JSON safely (parse once, reuse)
  1308. const parseStartTime = performance.now();
  1309. let qrData: any = null;
  1310. let parseTime = 0;
  1311. try {
  1312. qrData = JSON.parse(latestQr);
  1313. parseTime = performance.now() - parseStartTime;
  1314. console.log(`⏱️ [PERF] JSON parse time: ${parseTime.toFixed(2)}ms`);
  1315. } catch {
  1316. console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches.");
  1317. startTransition(() => {
  1318. setQrScanError(true);
  1319. setQrScanSuccess(false);
  1320. });
  1321. return;
  1322. }
  1323. try {
  1324. const validationStartTime = performance.now();
  1325. if (!(qrData?.stockInLineId && qrData?.itemId)) {
  1326. console.log("QR JSON missing required fields (itemId, stockInLineId).");
  1327. startTransition(() => {
  1328. setQrScanError(true);
  1329. setQrScanSuccess(false);
  1330. });
  1331. return;
  1332. }
  1333. const validationTime = performance.now() - validationStartTime;
  1334. console.log(`⏱️ [PERF] Validation time: ${validationTime.toFixed(2)}ms`);
  1335. const scannedItemId = qrData.itemId;
  1336. const scannedStockInLineId = qrData.stockInLineId;
  1337. // ✅ Check if this combination was already processed
  1338. const duplicateCheckStartTime = performance.now();
  1339. const itemProcessedSet = processedQrCombinations.get(scannedItemId);
  1340. if (itemProcessedSet?.has(scannedStockInLineId)) {
  1341. const duplicateCheckTime = performance.now() - duplicateCheckStartTime;
  1342. console.log(`⏱️ [SKIP] Already processed combination: itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId} (check time: ${duplicateCheckTime.toFixed(2)}ms)`);
  1343. return;
  1344. }
  1345. const duplicateCheckTime = performance.now() - duplicateCheckStartTime;
  1346. console.log(`⏱️ [PERF] Duplicate check time: ${duplicateCheckTime.toFixed(2)}ms`);
  1347. // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed)
  1348. const lookupStartTime = performance.now();
  1349. const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
  1350. // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected
  1351. const allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
  1352. const lookupTime = performance.now() - lookupStartTime;
  1353. console.log(`⏱️ [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${activeSuggestedLots.length} active lots, ${allLotsForItem.length} total lots`);
  1354. // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots
  1355. // This allows users to scan other lots even when all suggested lots are rejected
  1356. const scannedLot = allLotsForItem.find(
  1357. (lot: any) => lot.stockInLineId === scannedStockInLineId
  1358. );
  1359. if (scannedLot) {
  1360. const isRejected =
  1361. scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1362. scannedLot.lotAvailability === 'rejected' ||
  1363. scannedLot.lotAvailability === 'status_unavailable';
  1364. if (isRejected) {
  1365. console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`);
  1366. startTransition(() => {
  1367. setQrScanError(true);
  1368. setQrScanSuccess(false);
  1369. setQrScanErrorMsg(
  1370. `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
  1371. );
  1372. });
  1373. // Mark as processed to prevent re-processing
  1374. setProcessedQrCombinations(prev => {
  1375. const newMap = new Map(prev);
  1376. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1377. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1378. return newMap;
  1379. });
  1380. return;
  1381. }
  1382. }
  1383. // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching
  1384. if (activeSuggestedLots.length === 0) {
  1385. // Check if there are any lots for this item (even if all are rejected)
  1386. if (allLotsForItem.length === 0) {
  1387. console.error("No lots found for this item");
  1388. startTransition(() => {
  1389. setQrScanError(true);
  1390. setQrScanSuccess(false);
  1391. setQrScanErrorMsg("当前订单中没有此物品的批次信息");
  1392. });
  1393. return;
  1394. }
  1395. // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot
  1396. // This allows users to switch to a new lot even when all suggested lots are rejected
  1397. console.log(`⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching.`);
  1398. // Find a rejected lot as expected lot (the one that was rejected)
  1399. const rejectedLot = allLotsForItem.find((lot: any) =>
  1400. lot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1401. lot.lotAvailability === 'rejected' ||
  1402. lot.lotAvailability === 'status_unavailable'
  1403. );
  1404. const expectedLot = rejectedLot || allLotsForItem[0]; // Use rejected lot if exists, otherwise first lot
  1405. // ✅ Always open confirmation modal when no active lots (user needs to confirm switching)
  1406. // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed
  1407. console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`);
  1408. setSelectedLotForQr(expectedLot);
  1409. handleLotMismatch(
  1410. {
  1411. lotNo: expectedLot.lotNo,
  1412. itemCode: expectedLot.itemCode,
  1413. itemName: expectedLot.itemName
  1414. },
  1415. {
  1416. lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
  1417. itemCode: expectedLot.itemCode,
  1418. itemName: expectedLot.itemName,
  1419. inventoryLotLineId: scannedLot?.lotId || null,
  1420. stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
  1421. }
  1422. );
  1423. return;
  1424. }
  1425. // ✅ OPTIMIZATION: Direct Map lookup for stockInLineId match (O(1))
  1426. const matchStartTime = performance.now();
  1427. let exactMatch: any = null;
  1428. const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || [];
  1429. // Find exact match from stockInLineId index, then verify it's in active lots
  1430. for (let i = 0; i < stockInLineLots.length; i++) {
  1431. const lot = stockInLineLots[i];
  1432. if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) {
  1433. exactMatch = lot;
  1434. break;
  1435. }
  1436. }
  1437. const matchTime = performance.now() - matchStartTime;
  1438. console.log(`⏱️ [PERF] Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`);
  1439. // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots
  1440. // This handles the case where Lot A is rejected and user scans Lot B
  1441. // Also handle case where scanned lot is not in allLotsForItem (scannedLot is undefined)
  1442. if (!exactMatch) {
  1443. // Scanned lot is not in active suggested lots, open confirmation modal
  1444. const expectedLot = activeSuggestedLots[0] || allLotsForItem[0]; // Use first active lot or first lot as expected
  1445. if (expectedLot) {
  1446. // Check if scanned lot is different from expected, or if scannedLot is undefined (not in allLotsForItem)
  1447. const shouldOpenModal = !scannedLot || (scannedLot.stockInLineId !== expectedLot.stockInLineId);
  1448. if (shouldOpenModal) {
  1449. console.log(`⚠️ [QR PROCESS] Opening confirmation modal (scanned lot ${scannedLot?.lotNo || 'not in data'} is not in active suggested lots)`);
  1450. setSelectedLotForQr(expectedLot);
  1451. handleLotMismatch(
  1452. {
  1453. lotNo: expectedLot.lotNo,
  1454. itemCode: expectedLot.itemCode,
  1455. itemName: expectedLot.itemName
  1456. },
  1457. {
  1458. lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
  1459. itemCode: expectedLot.itemCode,
  1460. itemName: expectedLot.itemName,
  1461. inventoryLotLineId: scannedLot?.lotId || null,
  1462. stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
  1463. }
  1464. );
  1465. return;
  1466. }
  1467. }
  1468. }
  1469. if (exactMatch) {
  1470. // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认
  1471. console.log(`✅ Exact stockInLineId match found for lot: ${exactMatch.lotNo}`);
  1472. if (!exactMatch.stockOutLineId) {
  1473. console.warn("No stockOutLineId on exactMatch, cannot update status by QR.");
  1474. startTransition(() => {
  1475. setQrScanError(true);
  1476. setQrScanSuccess(false);
  1477. });
  1478. return;
  1479. }
  1480. try {
  1481. const apiStartTime = performance.now();
  1482. console.log(`⏱️ [API CALL START] Calling updateStockOutLineStatusByQRCodeAndLotNo`);
  1483. console.log(`⏰ [API CALL] API start time: ${new Date().toISOString()}`);
  1484. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1485. pickOrderLineId: exactMatch.pickOrderLineId,
  1486. inventoryLotNo: exactMatch.lotNo,
  1487. stockOutLineId: exactMatch.stockOutLineId,
  1488. itemId: exactMatch.itemId,
  1489. status: "checked",
  1490. });
  1491. const apiTime = performance.now() - apiStartTime;
  1492. console.log(`⏱️ [API CALL END] Total API time: ${apiTime.toFixed(2)}ms (${(apiTime / 1000).toFixed(3)}s)`);
  1493. console.log(`⏰ [API CALL] API end time: ${new Date().toISOString()}`);
  1494. if (res.code === "checked" || res.code === "SUCCESS") {
  1495. const entity = res.entity as any;
  1496. // ✅ Batch state updates using startTransition
  1497. const stateUpdateStartTime = performance.now();
  1498. startTransition(() => {
  1499. setQrScanError(false);
  1500. setQrScanSuccess(true);
  1501. setCombinedLotData(prev => prev.map(lot => {
  1502. if (lot.stockOutLineId === exactMatch.stockOutLineId &&
  1503. lot.pickOrderLineId === exactMatch.pickOrderLineId) {
  1504. return {
  1505. ...lot,
  1506. stockOutLineStatus: 'checked',
  1507. stockOutLineQty: entity?.qty ?? lot.stockOutLineQty,
  1508. };
  1509. }
  1510. return lot;
  1511. }));
  1512. setOriginalCombinedData(prev => prev.map(lot => {
  1513. if (lot.stockOutLineId === exactMatch.stockOutLineId &&
  1514. lot.pickOrderLineId === exactMatch.pickOrderLineId) {
  1515. return {
  1516. ...lot,
  1517. stockOutLineStatus: 'checked',
  1518. stockOutLineQty: entity?.qty ?? lot.stockOutLineQty,
  1519. };
  1520. }
  1521. return lot;
  1522. }));
  1523. });
  1524. const stateUpdateTime = performance.now() - stateUpdateStartTime;
  1525. console.log(`⏱️ [PERF] State update time: ${stateUpdateTime.toFixed(2)}ms`);
  1526. // Mark this combination as processed
  1527. const markProcessedStartTime = performance.now();
  1528. setProcessedQrCombinations(prev => {
  1529. const newMap = new Map(prev);
  1530. if (!newMap.has(scannedItemId)) {
  1531. newMap.set(scannedItemId, new Set());
  1532. }
  1533. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1534. return newMap;
  1535. });
  1536. const markProcessedTime = performance.now() - markProcessedStartTime;
  1537. console.log(`⏱️ [PERF] Mark processed time: ${markProcessedTime.toFixed(2)}ms`);
  1538. const totalTime = performance.now() - totalStartTime;
  1539. console.log(`✅ [PROCESS OUTSIDE QR END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  1540. console.log(`⏰ End time: ${new Date().toISOString()}`);
  1541. console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(2)}ms, stateUpdate=${stateUpdateTime.toFixed(2)}ms, markProcessed=${markProcessedTime.toFixed(2)}ms`);
  1542. console.log("✅ Status updated locally, no full data refresh needed");
  1543. } else {
  1544. console.warn("Unexpected response code from backend:", res.code);
  1545. startTransition(() => {
  1546. setQrScanError(true);
  1547. setQrScanSuccess(false);
  1548. });
  1549. }
  1550. } catch (e) {
  1551. const totalTime = performance.now() - totalStartTime;
  1552. console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`);
  1553. console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e);
  1554. startTransition(() => {
  1555. setQrScanError(true);
  1556. setQrScanSuccess(false);
  1557. });
  1558. }
  1559. return; // ✅ 直接返回,不需要确认表单
  1560. }
  1561. // ✅ Case 2: itemId 匹配但 stockInLineId 不匹配 - 显示确认表单
  1562. // Check if we should allow reopening (different stockInLineId)
  1563. const mismatchCheckStartTime = performance.now();
  1564. const itemProcessedSet2 = processedQrCombinations.get(scannedItemId);
  1565. if (itemProcessedSet2?.has(scannedStockInLineId)) {
  1566. const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
  1567. console.log(`⏱️ [SKIP] Already processed this exact combination (check time: ${mismatchCheckTime.toFixed(2)}ms)`);
  1568. return;
  1569. }
  1570. const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
  1571. console.log(`⏱️ [PERF] Mismatch check time: ${mismatchCheckTime.toFixed(2)}ms`);
  1572. // 取第一个活跃的 lot 作为期望的 lot
  1573. const expectedLotStartTime = performance.now();
  1574. const expectedLot = activeSuggestedLots[0];
  1575. if (!expectedLot) {
  1576. console.error("Could not determine expected lot for confirmation");
  1577. startTransition(() => {
  1578. setQrScanError(true);
  1579. setQrScanSuccess(false);
  1580. });
  1581. return;
  1582. }
  1583. const expectedLotTime = performance.now() - expectedLotStartTime;
  1584. console.log(`⏱️ [PERF] Get expected lot time: ${expectedLotTime.toFixed(2)}ms`);
  1585. // ✅ 立即打开确认模态框,不等待其他操作
  1586. console.log(`⚠️ Lot mismatch: Expected stockInLineId=${expectedLot.stockInLineId}, Scanned stockInLineId=${scannedStockInLineId}`);
  1587. // Set selected lot immediately (no transition delay)
  1588. const setSelectedLotStartTime = performance.now();
  1589. setSelectedLotForQr(expectedLot);
  1590. const setSelectedLotTime = performance.now() - setSelectedLotStartTime;
  1591. console.log(`⏱️ [PERF] Set selected lot time: ${setSelectedLotTime.toFixed(2)}ms`);
  1592. // ✅ 获取扫描的 lot 信息(从 QR 数据中提取,或使用默认值)
  1593. // Call handleLotMismatch immediately - it will open the modal
  1594. const handleMismatchStartTime = performance.now();
  1595. handleLotMismatch(
  1596. {
  1597. lotNo: expectedLot.lotNo,
  1598. itemCode: expectedLot.itemCode,
  1599. itemName: expectedLot.itemName
  1600. },
  1601. {
  1602. lotNo: null, // 扫描的 lotNo 未知,需要从后端获取或显示为未知
  1603. itemCode: expectedLot.itemCode,
  1604. itemName: expectedLot.itemName,
  1605. inventoryLotLineId: null,
  1606. stockInLineId: scannedStockInLineId // ✅ 传递 stockInLineId
  1607. }
  1608. );
  1609. const handleMismatchTime = performance.now() - handleMismatchStartTime;
  1610. console.log(`⏱️ [PERF] Handle mismatch call time: ${handleMismatchTime.toFixed(2)}ms`);
  1611. const totalTime = performance.now() - totalStartTime;
  1612. console.log(`⚠️ [PROCESS OUTSIDE QR MISMATCH] Total time before modal: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  1613. console.log(`⏰ End time: ${new Date().toISOString()}`);
  1614. console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, validation=${validationTime.toFixed(2)}ms, duplicateCheck=${duplicateCheckTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, mismatchCheck=${mismatchCheckTime.toFixed(2)}ms, expectedLot=${expectedLotTime.toFixed(2)}ms, setSelectedLot=${setSelectedLotTime.toFixed(2)}ms, handleMismatch=${handleMismatchTime.toFixed(2)}ms`);
  1615. } catch (error) {
  1616. const totalTime = performance.now() - totalStartTime;
  1617. console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`);
  1618. console.error("Error during QR code processing:", error);
  1619. startTransition(() => {
  1620. setQrScanError(true);
  1621. setQrScanSuccess(false);
  1622. });
  1623. return;
  1624. }
  1625. }, [lotDataIndexes, handleLotMismatch, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached]);
  1626. // Store processOutsideQrCode in ref for immediate access (update on every render)
  1627. processOutsideQrCodeRef.current = processOutsideQrCode;
  1628. useEffect(() => {
  1629. // Skip if scanner is not active or no data available
  1630. if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
  1631. return;
  1632. }
  1633. const qrValuesChangeStartTime = performance.now();
  1634. console.log(`⏱️ [QR VALUES EFFECT] Triggered at: ${new Date().toISOString()}`);
  1635. console.log(`⏱️ [QR VALUES EFFECT] qrValues.length: ${qrValues.length}`);
  1636. console.log(`⏱️ [QR VALUES EFFECT] qrValues:`, qrValues);
  1637. const latestQr = qrValues[qrValues.length - 1];
  1638. console.log(`⏱️ [QR VALUES EFFECT] Latest QR: ${latestQr}`);
  1639. console.log(`⏰ [QR VALUES EFFECT] Latest QR detected at: ${new Date().toISOString()}`);
  1640. // ✅ FIXED: Handle test shortcut {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId
  1641. // Support both formats: {2fitest (2 t's) and {2fittest (3 t's)
  1642. if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) {
  1643. // Extract content: remove "{2fitest" or "{2fittest" and "}"
  1644. let content = '';
  1645. if (latestQr.startsWith("{2fittest")) {
  1646. content = latestQr.substring(9, latestQr.length - 1); // Remove "{2fittest" and "}"
  1647. } else if (latestQr.startsWith("{2fitest")) {
  1648. content = latestQr.substring(8, latestQr.length - 1); // Remove "{2fitest" and "}"
  1649. }
  1650. const parts = content.split(',');
  1651. if (parts.length === 2) {
  1652. const itemId = parseInt(parts[0].trim(), 10);
  1653. const stockInLineId = parseInt(parts[1].trim(), 10);
  1654. if (!isNaN(itemId) && !isNaN(stockInLineId)) {
  1655. console.log(
  1656. `%c TEST QR: Detected ${latestQr.substring(0, 9)}... - Simulating QR input (itemId=${itemId}, stockInLineId=${stockInLineId})`,
  1657. "color: purple; font-weight: bold"
  1658. );
  1659. // ✅ Simulate QR code JSON format
  1660. const simulatedQr = JSON.stringify({
  1661. itemId: itemId,
  1662. stockInLineId: stockInLineId
  1663. });
  1664. console.log(`⏱️ [TEST QR] Simulated QR content: ${simulatedQr}`);
  1665. console.log(`⏱️ [TEST QR] Start time: ${new Date().toISOString()}`);
  1666. const testStartTime = performance.now();
  1667. // ✅ Mark as processed FIRST to avoid duplicate processing
  1668. lastProcessedQrRef.current = latestQr;
  1669. processedQrCodesRef.current.add(latestQr);
  1670. if (processedQrCodesRef.current.size > 100) {
  1671. const firstValue = processedQrCodesRef.current.values().next().value;
  1672. if (firstValue !== undefined) {
  1673. processedQrCodesRef.current.delete(firstValue);
  1674. }
  1675. }
  1676. setLastProcessedQr(latestQr);
  1677. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1678. // ✅ Process immediately (bypass QR scanner delay)
  1679. if (processOutsideQrCodeRef.current) {
  1680. processOutsideQrCodeRef.current(simulatedQr).then(() => {
  1681. const testTime = performance.now() - testStartTime;
  1682. console.log(`⏱️ [TEST QR] Total processing time: ${testTime.toFixed(2)}ms (${(testTime / 1000).toFixed(3)}s)`);
  1683. console.log(`⏱️ [TEST QR] End time: ${new Date().toISOString()}`);
  1684. }).catch((error) => {
  1685. const testTime = performance.now() - testStartTime;
  1686. console.error(`❌ [TEST QR] Error after ${testTime.toFixed(2)}ms:`, error);
  1687. });
  1688. }
  1689. // Reset scan
  1690. if (resetScanRef.current) {
  1691. resetScanRef.current();
  1692. }
  1693. const qrValuesChangeTime = performance.now() - qrValuesChangeStartTime;
  1694. console.log(`⏱️ [QR VALUES EFFECT] Test QR handling time: ${qrValuesChangeTime.toFixed(2)}ms`);
  1695. return; // ✅ IMPORTANT: Return early to prevent normal processing
  1696. } else {
  1697. console.warn(`⏱️ [TEST QR] Invalid itemId or stockInLineId: itemId=${parts[0]}, stockInLineId=${parts[1]}`);
  1698. }
  1699. } else {
  1700. console.warn(`⏱️ [TEST QR] Invalid format. Expected {2fitestx,y} or {2fittestx,y}, got: ${latestQr}`);
  1701. }
  1702. }
  1703. // Skip processing if confirmation modals are open
  1704. // BUT: Allow processing if modal was just closed (to allow reopening for different stockInLineId)
  1705. if (lotConfirmationOpen || manualLotConfirmationOpen) {
  1706. // Check if this is a different QR code than what triggered the modal
  1707. const modalTriggerQr = lastProcessedQrRef.current;
  1708. if (latestQr === modalTriggerQr) {
  1709. console.log(`⏱️ [QR PROCESS] Skipping - modal open for same QR: lotConfirmation=${lotConfirmationOpen}, manual=${manualLotConfirmationOpen}`);
  1710. return;
  1711. }
  1712. // If it's a different QR, allow processing (user might have canceled and scanned different lot)
  1713. console.log(`⏱️ [QR PROCESS] Different QR detected while modal open, allowing processing`);
  1714. }
  1715. const qrDetectionStartTime = performance.now();
  1716. console.log(`⏱️ [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`);
  1717. console.log(`⏰ [QR DETECTION] Detection time: ${new Date().toISOString()}`);
  1718. console.log(`⏱️ [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`);
  1719. // Skip if already processed (use refs to avoid dependency issues and delays)
  1720. const checkProcessedStartTime = performance.now();
  1721. if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) {
  1722. const checkTime = performance.now() - checkProcessedStartTime;
  1723. console.log(`⏱️ [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`);
  1724. return;
  1725. }
  1726. const checkTime = performance.now() - checkProcessedStartTime;
  1727. console.log(`⏱️ [QR PROCESS] Not processed check time: ${checkTime.toFixed(2)}ms`);
  1728. // Handle special shortcut
  1729. if (latestQr === "{2fic}") {
  1730. console.log(" Detected {2fic} shortcut - opening manual lot confirmation form");
  1731. setManualLotConfirmationOpen(true);
  1732. if (resetScanRef.current) {
  1733. resetScanRef.current();
  1734. }
  1735. lastProcessedQrRef.current = latestQr;
  1736. processedQrCodesRef.current.add(latestQr);
  1737. if (processedQrCodesRef.current.size > 100) {
  1738. const firstValue = processedQrCodesRef.current.values().next().value;
  1739. if (firstValue !== undefined) {
  1740. processedQrCodesRef.current.delete(firstValue);
  1741. }
  1742. }
  1743. setLastProcessedQr(latestQr);
  1744. setProcessedQrCodes(prev => {
  1745. const newSet = new Set(prev);
  1746. newSet.add(latestQr);
  1747. if (newSet.size > 100) {
  1748. const firstValue = newSet.values().next().value;
  1749. if (firstValue !== undefined) {
  1750. newSet.delete(firstValue);
  1751. }
  1752. }
  1753. return newSet;
  1754. });
  1755. return;
  1756. }
  1757. // Process new QR code immediately (background mode - no modal)
  1758. // Check against refs to avoid state update delays
  1759. if (latestQr && latestQr !== lastProcessedQrRef.current) {
  1760. const processingStartTime = performance.now();
  1761. console.log(`⏱️ [QR PROCESS] Starting processing at: ${new Date().toISOString()}`);
  1762. console.log(`⏱️ [QR PROCESS] Time since detection: ${(processingStartTime - qrDetectionStartTime).toFixed(2)}ms`);
  1763. // ✅ Process immediately for better responsiveness
  1764. // Clear any pending debounced processing
  1765. if (qrProcessingTimeoutRef.current) {
  1766. clearTimeout(qrProcessingTimeoutRef.current);
  1767. qrProcessingTimeoutRef.current = null;
  1768. }
  1769. // Log immediately (console.log is synchronous)
  1770. console.log(`⏱️ [QR PROCESS] Processing new QR code with enhanced validation: ${latestQr}`);
  1771. // Update refs immediately (no state update delay) - do this FIRST
  1772. const refUpdateStartTime = performance.now();
  1773. lastProcessedQrRef.current = latestQr;
  1774. processedQrCodesRef.current.add(latestQr);
  1775. if (processedQrCodesRef.current.size > 100) {
  1776. const firstValue = processedQrCodesRef.current.values().next().value;
  1777. if (firstValue !== undefined) {
  1778. processedQrCodesRef.current.delete(firstValue);
  1779. }
  1780. }
  1781. const refUpdateTime = performance.now() - refUpdateStartTime;
  1782. console.log(`⏱️ [QR PROCESS] Ref update time: ${refUpdateTime.toFixed(2)}ms`);
  1783. // Process immediately in background - no modal/form needed, no delays
  1784. // Use ref to avoid dependency issues
  1785. const processCallStartTime = performance.now();
  1786. if (processOutsideQrCodeRef.current) {
  1787. processOutsideQrCodeRef.current(latestQr).then(() => {
  1788. const processCallTime = performance.now() - processCallStartTime;
  1789. const totalProcessingTime = performance.now() - processingStartTime;
  1790. console.log(`⏱️ [QR PROCESS] processOutsideQrCode call time: ${processCallTime.toFixed(2)}ms`);
  1791. console.log(`⏱️ [QR PROCESS] Total processing time: ${totalProcessingTime.toFixed(2)}ms (${(totalProcessingTime / 1000).toFixed(3)}s)`);
  1792. }).catch((error) => {
  1793. const processCallTime = performance.now() - processCallStartTime;
  1794. const totalProcessingTime = performance.now() - processingStartTime;
  1795. console.error(`❌ [QR PROCESS] processOutsideQrCode error after ${processCallTime.toFixed(2)}ms:`, error);
  1796. console.error(`❌ [QR PROCESS] Total processing time before error: ${totalProcessingTime.toFixed(2)}ms`);
  1797. });
  1798. }
  1799. // Update state for UI (but don't block on it)
  1800. const stateUpdateStartTime = performance.now();
  1801. setLastProcessedQr(latestQr);
  1802. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1803. const stateUpdateTime = performance.now() - stateUpdateStartTime;
  1804. console.log(`⏱️ [QR PROCESS] State update time: ${stateUpdateTime.toFixed(2)}ms`);
  1805. const detectionTime = performance.now() - qrDetectionStartTime;
  1806. const totalEffectTime = performance.now() - qrValuesChangeStartTime;
  1807. console.log(`⏱️ [QR DETECTION] Total detection time: ${detectionTime.toFixed(2)}ms`);
  1808. console.log(`⏱️ [QR VALUES EFFECT] Total effect time: ${totalEffectTime.toFixed(2)}ms`);
  1809. }
  1810. return () => {
  1811. if (qrProcessingTimeoutRef.current) {
  1812. clearTimeout(qrProcessingTimeoutRef.current);
  1813. qrProcessingTimeoutRef.current = null;
  1814. }
  1815. };
  1816. }, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]);
  1817. const renderCountRef = useRef(0);
  1818. const renderStartTimeRef = useRef<number | null>(null);
  1819. // Track render performance
  1820. useEffect(() => {
  1821. renderCountRef.current++;
  1822. const now = performance.now();
  1823. if (renderStartTimeRef.current !== null) {
  1824. const renderTime = now - renderStartTimeRef.current;
  1825. if (renderTime > 100) { // Only log slow renders (>100ms)
  1826. console.log(`⏱️ [PERF] Render #${renderCountRef.current} took ${renderTime.toFixed(2)}ms, combinedLotData length: ${combinedLotData.length}`);
  1827. }
  1828. renderStartTimeRef.current = null;
  1829. }
  1830. // Track when lotConfirmationOpen changes
  1831. if (lotConfirmationOpen) {
  1832. renderStartTimeRef.current = performance.now();
  1833. console.log(`⏱️ [PERF] Render triggered by lotConfirmationOpen=true`);
  1834. }
  1835. }, [combinedLotData.length, lotConfirmationOpen]);
  1836. // Auto-start scanner only once on mount
  1837. const scannerInitializedRef = useRef(false);
  1838. useEffect(() => {
  1839. if (session && currentUserId && !initializationRef.current) {
  1840. console.log(" Session loaded, initializing pick order...");
  1841. initializationRef.current = true;
  1842. // Only fetch existing data, no auto-assignment
  1843. fetchAllCombinedLotData();
  1844. }
  1845. }, [session, currentUserId, fetchAllCombinedLotData]);
  1846. // Separate effect for auto-starting scanner (only once, prevents multiple resets)
  1847. useEffect(() => {
  1848. if (session && currentUserId && !scannerInitializedRef.current) {
  1849. scannerInitializedRef.current = true;
  1850. // ✅ Auto-start scanner on mount for tablet use (background mode - no modal)
  1851. console.log("✅ Auto-starting QR scanner in background mode");
  1852. setIsManualScanning(true);
  1853. startScan();
  1854. }
  1855. }, [session, currentUserId, startScan]);
  1856. // Add event listener for manual assignment
  1857. useEffect(() => {
  1858. const handlePickOrderAssigned = () => {
  1859. console.log("🔄 Pick order assigned event received, refreshing data...");
  1860. fetchAllCombinedLotData();
  1861. };
  1862. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  1863. return () => {
  1864. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  1865. };
  1866. }, [fetchAllCombinedLotData]);
  1867. const handleManualInputSubmit = useCallback(() => {
  1868. if (qrScanInput.trim() !== '') {
  1869. handleQrCodeSubmit(qrScanInput.trim());
  1870. }
  1871. }, [qrScanInput, handleQrCodeSubmit]);
  1872. // Handle QR code submission from modal (internal scanning)
  1873. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  1874. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  1875. console.log(` QR Code verified for lot: ${lotNo}`);
  1876. const requiredQty = selectedLotForQr.requiredQty;
  1877. const lotId = selectedLotForQr.lotId;
  1878. // Create stock out line
  1879. try {
  1880. const stockOutLineUpdate = await updateStockOutLineStatus({
  1881. id: selectedLotForQr.stockOutLineId,
  1882. status: 'checked',
  1883. qty: selectedLotForQr.stockOutLineQty || 0
  1884. });
  1885. console.log("Stock out line updated successfully!");
  1886. setQrScanSuccess(true);
  1887. setQrScanError(false);
  1888. // Clear selected lot (scanner stays active)
  1889. setSelectedLotForQr(null);
  1890. // Set pick quantity
  1891. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  1892. setTimeout(() => {
  1893. setPickQtyData(prev => ({
  1894. ...prev,
  1895. [lotKey]: requiredQty
  1896. }));
  1897. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  1898. }, 500);
  1899. } catch (error) {
  1900. console.error("Error creating stock out line:", error);
  1901. }
  1902. }
  1903. }, [selectedLotForQr]);
  1904. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  1905. if (value === '' || value === null || value === undefined) {
  1906. setPickQtyData(prev => ({
  1907. ...prev,
  1908. [lotKey]: 0
  1909. }));
  1910. return;
  1911. }
  1912. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  1913. if (isNaN(numericValue)) {
  1914. setPickQtyData(prev => ({
  1915. ...prev,
  1916. [lotKey]: 0
  1917. }));
  1918. return;
  1919. }
  1920. setPickQtyData(prev => ({
  1921. ...prev,
  1922. [lotKey]: numericValue
  1923. }));
  1924. }, []);
  1925. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  1926. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  1927. const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
  1928. const checkAndAutoAssignNext = useCallback(async () => {
  1929. if (!currentUserId) return;
  1930. try {
  1931. const completionResponse = await checkPickOrderCompletion(currentUserId);
  1932. if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
  1933. console.log("Found completed pick orders, auto-assigning next...");
  1934. // 移除前端的自动分配逻辑,因为后端已经处理了
  1935. // await handleAutoAssignAndRelease(); // 删除这个函数
  1936. }
  1937. } catch (error) {
  1938. console.error("Error checking pick order completion:", error);
  1939. }
  1940. }, [currentUserId]);
  1941. // Handle reject lot
  1942. // Handle pick execution form
  1943. const handlePickExecutionForm = useCallback((lot: any) => {
  1944. console.log("=== Pick Execution Form ===");
  1945. console.log("Lot data:", lot);
  1946. if (!lot) {
  1947. console.warn("No lot data provided for pick execution form");
  1948. return;
  1949. }
  1950. console.log("Opening pick execution form for lot:", lot.lotNo);
  1951. setSelectedLotForExecutionForm(lot);
  1952. setPickExecutionFormOpen(true);
  1953. console.log("Pick execution form opened for lot ID:", lot.lotId);
  1954. }, []);
  1955. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  1956. try {
  1957. console.log("Pick execution form submitted:", data);
  1958. const issueData = {
  1959. ...data,
  1960. type: "Do", // Delivery Order Record 类型
  1961. pickerName: session?.user?.name || '',
  1962. };
  1963. const result = await recordPickExecutionIssue(issueData);
  1964. console.log("Pick execution issue recorded:", result);
  1965. if (result && result.code === "SUCCESS") {
  1966. console.log(" Pick execution issue recorded successfully");
  1967. } else {
  1968. console.error(" Failed to record pick execution issue:", result);
  1969. }
  1970. setPickExecutionFormOpen(false);
  1971. setSelectedLotForExecutionForm(null);
  1972. setQrScanError(false);
  1973. setQrScanSuccess(false);
  1974. setQrScanInput('');
  1975. // ✅ Keep scanner active after form submission - don't stop scanning
  1976. // Only clear processed QR codes for the specific lot, not all
  1977. // setIsManualScanning(false); // Removed - keep scanner active
  1978. // stopScan(); // Removed - keep scanner active
  1979. // resetScan(); // Removed - keep scanner active
  1980. // Don't clear all processed codes - only clear for this specific lot if needed
  1981. await fetchAllCombinedLotData();
  1982. } catch (error) {
  1983. console.error("Error submitting pick execution form:", error);
  1984. }
  1985. }, [fetchAllCombinedLotData]);
  1986. // Calculate remaining required quantity
  1987. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  1988. const requiredQty = lot.requiredQty || 0;
  1989. const stockOutLineQty = lot.stockOutLineQty || 0;
  1990. return Math.max(0, requiredQty - stockOutLineQty);
  1991. }, []);
  1992. // Search criteria
  1993. const searchCriteria: Criterion<any>[] = [
  1994. {
  1995. label: t("Pick Order Code"),
  1996. paramName: "pickOrderCode",
  1997. type: "text",
  1998. },
  1999. {
  2000. label: t("Item Code"),
  2001. paramName: "itemCode",
  2002. type: "text",
  2003. },
  2004. {
  2005. label: t("Item Name"),
  2006. paramName: "itemName",
  2007. type: "text",
  2008. },
  2009. {
  2010. label: t("Lot No"),
  2011. paramName: "lotNo",
  2012. type: "text",
  2013. },
  2014. ];
  2015. const handleSearch = useCallback((query: Record<string, any>) => {
  2016. setSearchQuery({ ...query });
  2017. console.log("Search query:", query);
  2018. if (!originalCombinedData) return;
  2019. const filtered = originalCombinedData.filter((lot: any) => {
  2020. const pickOrderCodeMatch = !query.pickOrderCode ||
  2021. lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  2022. const itemCodeMatch = !query.itemCode ||
  2023. lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  2024. const itemNameMatch = !query.itemName ||
  2025. lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  2026. const lotNoMatch = !query.lotNo ||
  2027. lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
  2028. return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
  2029. });
  2030. setCombinedLotData(filtered);
  2031. console.log("Filtered lots count:", filtered.length);
  2032. }, [originalCombinedData]);
  2033. const handleReset = useCallback(() => {
  2034. setSearchQuery({});
  2035. if (originalCombinedData) {
  2036. setCombinedLotData(originalCombinedData);
  2037. }
  2038. }, [originalCombinedData]);
  2039. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  2040. setPaginationController(prev => ({
  2041. ...prev,
  2042. pageNum: newPage,
  2043. }));
  2044. }, []);
  2045. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  2046. const newPageSize = parseInt(event.target.value, 10);
  2047. setPaginationController({
  2048. pageNum: 0,
  2049. pageSize: newPageSize === -1 ? -1 : newPageSize,
  2050. });
  2051. }, []);
  2052. // Pagination data with sorting by routerIndex
  2053. // Remove the sorting logic and just do pagination
  2054. // ✅ Memoize paginatedData to prevent re-renders when modal opens
  2055. const paginatedData = useMemo(() => {
  2056. if (paginationController.pageSize === -1) {
  2057. return combinedLotData; // Show all items
  2058. }
  2059. const startIndex = paginationController.pageNum * paginationController.pageSize;
  2060. const endIndex = startIndex + paginationController.pageSize;
  2061. return combinedLotData.slice(startIndex, endIndex); // No sorting needed
  2062. }, [combinedLotData, paginationController.pageNum, paginationController.pageSize]);
  2063. const allItemsReady = useMemo(() => {
  2064. if (combinedLotData.length === 0) return false;
  2065. return combinedLotData.every((lot: any) => {
  2066. const status = lot.stockOutLineStatus?.toLowerCase();
  2067. const isRejected =
  2068. status === 'rejected' || lot.lotAvailability === 'rejected';
  2069. const isCompleted =
  2070. status === 'completed' || status === 'partially_completed' || status === 'partially_complete';
  2071. const isChecked = status === 'checked';
  2072. const isPending = status === 'pending';
  2073. // ✅ FIXED: 无库存(noLot)行:pending 状态也应该被视为 ready(可以提交)
  2074. if (lot.noLot === true) {
  2075. return isChecked || isCompleted || isRejected || isPending;
  2076. }
  2077. // 正常 lot:必须已扫描/提交或者被拒收
  2078. return isChecked || isCompleted || isRejected;
  2079. });
  2080. }, [combinedLotData]);
  2081. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  2082. if (!lot.stockOutLineId) {
  2083. console.error("No stock out line found for this lot");
  2084. return;
  2085. }
  2086. try {
  2087. // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0
  2088. if (submitQty === 0) {
  2089. console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
  2090. console.log(`Lot: ${lot.lotNo}`);
  2091. console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
  2092. console.log(`Setting status to 'completed' with qty: 0`);
  2093. const updateResult = await updateStockOutLineStatus({
  2094. id: lot.stockOutLineId,
  2095. status: 'completed',
  2096. qty: 0
  2097. });
  2098. console.log('Update result:', updateResult);
  2099. const r: any = updateResult as any;
  2100. const updateOk =
  2101. r?.code === 'SUCCESS' ||
  2102. r?.type === 'completed' ||
  2103. typeof r?.id === 'number' ||
  2104. typeof r?.entity?.id === 'number' ||
  2105. (r?.message && r.message.includes('successfully'));
  2106. if (!updateResult || !updateOk) {
  2107. console.error('Failed to update stock out line status:', updateResult);
  2108. throw new Error('Failed to update stock out line status');
  2109. }
  2110. // Check if pick order is completed
  2111. if (lot.pickOrderConsoCode) {
  2112. console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  2113. try {
  2114. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  2115. console.log(` Pick order completion check result:`, completionResponse);
  2116. if (completionResponse.code === "SUCCESS") {
  2117. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  2118. } else if (completionResponse.message === "not completed") {
  2119. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  2120. } else {
  2121. console.error(` Error checking completion: ${completionResponse.message}`);
  2122. }
  2123. } catch (error) {
  2124. console.error("Error checking pick order completion:", error);
  2125. }
  2126. }
  2127. await fetchAllCombinedLotData();
  2128. console.log("All zeros submission completed successfully!");
  2129. setTimeout(() => {
  2130. checkAndAutoAssignNext();
  2131. }, 1000);
  2132. return;
  2133. }
  2134. // FIXED: Calculate cumulative quantity correctly
  2135. const currentActualPickQty = lot.actualPickQty || 0;
  2136. const cumulativeQty = currentActualPickQty + submitQty;
  2137. // FIXED: Determine status based on cumulative quantity vs required quantity
  2138. let newStatus = 'partially_completed';
  2139. if (cumulativeQty >= lot.requiredQty) {
  2140. newStatus = 'completed';
  2141. } else if (cumulativeQty > 0) {
  2142. newStatus = 'partially_completed';
  2143. } else {
  2144. newStatus = 'checked'; // QR scanned but no quantity submitted yet
  2145. }
  2146. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  2147. console.log(`Lot: ${lot.lotNo}`);
  2148. console.log(`Required Qty: ${lot.requiredQty}`);
  2149. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  2150. console.log(`New Submitted Qty: ${submitQty}`);
  2151. console.log(`Cumulative Qty: ${cumulativeQty}`);
  2152. console.log(`New Status: ${newStatus}`);
  2153. console.log(`=====================================`);
  2154. await updateStockOutLineStatus({
  2155. id: lot.stockOutLineId,
  2156. status: newStatus,
  2157. qty: cumulativeQty // Use cumulative quantity
  2158. });
  2159. if (submitQty > 0) {
  2160. await updateInventoryLotLineQuantities({
  2161. inventoryLotLineId: lot.lotId,
  2162. qty: submitQty,
  2163. status: 'available',
  2164. operation: 'pick'
  2165. });
  2166. }
  2167. // Check if pick order is completed when lot status becomes 'completed'
  2168. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  2169. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  2170. try {
  2171. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  2172. console.log(` Pick order completion check result:`, completionResponse);
  2173. if (completionResponse.code === "SUCCESS") {
  2174. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  2175. } else if (completionResponse.message === "not completed") {
  2176. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  2177. } else {
  2178. console.error(` Error checking completion: ${completionResponse.message}`);
  2179. }
  2180. } catch (error) {
  2181. console.error("Error checking pick order completion:", error);
  2182. }
  2183. }
  2184. await fetchAllCombinedLotData();
  2185. console.log("Pick quantity submitted successfully!");
  2186. setTimeout(() => {
  2187. checkAndAutoAssignNext();
  2188. }, 1000);
  2189. } catch (error) {
  2190. console.error("Error submitting pick quantity:", error);
  2191. }
  2192. }, [fetchAllCombinedLotData, checkAndAutoAssignNext]);
  2193. const handleSkip = useCallback(async (lot: any) => {
  2194. try {
  2195. console.log("Skip clicked, submit lot required qty for lot:", lot.lotNo);
  2196. await handleSubmitPickQtyWithQty(lot, lot.requiredQty);
  2197. } catch (err) {
  2198. console.error("Error in Skip:", err);
  2199. }
  2200. }, [handleSubmitPickQtyWithQty]);
  2201. const handleStartScan = useCallback(() => {
  2202. const startTime = performance.now();
  2203. console.log(`⏱️ [START SCAN] Called at: ${new Date().toISOString()}`);
  2204. console.log(`⏱️ [START SCAN] Starting manual QR scan...`);
  2205. setIsManualScanning(true);
  2206. const setManualScanningTime = performance.now() - startTime;
  2207. console.log(`⏱️ [START SCAN] setManualScanning time: ${setManualScanningTime.toFixed(2)}ms`);
  2208. setProcessedQrCodes(new Set());
  2209. setLastProcessedQr('');
  2210. setQrScanError(false);
  2211. setQrScanSuccess(false);
  2212. const beforeStartScanTime = performance.now();
  2213. startScan();
  2214. const startScanTime = performance.now() - beforeStartScanTime;
  2215. console.log(`⏱️ [START SCAN] startScan() call time: ${startScanTime.toFixed(2)}ms`);
  2216. const totalTime = performance.now() - startTime;
  2217. console.log(`⏱️ [START SCAN] Total start scan time: ${totalTime.toFixed(2)}ms`);
  2218. console.log(`⏰ [START SCAN] Start scan completed at: ${new Date().toISOString()}`);
  2219. }, [startScan]);
  2220. const handlePickOrderSwitch = useCallback(async (pickOrderId: number) => {
  2221. if (pickOrderSwitching) return;
  2222. setPickOrderSwitching(true);
  2223. try {
  2224. console.log(" Switching to pick order:", pickOrderId);
  2225. setSelectedPickOrderId(pickOrderId);
  2226. // 强制刷新数据,确保显示正确的 pick order 数据
  2227. await fetchAllCombinedLotData(currentUserId, pickOrderId);
  2228. } catch (error) {
  2229. console.error("Error switching pick order:", error);
  2230. } finally {
  2231. setPickOrderSwitching(false);
  2232. }
  2233. }, [pickOrderSwitching, currentUserId, fetchAllCombinedLotData]);
  2234. const handleStopScan = useCallback(() => {
  2235. console.log("⏸️ Pausing QR scanner...");
  2236. setIsManualScanning(false);
  2237. setQrScanError(false);
  2238. setQrScanSuccess(false);
  2239. stopScan();
  2240. resetScan();
  2241. }, [stopScan, resetScan]);
  2242. // ... existing code around line 1469 ...
  2243. const handlelotnull = useCallback(async (lot: any) => {
  2244. // 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId
  2245. const stockOutLineId = lot.stockOutLineId;
  2246. if (!stockOutLineId) {
  2247. console.error(" No stockOutLineId found for lot:", lot);
  2248. return;
  2249. }
  2250. try {
  2251. // Step 1: Update stock out line status
  2252. await updateStockOutLineStatus({
  2253. id: stockOutLineId,
  2254. status: 'completed',
  2255. qty: 0
  2256. });
  2257. // Step 2: Create pick execution issue for no-lot case
  2258. // Get pick order ID from fgPickOrders or use 0 if not available
  2259. const pickOrderId = lot.pickOrderId || fgPickOrders[0]?.pickOrderId || 0;
  2260. const pickOrderCode = lot.pickOrderCode || fgPickOrders[0]?.pickOrderCode || lot.pickOrderConsoCode || '';
  2261. const issueData: PickExecutionIssueData = {
  2262. type: "Do", // Delivery Order type
  2263. pickOrderId: pickOrderId,
  2264. pickOrderCode: pickOrderCode,
  2265. pickOrderCreateDate: dayjs().format('YYYY-MM-DD'), // Use dayjs format
  2266. pickExecutionDate: dayjs().format('YYYY-MM-DD'),
  2267. pickOrderLineId: lot.pickOrderLineId,
  2268. itemId: lot.itemId,
  2269. itemCode: lot.itemCode || '',
  2270. itemDescription: lot.itemName || '',
  2271. lotId: null, // No lot available
  2272. lotNo: null, // No lot number
  2273. storeLocation: lot.location || '',
  2274. requiredQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0,
  2275. actualPickQty: 0, // No items picked (no lot available)
  2276. missQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, // All quantity is missing
  2277. badItemQty: 0,
  2278. issueRemark: `No lot available for this item. Handled via handlelotnull.`,
  2279. pickerName: session?.user?.name || '',
  2280. };
  2281. const result = await recordPickExecutionIssue(issueData);
  2282. console.log(" Pick execution issue created for no-lot item:", result);
  2283. if (result && result.code === "SUCCESS") {
  2284. console.log(" No-lot item handled and issue recorded successfully");
  2285. } else {
  2286. console.error(" Failed to record pick execution issue:", result);
  2287. }
  2288. // Step 3: Refresh data
  2289. await fetchAllCombinedLotData();
  2290. } catch (error) {
  2291. console.error(" Error in handlelotnull:", error);
  2292. }
  2293. }, [fetchAllCombinedLotData, session, currentUserId, fgPickOrders]);
  2294. const handleBatchScan = useCallback(async () => {
  2295. const startTime = performance.now();
  2296. console.log(`⏱️ [BATCH SCAN START]`);
  2297. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  2298. // 获取所有活跃批次(未扫描的)
  2299. const activeLots = combinedLotData.filter(lot => {
  2300. return (
  2301. lot.lotAvailability !== 'rejected' &&
  2302. lot.stockOutLineStatus !== 'rejected' &&
  2303. lot.stockOutLineStatus !== 'completed' &&
  2304. lot.stockOutLineStatus !== 'checked' && // ✅ 只处理未扫描的
  2305. lot.processingStatus !== 'completed' &&
  2306. lot.noLot !== true &&
  2307. lot.lotNo // ✅ 必须有 lotNo
  2308. );
  2309. });
  2310. if (activeLots.length === 0) {
  2311. console.log("No active lots to scan");
  2312. return;
  2313. }
  2314. console.log(`📦 Batch scanning ${activeLots.length} active lots using batch API...`);
  2315. try {
  2316. // ✅ 转换为批量扫描 API 所需的格式
  2317. const lines: BatchScanLineRequest[] = activeLots.map((lot) => ({
  2318. pickOrderLineId: Number(lot.pickOrderLineId),
  2319. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  2320. pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
  2321. lotNo: lot.lotNo || null,
  2322. itemId: Number(lot.itemId),
  2323. itemCode: String(lot.itemCode || ''),
  2324. stockOutLineId: lot.stockOutLineId ? Number(lot.stockOutLineId) : null, // ✅ 新增
  2325. }));
  2326. const request: BatchScanRequest = {
  2327. userId: currentUserId || 0,
  2328. lines: lines
  2329. };
  2330. console.log(`📤 Sending batch scan request with ${lines.length} lines`);
  2331. console.log(`📋 Request data:`, JSON.stringify(request, null, 2));
  2332. const scanStartTime = performance.now();
  2333. // ✅ 使用新的批量扫描 API(一次性处理所有请求)
  2334. const result = await batchScan(request);
  2335. const scanTime = performance.now() - scanStartTime;
  2336. console.log(`⏱️ Batch scan API call completed in ${scanTime.toFixed(2)}ms (${(scanTime / 1000).toFixed(3)}s)`);
  2337. console.log(`📥 Batch scan result:`, result);
  2338. // ✅ 刷新数据以获取最新的状态
  2339. const refreshStartTime = performance.now();
  2340. await fetchAllCombinedLotData();
  2341. const refreshTime = performance.now() - refreshStartTime;
  2342. console.log(`⏱️ Data refresh time: ${refreshTime.toFixed(2)}ms (${(refreshTime / 1000).toFixed(3)}s)`);
  2343. const totalTime = performance.now() - startTime;
  2344. console.log(`⏱️ [BATCH SCAN END]`);
  2345. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  2346. console.log(`⏰ End time: ${new Date().toISOString()}`);
  2347. if (result && result.code === "SUCCESS") {
  2348. setQrScanSuccess(true);
  2349. setQrScanError(false);
  2350. } else {
  2351. console.error("❌ Batch scan failed:", result);
  2352. setQrScanError(true);
  2353. setQrScanSuccess(false);
  2354. }
  2355. } catch (error) {
  2356. console.error("❌ Error in batch scan:", error);
  2357. setQrScanError(true);
  2358. setQrScanSuccess(false);
  2359. }
  2360. }, [combinedLotData, fetchAllCombinedLotData, currentUserId]);
  2361. const handleSubmitAllScanned = useCallback(async () => {
  2362. const startTime = performance.now();
  2363. console.log(`⏱️ [BATCH SUBMIT START]`);
  2364. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  2365. const scannedLots = combinedLotData.filter(lot => {
  2366. // 如果是 noLot 情况,检查状态是否为 pending 或 partially_complete
  2367. if (lot.noLot === true) {
  2368. return lot.stockOutLineStatus === 'checked' ||
  2369. lot.stockOutLineStatus === 'pending' ||
  2370. lot.stockOutLineStatus === 'partially_completed' ||
  2371. lot.stockOutLineStatus === 'PARTIALLY_COMPLETE';
  2372. }
  2373. // 正常情况:只包含 checked 状态
  2374. return lot.stockOutLineStatus === 'checked';
  2375. });
  2376. if (scannedLots.length === 0) {
  2377. console.log("No scanned items to submit");
  2378. return;
  2379. }
  2380. setIsSubmittingAll(true);
  2381. console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
  2382. try {
  2383. // 转换为 batchSubmitList 所需的格式(与后端 QrPickBatchSubmitRequest 匹配)
  2384. const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
  2385. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty || 0;
  2386. const currentActualPickQty = lot.actualPickQty || 0;
  2387. const cumulativeQty = currentActualPickQty + submitQty;
  2388. let newStatus = 'partially_completed';
  2389. if (cumulativeQty >= (lot.requiredQty || 0)) {
  2390. newStatus = 'completed';
  2391. }
  2392. return {
  2393. stockOutLineId: Number(lot.stockOutLineId) || 0,
  2394. pickOrderLineId: Number(lot.pickOrderLineId),
  2395. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  2396. requiredQty: Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0),
  2397. actualPickQty: Number(cumulativeQty),
  2398. stockOutLineStatus: newStatus,
  2399. pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
  2400. noLot: Boolean(lot.noLot === true)
  2401. };
  2402. });
  2403. const request: batchSubmitListRequest = {
  2404. userId: currentUserId || 0,
  2405. lines: lines
  2406. };
  2407. console.log(`📤 Sending batch submit request with ${lines.length} lines`);
  2408. console.log(`📋 Request data:`, JSON.stringify(request, null, 2));
  2409. const submitStartTime = performance.now();
  2410. // 使用 batchSubmitList API
  2411. const result = await batchSubmitList(request);
  2412. const submitTime = performance.now() - submitStartTime;
  2413. console.log(`⏱️ Batch submit API call completed in ${submitTime.toFixed(2)}ms (${(submitTime / 1000).toFixed(3)}s)`);
  2414. console.log(`📥 Batch submit result:`, result);
  2415. // Refresh data once after batch submission
  2416. const refreshStartTime = performance.now();
  2417. await fetchAllCombinedLotData();
  2418. const refreshTime = performance.now() - refreshStartTime;
  2419. console.log(`⏱️ Data refresh time: ${refreshTime.toFixed(2)}ms (${(refreshTime / 1000).toFixed(3)}s)`);
  2420. const totalTime = performance.now() - startTime;
  2421. console.log(`⏱️ [BATCH SUBMIT END]`);
  2422. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  2423. console.log(`⏰ End time: ${new Date().toISOString()}`);
  2424. if (result && result.code === "SUCCESS") {
  2425. setQrScanSuccess(true);
  2426. setTimeout(() => {
  2427. setQrScanSuccess(false);
  2428. checkAndAutoAssignNext();
  2429. if (onSwitchToRecordTab) {
  2430. onSwitchToRecordTab();
  2431. }
  2432. if (onRefreshReleasedOrderCount) {
  2433. onRefreshReleasedOrderCount();
  2434. }
  2435. }, 2000);
  2436. } else {
  2437. console.error("Batch submit failed:", result);
  2438. setQrScanError(true);
  2439. }
  2440. } catch (error) {
  2441. console.error("Error submitting all scanned items:", error);
  2442. setQrScanError(true);
  2443. } finally {
  2444. setIsSubmittingAll(false);
  2445. }
  2446. }, [combinedLotData, fetchAllCombinedLotData, checkAndAutoAssignNext, currentUserId, onSwitchToRecordTab, onRefreshReleasedOrderCount]);
  2447. // Calculate scanned items count
  2448. // Calculate scanned items count (should match handleSubmitAllScanned filter logic)
  2449. const scannedItemsCount = useMemo(() => {
  2450. const filtered = combinedLotData.filter(lot => {
  2451. // ✅ FIXED: 使用与 handleSubmitAllScanned 相同的过滤逻辑
  2452. if (lot.noLot === true) {
  2453. // ✅ 只包含可以提交的状态(与 handleSubmitAllScanned 保持一致)
  2454. return lot.stockOutLineStatus === 'checked' ||
  2455. lot.stockOutLineStatus === 'pending' ||
  2456. lot.stockOutLineStatus === 'partially_completed' ||
  2457. lot.stockOutLineStatus === 'PARTIALLY_COMPLETE';
  2458. }
  2459. // 正常情况:只包含 checked 状态
  2460. return lot.stockOutLineStatus === 'checked';
  2461. });
  2462. // 添加调试日志
  2463. const noLotCount = filtered.filter(l => l.noLot === true).length;
  2464. const normalCount = filtered.filter(l => l.noLot !== true).length;
  2465. console.log(`📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`);
  2466. console.log(`📊 All items breakdown:`, {
  2467. total: combinedLotData.length,
  2468. noLot: combinedLotData.filter(l => l.noLot === true).length,
  2469. normal: combinedLotData.filter(l => l.noLot !== true).length
  2470. });
  2471. return filtered.length;
  2472. }, [combinedLotData]);
  2473. // ADD THIS: Auto-stop scan when no data available
  2474. useEffect(() => {
  2475. if (isManualScanning && combinedLotData.length === 0) {
  2476. console.log("⏹️ No data available, auto-stopping QR scan...");
  2477. handleStopScan();
  2478. }
  2479. }, [combinedLotData.length, isManualScanning, handleStopScan]);
  2480. // Cleanup effect
  2481. useEffect(() => {
  2482. return () => {
  2483. // Cleanup when component unmounts (e.g., when switching tabs)
  2484. if (isManualScanning) {
  2485. console.log("🧹 Pick execution component unmounting, stopping QR scanner...");
  2486. stopScan();
  2487. resetScan();
  2488. }
  2489. };
  2490. }, [isManualScanning, stopScan, resetScan]);
  2491. const getStatusMessage = useCallback((lot: any) => {
  2492. switch (lot.stockOutLineStatus?.toLowerCase()) {
  2493. case 'pending':
  2494. return t("Please finish QR code scan and pick order.");
  2495. case 'checked':
  2496. return t("Please submit the pick order.");
  2497. case 'partially_completed':
  2498. return t("Partial quantity submitted. Please submit more or complete the order.");
  2499. case 'completed':
  2500. return t("Pick order completed successfully!");
  2501. case 'rejected':
  2502. return t("Lot has been rejected and marked as unavailable.");
  2503. case 'unavailable':
  2504. return t("This order is insufficient, please pick another lot.");
  2505. default:
  2506. return t("Please finish QR code scan and pick order.");
  2507. }
  2508. }, [t]);
  2509. return (
  2510. <TestQrCodeProvider
  2511. lotData={combinedLotData}
  2512. onScanLot={handleQrCodeSubmit}
  2513. onBatchScan={handleBatchScan}
  2514. filterActive={(lot) => (
  2515. lot.lotAvailability !== 'rejected' &&
  2516. lot.stockOutLineStatus !== 'rejected' &&
  2517. lot.stockOutLineStatus !== 'completed'
  2518. )}
  2519. >
  2520. <FormProvider {...formProps}>
  2521. <Stack spacing={2}>
  2522. <Box
  2523. sx={{
  2524. position: 'fixed',
  2525. top: 0,
  2526. left: 0,
  2527. right: 0,
  2528. zIndex: 1100, // Higher than other elements
  2529. backgroundColor: 'background.paper',
  2530. pt: 2,
  2531. pb: 1,
  2532. px: 2,
  2533. borderBottom: '1px solid',
  2534. borderColor: 'divider',
  2535. boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
  2536. }}
  2537. >
  2538. <LinearProgressWithLabel
  2539. completed={progress.completed}
  2540. total={progress.total}
  2541. label={t("Progress")}
  2542. />
  2543. <ScanStatusAlert
  2544. error={qrScanError}
  2545. success={qrScanSuccess}
  2546. errorMessage={t("QR code does not match any item in current orders.")}
  2547. successMessage={t("QR code verified.")}
  2548. />
  2549. </Box>
  2550. {/* DO Header */}
  2551. {/* 保留:Combined Lot Table - 包含所有 QR 扫描功能 */}
  2552. <Box>
  2553. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, mt: 10 }}>
  2554. <Typography variant="h6" gutterBottom sx={{ mb: 0 }}>
  2555. {t("All Pick Order Lots")}
  2556. </Typography>
  2557. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  2558. {/* Scanner status indicator (always visible) */}
  2559. {/*
  2560. <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
  2561. <QrCodeIcon
  2562. sx={{
  2563. color: isManualScanning ? '#4caf50' : '#9e9e9e',
  2564. animation: isManualScanning ? 'pulse 2s infinite' : 'none',
  2565. '@keyframes pulse': {
  2566. '0%, 100%': { opacity: 1 },
  2567. '50%': { opacity: 0.5 }
  2568. }
  2569. }}
  2570. />
  2571. <Typography variant="body2" sx={{ color: isManualScanning ? '#4caf50' : '#9e9e9e' }}>
  2572. {isManualScanning ? t("Scanner Active") : t("Scanner Inactive")}
  2573. </Typography>
  2574. </Box>
  2575. */}
  2576. {/* Pause/Resume button instead of Start/Stop */}
  2577. {isManualScanning ? (
  2578. <Button
  2579. variant="outlined"
  2580. startIcon={<QrCodeIcon />}
  2581. onClick={handleStopScan}
  2582. color="secondary"
  2583. sx={{ minWidth: '120px' }}
  2584. >
  2585. {t("Stop QR Scan")}
  2586. </Button>
  2587. ) : (
  2588. <Button
  2589. variant="contained"
  2590. startIcon={<QrCodeIcon />}
  2591. onClick={handleStartScan}
  2592. color="primary"
  2593. sx={{ minWidth: '120px' }}
  2594. >
  2595. {t("Start QR Scan")}
  2596. </Button>
  2597. )}
  2598. {/* 保留:Submit All Scanned Button */}
  2599. <Button
  2600. variant="contained"
  2601. color="success"
  2602. onClick={handleSubmitAllScanned}
  2603. disabled={
  2604. // scannedItemsCount === 0
  2605. !allItemsReady
  2606. || isSubmittingAll}
  2607. sx={{ minWidth: '160px' }}
  2608. >
  2609. {isSubmittingAll ? (
  2610. <>
  2611. <CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
  2612. {t("Submitting...")}
  2613. </>
  2614. ) : (
  2615. `${t("Submit All Scanned")} (${scannedItemsCount})`
  2616. )}
  2617. </Button>
  2618. </Box>
  2619. </Box>
  2620. {fgPickOrders.length > 0 && (
  2621. <Paper sx={{ p: 2, mb: 2 }}>
  2622. <Stack spacing={2}>
  2623. {/* 基本信息 */}
  2624. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  2625. <Typography variant="subtitle1">
  2626. <strong>{t("Shop Name")}:</strong> {fgPickOrders[0].shopName || '-'}
  2627. </Typography>
  2628. <Typography variant="subtitle1">
  2629. <strong>{t("Store ID")}:</strong> {fgPickOrders[0].storeId || '-'}
  2630. </Typography>
  2631. <Typography variant="subtitle1">
  2632. <strong>{t("Ticket No.")}:</strong> {fgPickOrders[0].ticketNo || '-'}
  2633. </Typography>
  2634. <Typography variant="subtitle1">
  2635. <strong>{t("Departure Time")}:</strong> {fgPickOrders[0].DepartureTime || '-'}
  2636. </Typography>
  2637. </Stack>
  2638. {/* 改进:三个字段显示在一起,使用表格式布局 */}
  2639. {/* 改进:三个字段合并显示 */}
  2640. {/* 改进:表格式显示每个 pick order */}
  2641. <Box sx={{
  2642. p: 2,
  2643. backgroundColor: '#f5f5f5',
  2644. borderRadius: 1
  2645. }}>
  2646. <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
  2647. {t("Pick Orders Details")}:
  2648. </Typography>
  2649. {(() => {
  2650. const pickOrderCodes = fgPickOrders[0].pickOrderCodes as string[] | string | undefined;
  2651. const deliveryNos = fgPickOrders[0].deliveryNos as string[] | string | undefined;
  2652. const lineCounts = fgPickOrders[0].lineCountsPerPickOrder;
  2653. const pickOrderCodesArray = Array.isArray(pickOrderCodes)
  2654. ? pickOrderCodes
  2655. : (typeof pickOrderCodes === 'string' ? pickOrderCodes.split(', ') : []);
  2656. const deliveryNosArray = Array.isArray(deliveryNos)
  2657. ? deliveryNos
  2658. : (typeof deliveryNos === 'string' ? deliveryNos.split(', ') : []);
  2659. const lineCountsArray = Array.isArray(lineCounts) ? lineCounts : [];
  2660. const maxLength = Math.max(
  2661. pickOrderCodesArray.length,
  2662. deliveryNosArray.length,
  2663. lineCountsArray.length
  2664. );
  2665. if (maxLength === 0) {
  2666. return <Typography variant="body2" color="text.secondary">-</Typography>;
  2667. }
  2668. // 使用与外部基本信息相同的样式
  2669. return Array.from({ length: maxLength }, (_, idx) => (
  2670. <Stack
  2671. key={idx}
  2672. direction="row"
  2673. spacing={4}
  2674. useFlexGap
  2675. flexWrap="wrap"
  2676. sx={{ mb: idx < maxLength - 1 ? 1 : 0 }} // 除了最后一行,都添加底部间距
  2677. >
  2678. <Typography variant="subtitle1">
  2679. <strong>{t("Delivery Order")}:</strong> {deliveryNosArray[idx] || '-'}
  2680. </Typography>
  2681. <Typography variant="subtitle1">
  2682. <strong>{t("Pick Order")}:</strong> {pickOrderCodesArray[idx] || '-'}
  2683. </Typography>
  2684. <Typography variant="subtitle1">
  2685. <strong>{t("Finsihed good items")}:</strong> {lineCountsArray[idx] || '-'}<strong>{t("kinds")}</strong>
  2686. </Typography>
  2687. </Stack>
  2688. ));
  2689. })()}
  2690. </Box>
  2691. </Stack>
  2692. </Paper>
  2693. )}
  2694. <TableContainer component={Paper}>
  2695. <Table>
  2696. <TableHead>
  2697. <TableRow>
  2698. <TableCell>{t("Index")}</TableCell>
  2699. <TableCell>{t("Route")}</TableCell>
  2700. <TableCell>{t("Item Code")}</TableCell>
  2701. <TableCell>{t("Item Name")}</TableCell>
  2702. <TableCell>{t("Lot#")}</TableCell>
  2703. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  2704. <TableCell align="center">{t("Scan Result")}</TableCell>
  2705. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  2706. </TableRow>
  2707. </TableHead>
  2708. <TableBody>
  2709. {paginatedData.length === 0 ? (
  2710. <TableRow>
  2711. <TableCell colSpan={11} align="center">
  2712. <Typography variant="body2" color="text.secondary">
  2713. {t("No data available")}
  2714. </Typography>
  2715. </TableCell>
  2716. </TableRow>
  2717. ) : (
  2718. // 在第 1797-1938 行之间,将整个 map 函数修改为:
  2719. paginatedData.map((lot, index) => {
  2720. // 检查是否是 issue lot
  2721. const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo;
  2722. return (
  2723. <TableRow
  2724. key={`${lot.pickOrderLineId}-${lot.lotId || 'null'}`}
  2725. sx={{
  2726. //backgroundColor: isIssueLot ? '#fff3e0' : 'inherit',
  2727. // opacity: isIssueLot ? 0.6 : 1,
  2728. '& .MuiTableCell-root': {
  2729. //color: isIssueLot ? 'warning.main' : 'inherit'
  2730. }
  2731. }}
  2732. >
  2733. <TableCell>
  2734. <Typography variant="body2" fontWeight="bold">
  2735. {paginationController.pageNum * paginationController.pageSize + index + 1}
  2736. </Typography>
  2737. </TableCell>
  2738. <TableCell>
  2739. <Typography variant="body2">
  2740. {lot.routerRoute || '-'}
  2741. </Typography>
  2742. </TableCell>
  2743. <TableCell>{lot.itemCode}</TableCell>
  2744. <TableCell>{lot.itemName + '(' + lot.stockUnit + ')'}</TableCell>
  2745. <TableCell>
  2746. <Box>
  2747. <Typography
  2748. sx={{
  2749. // color: isIssueLot ? 'warning.main' : lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  2750. }}
  2751. >
  2752. {lot.lotNo ||
  2753. t('No Stock Available')}
  2754. </Typography>
  2755. </Box>
  2756. </TableCell>
  2757. <TableCell align="right">
  2758. {(() => {
  2759. const requiredQty = lot.requiredQty || 0;
  2760. return requiredQty.toLocaleString() + '(' + lot.uomShortDesc + ')';
  2761. })()}
  2762. </TableCell>
  2763. <TableCell align="center">
  2764. {(() => {
  2765. const status = lot.stockOutLineStatus?.toLowerCase();
  2766. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2767. const isNoLot = !lot.lotNo;
  2768. // rejected lot:显示红色勾选(已扫描但被拒绝)
  2769. if (isRejected && !isNoLot) {
  2770. return (
  2771. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2772. <Checkbox
  2773. checked={true}
  2774. disabled={true}
  2775. readOnly={true}
  2776. size="large"
  2777. sx={{
  2778. color: 'error.main',
  2779. '&.Mui-checked': { color: 'error.main' },
  2780. transform: 'scale(1.3)',
  2781. }}
  2782. />
  2783. </Box>
  2784. );
  2785. }
  2786. // 正常 lot:已扫描(checked/partially_completed/completed)
  2787. if (!isNoLot && status !== 'pending' && status !== 'rejected') {
  2788. return (
  2789. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2790. <Checkbox
  2791. checked={true}
  2792. disabled={true}
  2793. readOnly={true}
  2794. size="large"
  2795. sx={{
  2796. color: 'success.main',
  2797. '&.Mui-checked': { color: 'success.main' },
  2798. transform: 'scale(1.3)',
  2799. }}
  2800. />
  2801. </Box>
  2802. );
  2803. }
  2804. // noLot 且已完成/部分完成:显示红色勾选
  2805. if (isNoLot && (status === 'partially_completed' || status === 'completed')) {
  2806. return (
  2807. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2808. <Checkbox
  2809. checked={true}
  2810. disabled={true}
  2811. readOnly={true}
  2812. size="large"
  2813. sx={{
  2814. color: 'error.main',
  2815. '&.Mui-checked': { color: 'error.main' },
  2816. transform: 'scale(1.3)',
  2817. }}
  2818. />
  2819. </Box>
  2820. );
  2821. }
  2822. return null;
  2823. })()}
  2824. </TableCell>
  2825. <TableCell align="center">
  2826. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  2827. {(() => {
  2828. const status = lot.stockOutLineStatus?.toLowerCase();
  2829. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2830. const isNoLot = !lot.lotNo;
  2831. // rejected lot:不显示任何按钮
  2832. if (isRejected && !isNoLot) {
  2833. return null;
  2834. }
  2835. // noLot 情况:只显示 Issue 按钮
  2836. if (isNoLot) {
  2837. return (
  2838. <Button
  2839. variant="outlined"
  2840. size="small"
  2841. onClick={() => handlelotnull(lot)}
  2842. disabled={status === 'completed'}
  2843. sx={{
  2844. fontSize: '0.7rem',
  2845. py: 0.5,
  2846. minHeight: '28px',
  2847. minWidth: '60px',
  2848. borderColor: 'warning.main',
  2849. color: 'warning.main'
  2850. }}
  2851. >
  2852. {t("Issue")}
  2853. </Button>
  2854. );
  2855. }
  2856. // 正常 lot:显示 Submit 和 Issue 按钮
  2857. return (
  2858. <Stack direction="row" spacing={1} alignItems="center">
  2859. <Button
  2860. variant="contained"
  2861. onClick={() => {
  2862. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  2863. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  2864. handlePickQtyChange(lotKey, submitQty);
  2865. handleSubmitPickQtyWithQty(lot, submitQty);
  2866. }}
  2867. disabled={
  2868. lot.lotAvailability === 'expired' ||
  2869. lot.lotAvailability === 'status_unavailable' ||
  2870. lot.lotAvailability === 'rejected' ||
  2871. lot.stockOutLineStatus === 'completed' ||
  2872. lot.stockOutLineStatus === 'pending'
  2873. }
  2874. sx={{ fontSize: '0.75rem', py: 0.5, minHeight: '28px', minWidth: '70px' }}
  2875. >
  2876. {t("Submit")}
  2877. </Button>
  2878. <Button
  2879. variant="outlined"
  2880. size="small"
  2881. onClick={() => handlePickExecutionForm(lot)}
  2882. disabled={
  2883. lot.lotAvailability === 'expired' ||
  2884. //lot.lotAvailability === 'status_unavailable' ||
  2885. // lot.lotAvailability === 'rejected' ||
  2886. lot.stockOutLineStatus === 'completed'
  2887. //lot.stockOutLineStatus === 'pending'
  2888. }
  2889. sx={{
  2890. fontSize: '0.7rem',
  2891. py: 0.5,
  2892. minHeight: '28px',
  2893. minWidth: '60px',
  2894. borderColor: 'warning.main',
  2895. color: 'warning.main'
  2896. }}
  2897. title="Report missing or bad items"
  2898. >
  2899. {t("Edit")}
  2900. </Button>
  2901. <Button
  2902. variant="outlined"
  2903. size="small"
  2904. onClick={() => handleSkip(lot)}
  2905. disabled={lot.stockOutLineStatus === 'completed'}
  2906. sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }}
  2907. >
  2908. {t("Just Completed")}
  2909. </Button>
  2910. </Stack>
  2911. );
  2912. })()}
  2913. </Box>
  2914. </TableCell>
  2915. </TableRow>
  2916. );
  2917. })
  2918. )}
  2919. </TableBody>
  2920. </Table>
  2921. </TableContainer>
  2922. <TablePagination
  2923. component="div"
  2924. count={combinedLotData.length}
  2925. page={paginationController.pageNum}
  2926. rowsPerPage={paginationController.pageSize}
  2927. onPageChange={handlePageChange}
  2928. onRowsPerPageChange={handlePageSizeChange}
  2929. rowsPerPageOptions={[10, 25, 50,-1]}
  2930. labelRowsPerPage={t("Rows per page")}
  2931. labelDisplayedRows={({ from, to, count }) =>
  2932. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  2933. }
  2934. />
  2935. </Box>
  2936. </Stack>
  2937. {/* QR Code Scanner works in background - no modal needed */}
  2938. <ManualLotConfirmationModal
  2939. open={manualLotConfirmationOpen}
  2940. onClose={() => {
  2941. setManualLotConfirmationOpen(false);
  2942. }}
  2943. onConfirm={handleManualLotConfirmation}
  2944. expectedLot={expectedLotData}
  2945. scannedLot={scannedLotData}
  2946. isLoading={isConfirmingLot}
  2947. />
  2948. {/* 保留:Lot Confirmation Modal */}
  2949. {lotConfirmationOpen && expectedLotData && scannedLotData && (
  2950. <LotConfirmationModal
  2951. open={lotConfirmationOpen}
  2952. onClose={() => {
  2953. console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`);
  2954. setLotConfirmationOpen(false);
  2955. setExpectedLotData(null);
  2956. setScannedLotData(null);
  2957. setSelectedLotForQr(null);
  2958. // ✅ IMPORTANT: Clear refs to allow reprocessing the same QR code if user cancels and scans again
  2959. // This allows the modal to reopen for the same itemId with a different stockInLineId
  2960. setTimeout(() => {
  2961. lastProcessedQrRef.current = '';
  2962. processedQrCodesRef.current.clear();
  2963. console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs to allow reprocessing`);
  2964. }, 100);
  2965. // ✅ Don't clear processedQrCombinations - it tracks by itemId+stockInLineId,
  2966. // so reopening for same itemId but different stockInLineId is allowed
  2967. }}
  2968. onConfirm={handleLotConfirmation}
  2969. expectedLot={expectedLotData}
  2970. scannedLot={scannedLotData}
  2971. isLoading={isConfirmingLot}
  2972. />
  2973. )}
  2974. {/* 保留:Good Pick Execution Form Modal */}
  2975. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  2976. <GoodPickExecutionForm
  2977. open={pickExecutionFormOpen}
  2978. onClose={() => {
  2979. setPickExecutionFormOpen(false);
  2980. setSelectedLotForExecutionForm(null);
  2981. }}
  2982. onSubmit={handlePickExecutionFormSubmit}
  2983. selectedLot={selectedLotForExecutionForm}
  2984. selectedPickOrderLine={{
  2985. id: selectedLotForExecutionForm.pickOrderLineId,
  2986. itemId: selectedLotForExecutionForm.itemId,
  2987. itemCode: selectedLotForExecutionForm.itemCode,
  2988. itemName: selectedLotForExecutionForm.itemName,
  2989. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  2990. availableQty: selectedLotForExecutionForm.availableQty || 0,
  2991. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  2992. // uomCode: selectedLotForExecutionForm.uomCode || '',
  2993. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  2994. pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
  2995. uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
  2996. suggestedList: [],
  2997. noLotLines: [],
  2998. }}
  2999. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  3000. pickOrderCreateDate={new Date()}
  3001. />
  3002. )}
  3003. </FormProvider>
  3004. </TestQrCodeProvider>
  3005. );
  3006. };
  3007. export default PickExecution;