index(2).html 630 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675106761067710678106791068010681106821068310684106851068610687106881068910690106911069210693106941069510696106971069810699107001070110702107031070410705107061070710708107091071010711107121071310714107151071610717107181071910720107211072210723107241072510726107271072810729107301073110732107331073410735107361073710738107391074010741107421074310744107451074610747107481074910750107511075210753107541075510756107571075810759107601076110762107631076410765107661076710768107691077010771107721077310774107751077610777107781077910780107811078210783107841078510786107871078810789107901079110792107931079410795107961079710798107991080010801108021080310804108051080610807108081080910810108111081210813108141081510816108171081810819108201082110822108231082410825108261082710828108291083010831108321083310834108351083610837108381083910840108411084210843108441084510846108471084810849108501085110852108531085410855108561085710858108591086010861108621086310864108651086610867108681086910870108711087210873108741087510876108771087810879108801088110882108831088410885108861088710888108891089010891108921089310894108951089610897108981089910900109011090210903109041090510906109071090810909109101091110912109131091410915109161091710918109191092010921109221092310924109251092610927109281092910930109311093210933109341093510936109371093810939109401094110942109431094410945109461094710948109491095010951109521095310954109551095610957109581095910960109611096210963109641096510966109671096810969109701097110972109731097410975109761097710978109791098010981109821098310984109851098610987109881098910990109911099210993109941099510996109971099810999110001100111002110031100411005110061100711008110091101011011110121101311014110151101611017
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <title>山峡代工一体化平台</title>
  7. <link rel="preconnect" href="https://fonts.googleapis.com" />
  8. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  9. <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet" />
  10. <style>
  11. :root {
  12. --green: #167c4a;
  13. --green-dark: #0f5e38;
  14. --green-soft: #e8f6ee;
  15. --green-line: #b7dfc8;
  16. --blue: #2a6bb3;
  17. --blue-soft: #e8f1fb;
  18. --blue-line: #b7cce8;
  19. --ink: #1c2430;
  20. --muted: #5b6675;
  21. --line: #e6ebe7;
  22. --paper: #f4f7f5;
  23. --white: #ffffff;
  24. --warn: #d94841;
  25. --warn-soft: #fdecea;
  26. --amber: #c47d11;
  27. --amber-soft: #fff6e5;
  28. --sidebar: 232px;
  29. --header: 56px;
  30. --radius: 12px;
  31. --shadow: 0 1px 2px rgba(28, 36, 48, 0.04), 0 8px 24px rgba(28, 36, 48, 0.04);
  32. }
  33. * { box-sizing: border-box; margin: 0; padding: 0; }
  34. html, body { height: 100%; }
  35. body {
  36. font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", "Microsoft YaHei", sans-serif;
  37. color: var(--ink); background: var(--paper); font-size: 13px; line-height: 1.5;
  38. }
  39. button, input, select, textarea { font-family: inherit; font-size: 13px; }
  40. button { cursor: pointer; border: 0; background: none; }
  41. .app {
  42. display: grid;
  43. grid-template-columns: var(--sidebar) 1fr;
  44. min-height: 100vh;
  45. }
  46. .sidebar {
  47. background: var(--white);
  48. border-right: 1px solid var(--line);
  49. display: flex;
  50. flex-direction: column;
  51. position: sticky;
  52. top: 0;
  53. height: 100vh;
  54. }
  55. .brand {
  56. height: var(--header);
  57. display: flex;
  58. align-items: center;
  59. gap: 10px;
  60. padding: 0 18px;
  61. border-bottom: 1px solid var(--line);
  62. min-width: 0;
  63. }
  64. .logo {
  65. width: 28px; height: 28px; border-radius: 8px; flex: none;
  66. background: linear-gradient(160deg, #22a05d, var(--green-dark));
  67. display: grid; place-items: center; color: #fff; font-weight: 700; font-size: 13px;
  68. }
  69. .brand strong { font-size: 13px; letter-spacing: 0.02em; font-weight: 700; }
  70. .brand .sub { display: block; font-size: 11px; color: var(--muted); font-weight: 400; margin-top: 1px; }
  71. .nav { padding: 12px 10px; overflow: auto; flex: 1; }
  72. .nav-label {
  73. font-size: 11px; color: #8a94a0; letter-spacing: 0.08em;
  74. padding: 10px 12px 6px; text-transform: uppercase;
  75. }
  76. .side-item {
  77. display: flex; align-items: center; gap: 10px;
  78. padding: 9px 12px; margin-bottom: 2px;
  79. border-radius: 8px; color: #3d4754; width: 100%; text-align: left; font-size: 13.5px;
  80. }
  81. .side-item svg { width: 16px; height: 16px; flex: none; stroke: currentColor; fill: none; stroke-width: 1.8; }
  82. .side-item:hover { background: #f3f6f4; }
  83. .side-item.active { background: var(--green-soft); color: var(--green-dark); font-weight: 600; }
  84. .side-dot { display: none; }
  85. .badge-n {
  86. margin-left: auto; min-width: 18px; height: 18px; padding: 0 5px;
  87. border-radius: 9px; background: var(--warn); color: #fff;
  88. font-size: 10px; display: grid; place-items: center; font-weight: 600;
  89. }
  90. .tag {
  91. margin-left: auto; font-size: 10px; color: var(--green); background: var(--green-soft);
  92. border-radius: 4px; padding: 1px 5px; font-weight: 600;
  93. }
  94. .nav-group { margin-bottom: 2px; }
  95. .nav-group > .side-item .chev {
  96. margin-left: auto; width: 14px; height: 14px; opacity: .45;
  97. transition: transform .15s; flex: none;
  98. }
  99. .nav-group.open > .side-item .chev { transform: rotate(90deg); }
  100. .nav-sub { display: none; padding: 0 0 6px; }
  101. .nav-group.open > .nav-sub { display: block; }
  102. .side-sub { padding: 8px 12px 8px 38px; font-size: 13px; margin-bottom: 1px; }
  103. .coming {
  104. background: var(--white); border: 1px solid var(--line); border-radius: var(--radius);
  105. box-shadow: var(--shadow); padding: 56px 24px; text-align: center;
  106. }
  107. .coming h2 { font-size: 16px; margin-bottom: 8px; }
  108. .coming p { color: var(--muted); font-size: 13px; }
  109. .main { min-width: 0; overflow: visible; }
  110. .topbar {
  111. height: var(--header);
  112. background: var(--white);
  113. border-bottom: 1px solid var(--line);
  114. display: flex; align-items: center; gap: 16px;
  115. padding: 0 28px; position: sticky; top: 0; z-index: 5;
  116. }
  117. .crumb { font-size: 13px; color: var(--muted); }
  118. .crumb b { color: var(--ink); font-weight: 600; }
  119. .search {
  120. margin-left: auto; display: flex; align-items: center; gap: 8px;
  121. background: var(--paper); border: 1px solid var(--line);
  122. border-radius: 8px; padding: 6px 10px; width: 260px;
  123. }
  124. .search input, .top-search {
  125. border: 0; outline: 0; background: transparent; width: 100%; font-size: 13px; color: var(--ink);
  126. }
  127. .search input::placeholder, .top-search::placeholder { color: #8a94a0; }
  128. .user {
  129. display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); flex-shrink: 0;
  130. }
  131. .avatar {
  132. width: 28px; height: 28px; border-radius: 50%;
  133. background: #d7eee0; color: var(--green-dark);
  134. display: grid; place-items: center; font-weight: 700; font-size: 12px;
  135. }
  136. .page { padding: 24px 28px 40px; overflow: visible; }
  137. .subnav { display: flex; gap: 4px; background: var(--white); border: 1px solid var(--line); border-radius: 10px; padding: 4px; margin-bottom: 18px; box-shadow: var(--shadow); flex-wrap: wrap; }
  138. #mod-fleet > .subnav { display: none; }
  139. .subnav button { height: 32px; padding: 0 12px; border-radius: 8px; color: #4b5563; font-weight: 500; white-space: nowrap; }
  140. .subnav button.on { background: var(--green-soft); color: var(--green-dark); font-weight: 650; }
  141. .page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; }
  142. .page-head h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
  143. .page-head p { color: var(--muted); margin-top: 6px; font-size: 13px; }
  144. .actions { display: flex; gap: 8px; flex-wrap: wrap; }
  145. .btn { height: 32px; padding: 0 12px; border-radius: 8px; font-weight: 500; border: 1px solid var(--line); background: var(--white); }
  146. .btn:hover { border-color: var(--green-line); }
  147. .btn-primary { background: var(--green); color: #fff; border-color: var(--green); }
  148. .btn-primary:hover { background: var(--green-dark); }
  149. .btn-sm { height: 26px; padding: 0 8px; font-size: 12px; }
  150. .alerts { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
  151. .alert { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 8px; background: var(--amber-soft); color: var(--amber); border: 1px solid #f3ddb0; font-size: 12px; }
  152. .alert.red { background: var(--warn-soft); color: var(--warn); border-color: #f5c5c2; }
  153. .alert.ok { background: var(--green-soft); color: var(--green-dark); border-color: var(--green-line); }
  154. .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 22px; }
  155. .stats.five { grid-template-columns: repeat(5, 1fr); }
  156. .stats.three { grid-template-columns: repeat(3, 1fr); }
  157. .stat { background: var(--white); border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; box-shadow: var(--shadow); }
  158. .stat .k { color: var(--muted); font-size: 12px; }
  159. .stat .v { font-size: 26px; font-weight: 700; margin: 4px 0 2px; letter-spacing: -.03em; }
  160. #taskStats .stat .k {
  161. display: flex;
  162. align-items: baseline;
  163. gap: 10px;
  164. white-space: nowrap;
  165. }
  166. #taskStats .stat .k .stat-name {
  167. font-weight: 650;
  168. font-size: 13px;
  169. }
  170. #taskStats .stat .k .stat-name.wait { color: var(--amber); }
  171. #taskStats .stat .k .stat-name.run { color: var(--blue); }
  172. #taskStats .stat .k .stat-name.done { color: var(--green); }
  173. #taskStats .stat .k .stat-name.all { color: var(--ink); }
  174. #taskStats .stat .k .kinds { font-weight: 400; color: var(--muted); }
  175. #taskStats .stat .v { font-size: 22px; letter-spacing: 0; white-space: nowrap; margin-bottom: 0; }
  176. #taskStats .stat .v i,
  177. #taskStats .stat .k .kinds i {
  178. font-style: normal;
  179. font-weight: 400;
  180. color: #b4bcc6;
  181. margin: 0 .42em;
  182. vertical-align: .12em;
  183. }
  184. #taskStats .stat .v i { font-size: 12px; }
  185. #taskStats .stat .k .kinds i { font-size: 10px; }
  186. .stat .s { font-size: 12px; color: var(--muted); margin-top: 0; }
  187. .plan-n-link {
  188. display: inline-flex; align-items: center; height: 22px; padding: 0 8px;
  189. border-radius: 6px; border: 1px solid var(--green-line); background: var(--green-soft);
  190. color: var(--green-dark); font-weight: 650; font-size: 12px; cursor: pointer;
  191. }
  192. .plan-n-link:hover { background: #d7eee0; }
  193. .plan-n-link.off { background: #f3f4f6; border-color: #e5e7eb; color: #6b7280; }
  194. #vehPlanBody tfoot td {
  195. font-weight: 700; background: #f7faf8; border-top: 1px solid var(--line);
  196. }
  197. .grid { display: grid; grid-template-columns: 1.35fr .9fr; gap: 12px; align-items: start; }
  198. .stack { display: flex; flex-direction: column; gap: 12px; }
  199. .card { background: var(--white); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
  200. .card-h { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border-bottom: 1px solid var(--line); }
  201. .card-h h2 { font-size: 14px; font-weight: 650; }
  202. .card-b { padding: 12px 14px; }
  203. .quick { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
  204. .qbtn {
  205. text-align: left; border: 1px solid var(--line); border-radius: var(--radius);
  206. padding: 14px; background: var(--white); min-height: 88px;
  207. box-shadow: var(--shadow); transition: border-color .15s, transform .15s;
  208. }
  209. .qbtn:hover { border-color: var(--green-line); transform: translateY(-1px); background: var(--white); }
  210. .qbtn .ico {
  211. width: 40px; height: 40px; border-radius: 10px; display: grid; place-items: center;
  212. margin-bottom: 8px; font-weight: 700; font-size: 12px;
  213. background: var(--green-soft); color: var(--green-dark);
  214. }
  215. .qbtn h3 { font-size: 13.5px; font-weight: 650; }
  216. .qbtn p { color: var(--muted); font-size: 12px; margin-top: 4px; line-height: 1.45; }
  217. table { width: 100%; border-collapse: collapse; }
  218. th, td { text-align: left; padding: 8px 6px; border-bottom: 1px solid #eef1f4; font-size: 12px; }
  219. th { color: var(--muted); font-weight: 500; }
  220. tr:hover td { background: #f8faf9; }
  221. .st { display: inline-flex; align-items: center; height: 20px; padding: 0 7px; border-radius: 99px; font-size: 11px; font-weight: 600; }
  222. .st-run { background: var(--green-soft); color: var(--green-dark); }
  223. .st-fix { background: #fff7ed; color: var(--amber); }
  224. .st-stop { background: #f3f4f6; color: #4b5563; }
  225. .st-due { background: #fef2f2; color: #b91c1c; }
  226. .timeline { display: flex; flex-direction: column; gap: 0; }
  227. .tl { display: grid; grid-template-columns: 72px 1fr; gap: 8px; padding: 8px 0; border-bottom: 1px solid #eef1f4; }
  228. .tl:last-child { border: 0; }
  229. .tl .t { color: var(--muted); font-size: 11px; }
  230. .tl b { font-weight: 600; }
  231. .tl .d { color: var(--muted); }
  232. .bars { display: flex; flex-direction: column; gap: 8px; }
  233. .bar-row { display: grid; grid-template-columns: 88px 1fr 64px; gap: 8px; align-items: center; }
  234. .bar-track { height: 8px; background: #eef1f4; border-radius: 99px; overflow: visible; position: relative; }
  235. .bar-fill { height: 100%; background: linear-gradient(90deg, #167c4a, #22a05d); border-radius: 99px; }
  236. .bar-fill.warn { background: linear-gradient(90deg, #e24b4a, #f97316); }
  237. .bar-mark { position: absolute; top: -3px; height: 14px; width: 2px; background: var(--warn); border-radius: 1px; }
  238. .bar-row .n { color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
  239. .rate-over { color: #b91c1c; font-weight: 650; }
  240. .filters { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
  241. .filters input, .filters select, .form input, .form select, .form textarea {
  242. height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; background: #fff; outline: none;
  243. }
  244. .form textarea { height: 64px; padding: 8px 10px; resize: vertical; }
  245. .form { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
  246. .form label { display: flex; flex-direction: column; gap: 4px; color: var(--muted); font-size: 12px; }
  247. .form .full { grid-column: 1 / -1; }
  248. .form .span2 { grid-column: span 2; }
  249. .fuel-plate-grid {
  250. display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px;
  251. }
  252. .fuel-plate-card {
  253. background: var(--white); border: 1px solid var(--line); border-radius: var(--radius);
  254. padding: 14px 16px; box-shadow: var(--shadow); min-width: 0; cursor: pointer;
  255. }
  256. .fuel-plate-card:hover { border-color: var(--blue); }
  257. .fuel-plate-card .plate { font-size: 15px; font-weight: 700; color: var(--ink); letter-spacing: .01em; white-space: nowrap; }
  258. .fuel-plate-card .meta { color: var(--muted); font-size: 12px; margin: 6px 0 10px; }
  259. .fuel-plate-card .nums { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
  260. .fuel-plate-card .nums.four { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
  261. .fuel-plate-card .nums.four b { font-size: 16px; }
  262. .fuel-plate-card .nums b { display: block; font-size: 18px; font-weight: 700; letter-spacing: -.03em; }
  263. .fuel-plate-card .nums span { color: var(--muted); font-size: 11px; }
  264. .fuel-plate-card .mom { display: block; font-size: 11px; font-style: normal; font-weight: 650; margin-top: 2px; }
  265. .fuel-plate-card .mom.up { color: #c2410c; }
  266. .fuel-plate-card .mom.down { color: var(--green-dark); }
  267. .fuel-plate-card .mom.flat { color: var(--muted); font-weight: 500; }
  268. @media (max-width: 1200px) { .fuel-plate-grid { grid-template-columns: repeat(2, 1fr); } }
  269. .file-box {
  270. border: 1px dashed var(--green-line); border-radius: 8px; background: #f7fbf8;
  271. padding: 8px 10px; min-height: 64px; display: flex; flex-direction: column; gap: 6px;
  272. }
  273. .file-box input[type=file] { height: auto; padding: 0; border: 0; background: transparent; }
  274. .file-hint { color: var(--amber); font-size: 11px; }
  275. .task-photo-row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 10px; }
  276. .task-photo-up { width: 132px; height: 96px; flex-shrink: 0; }
  277. .task-photo-thumbs { display: flex; flex-wrap: wrap; gap: 8px; }
  278. .task-photo-thumbs button { padding: 0; border: 0; background: transparent; cursor: pointer; }
  279. .task-photo-thumbs img {
  280. width: 88px; height: 88px; object-fit: cover; border-radius: 8px;
  281. border: 1px solid var(--line); display: block;
  282. }
  283. .chips { display: flex; flex-wrap: wrap; gap: 6px; }
  284. .chip {
  285. display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 99px;
  286. background: var(--green-soft); color: var(--green-dark); font-size: 11px; font-weight: 600; border: 0;
  287. }
  288. .chip.ghost { background: #f3f4f6; color: #4b5563; cursor: default; }
  289. .chip:disabled { opacity: .65; cursor: default; }
  290. dialog {
  291. border: 1px solid var(--line); border-radius: 12px; padding: 0; box-shadow: var(--shadow);
  292. max-width: min(720px, 92vw); width: 720px;
  293. }
  294. dialog.apply-dlg { max-width: min(760px, 94vw); width: 760px; }
  295. dialog.apply-dlg.wide-dlg { max-width: min(920px, 96vw); width: 920px; }
  296. .task-line-wrap { overflow: auto; border: 1px solid var(--line); border-radius: 8px; }
  297. .task-line-table { width: 100%; border-collapse: collapse; font-size: 12px; }
  298. .task-line-table th, .task-line-table td { border-bottom: 1px solid #eef1f4; padding: 7px 8px; text-align: left; }
  299. .task-line-table thead th { background: #f7faf8; color: #5b6675; font-weight: 600; }
  300. .task-sheet {
  301. background: #fff; border: 1px solid #222; padding: 16px 18px 12px; font-size: 13px; color: #111;
  302. }
  303. .task-sheet h3 { text-align: center; font-size: 20px; letter-spacing: .18em; margin: 0 0 6px; font-weight: 700; }
  304. .task-sheet .ts-no { text-align: right; font-size: 12px; margin: 0 2px 6px; }
  305. .task-sheet table { width: 100%; border-collapse: collapse; table-layout: fixed; }
  306. .task-sheet td, .task-sheet th { border: 1px solid #333; padding: 7px 8px; vertical-align: middle; }
  307. .task-sheet th { text-align: center; font-weight: 650; background: #fff; }
  308. .task-sheet td.idx, .task-sheet td.center { text-align: center; }
  309. .task-sheet .ts-foot { text-align: center; letter-spacing: .08em; }
  310. .task-sheet .ts-checks { display: inline-flex; align-items: center; gap: 14px; margin-left: 16px; }
  311. .task-sheet .ts-box {
  312. display: inline-block; width: 12px; height: 12px; border: 1px solid #333;
  313. margin-right: 4px; vertical-align: -1px; background: #fff;
  314. }
  315. .task-sheet .ts-box.on { background: #111; }
  316. dialog::backdrop { background: rgba(28, 36, 48, .35); }
  317. .dlg-h { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border-bottom: 1px solid var(--line); }
  318. .dlg-h.green { background: var(--green-soft); }
  319. .dlg-h h3 { font-size: 14px; }
  320. .dlg-x { width: 28px; height: 28px; border-radius: 6px; color: #5b6572; font-size: 18px; line-height: 1; }
  321. .dlg-x:hover { background: #e4eee7; }
  322. .dlg-b { padding: 14px; max-height: 70vh; overflow: auto; }
  323. .dlg-b img { max-width: 100%; border-radius: 8px; display: block; }
  324. .dlg-f { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--line); }
  325. .apply { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 28px; }
  326. .apply .row { display: grid; grid-template-columns: 88px 1fr; gap: 10px; align-items: start; }
  327. .apply .full { grid-column: 1 / -1; }
  328. .apply .lab { color: var(--ink); padding-top: 7px; text-align: right; }
  329. .apply input, .apply select, .apply textarea {
  330. width: 100%; height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; background: #fff; outline: none;
  331. }
  332. .apply textarea { height: 88px; padding: 8px 10px; resize: vertical; }
  333. .star { color: #e24b4a; margin-right: 2px; }
  334. .stepper { display: flex; height: 32px; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: #fff; }
  335. .stepper button { width: 36px; background: #f7f8fa; color: #6a7380; }
  336. .stepper button:hover { background: #eef1f4; }
  337. .stepper input { flex: 1; border: 0; border-radius: 0; border-left: 1px solid var(--line); border-right: 1px solid var(--line); text-align: center; height: 32px; width: auto; }
  338. .upload {
  339. width: 132px; height: 108px; border: 1px dashed var(--green-line); border-radius: 10px;
  340. background: #f4faf6; display: flex; flex-direction: column; align-items: center; justify-content: center;
  341. color: var(--green); gap: 4px; cursor: pointer; font-weight: 500;
  342. }
  343. .ocr-box .upload { width: 100%; height: 72px; }
  344. .upload:hover { background: #ebf7ef; }
  345. .upload input { display: none; }
  346. .upload .plus { font-size: 22px; line-height: 1; font-weight: 400; }
  347. .hint { color: var(--muted); font-size: 11px; margin-top: 8px; }
  348. .hidden { display: none !important; }
  349. .toast {
  350. position: fixed; right: 20px; bottom: 20px; background: #1c2430; color: #fff;
  351. padding: 10px 14px; border-radius: 8px; display: none; z-index: 50;
  352. }
  353. .toast.show { display: block; }
  354. .pie-wrap { display: flex; align-items: center; gap: 16px; }
  355. .legend { display: flex; flex-direction: column; gap: 6px; }
  356. .legend span { display: flex; align-items: center; gap: 6px; }
  357. .sw { width: 8px; height: 8px; border-radius: 2px; }
  358. .cell-no { color: var(--green-dark); font-weight: 600; }
  359. .muted { color: var(--muted); }
  360. .row-link { cursor: pointer; }
  361. .need-tag { display: inline-flex; height: 18px; padding: 0 6px; border-radius: 4px; background: #fef2f2; color: #b91c1c; font-size: 10px; font-weight: 700; margin-left: 4px; }
  362. .need-tag.bao { background: #fff7ed; color: var(--amber); }
  363. .remain { font-variant-numeric: tabular-nums; font-weight: 650; }
  364. .remain.warn { color: var(--amber); }
  365. .remain.bad { color: #b91c1c; }
  366. .ocr-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
  367. .ocr-box {
  368. border: 1px dashed var(--green-line); border-radius: 12px; background: #f7fbf8; padding: 12px;
  369. min-height: 148px; display: flex; flex-direction: column; gap: 8px;
  370. }
  371. .ocr-box.buy { border-color: var(--green-line); background: #f4faf6; }
  372. .ocr-box.trans { border-color: #b7cce8; background: #f3f7fc; }
  373. .ocr-box.drag { outline: 2px dashed var(--green); outline-offset: 2px; }
  374. .ocr-box.trans.drag { outline-color: var(--blue); }
  375. .ocr-box.buy h4 { color: var(--green-dark); }
  376. .ocr-box.trans h4 { color: var(--blue); }
  377. .ocr-box.trans .upload { border-color: #b7cce8; background: #eef4fb; color: var(--blue); }
  378. .ocr-box.trans .upload:hover { background: #e4eef8; }
  379. .split-h .col-buy h4 { color: var(--green-dark); border-bottom-color: var(--green-line); }
  380. .split-h .col-trans h4 { color: var(--blue); border-bottom-color: #c5d6ee; }
  381. dialog:not([open]) { display: none; }
  382. .ocr-box h4 { font-size: 13px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  383. .ocr-preview { width: 100%; max-height: 120px; object-fit: contain; border-radius: 8px; background: #fff; border: 1px solid var(--line); }
  384. .ocr-status { font-size: 12px; color: var(--amber); min-height: 18px; }
  385. .ocr-status.ok { color: var(--green-dark); }
  386. .split-h { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 4px; }
  387. .split-h h4 { font-size: 13px; margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1px solid var(--line); }
  388. .kv { display: grid; grid-template-columns: 108px 1fr; gap: 6px 10px; font-size: 12px; }
  389. .kv .lab { color: var(--muted); text-align: right; }
  390. .kv .val { font-weight: 500; }
  391. .sheet { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  392. .sheet .full-card { grid-column: 1 / -1; }
  393. .cert-duo {
  394. display: grid;
  395. grid-template-columns: minmax(0, 1fr) minmax(0, 3fr);
  396. gap: 16px;
  397. align-items: stretch;
  398. width: 100%;
  399. margin-top: 24px;
  400. }
  401. .cert-pane.buy .kv { grid-template-columns: 72px 1fr; }
  402. .cert-thumb {
  403. display: block; width: 100%; margin-top: 12px; padding: 6px;
  404. border: 1px solid var(--line); border-radius: 8px; background: #fff;
  405. cursor: zoom-in; text-align: center;
  406. }
  407. .cert-thumb:hover { border-color: var(--green-line); }
  408. .cert-pane.trans .cert-thumb:hover { border-color: var(--blue-line); }
  409. .cert-thumb img {
  410. display: block; width: 100%; max-height: 140px; object-fit: contain; margin: 0 auto;
  411. }
  412. dialog.cert-img-dlg {
  413. max-width: min(1100px, 96vw); width: auto; background: transparent;
  414. border: 0; box-shadow: none; padding: 0;
  415. }
  416. dialog.cert-img-dlg img {
  417. display: block; max-width: 94vw; max-height: 88vh; object-fit: contain;
  418. border-radius: 10px; background: #111; box-shadow: 0 16px 48px rgba(0,0,0,.35);
  419. }
  420. dialog.cert-img-dlg::backdrop { background: rgba(28, 36, 48, .72); }
  421. .cert-pane {
  422. border-radius: var(--radius);
  423. padding: 10px 10px 12px;
  424. min-width: 0;
  425. display: flex;
  426. flex-direction: column;
  427. }
  428. .cert-pane > .card { flex: 1; }
  429. .cert-pane.buy {
  430. background: linear-gradient(180deg, #e8f6ee 0%, #f4faf6 48px, #f7fbf8 100%);
  431. border: 1px solid var(--green-line);
  432. }
  433. .cert-pane.trans {
  434. background: linear-gradient(180deg, var(--blue-soft) 0%, #f3f7fc 48px, #f6f9fd 100%);
  435. border: 1px solid var(--blue-line);
  436. }
  437. .trans-card-row {
  438. display: flex;
  439. gap: 12px;
  440. align-items: stretch;
  441. min-width: 0;
  442. }
  443. .trans-card-row > .card {
  444. flex: 1 1 0;
  445. min-width: 0;
  446. margin: 0;
  447. }
  448. .trans-card-row .kv { grid-template-columns: 76px 1fr; }
  449. .cert-pane-h {
  450. display: flex; align-items: center; gap: 8px;
  451. padding: 4px 6px 10px;
  452. }
  453. .cert-pane-h h2 { font-size: 14px; font-weight: 650; }
  454. .cert-pane-h .muted { margin-left: auto; font-size: 12px; }
  455. .cert-tag {
  456. display: inline-flex; align-items: center; height: 22px; padding: 0 8px;
  457. border-radius: 6px; font-size: 11px; font-weight: 700; letter-spacing: 0.04em;
  458. }
  459. .cert-tag.buy { background: var(--green); color: #fff; }
  460. .cert-tag.trans { background: var(--blue); color: #fff; }
  461. .cert-pane.buy .card { border-color: var(--green-line); }
  462. .cert-pane.trans .card { border-color: var(--blue-line); }
  463. .cert-pane .card-h { background: #fff; }
  464. .cert-pane.buy .card-h { border-bottom-color: var(--green-line); }
  465. .cert-pane.trans .card-h { border-bottom-color: var(--blue-line); }
  466. .stat.tone-buy { border-color: var(--green-line); box-shadow: none; }
  467. .stat.tone-buy .k { color: var(--green-dark); }
  468. .stat.tone-trans { border-color: var(--blue-line); box-shadow: none; }
  469. .stat.tone-trans .k { color: var(--blue); }
  470. .qty-bar { height: 8px; background: #eef1f4; border-radius: 99px; overflow: hidden; margin-top: 8px; }
  471. .qty-fill { height: 100%; background: linear-gradient(90deg, #167c4a, #22a05d); }
  472. .qty-fill.warn { background: linear-gradient(90deg, #e24b4a, #f97316); }
  473. .warn-qty-table { width: 100%; border-collapse: collapse; }
  474. .warn-qty-table th, .warn-qty-table td {
  475. padding: 8px 10px; font-size: 12px; text-align: left; border-bottom: 1px solid #eef1f4; vertical-align: middle;
  476. }
  477. .warn-qty-table thead th { color: #5b6675; font-weight: 600; background: #f7faf8; }
  478. .warn-qty-edit { display: inline-flex; align-items: center; gap: 6px; }
  479. .warn-qty-edit input {
  480. width: 88px; height: 28px; border: 1px solid var(--line); border-radius: 6px; padding: 0 8px;
  481. }
  482. .erp-page { margin-top: -6px; }
  483. .rel-flow {
  484. display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center;
  485. font-size: 12.5px; color: var(--muted); margin: 0 0 12px; padding: 8px 12px;
  486. background: #fff; border: 1px dashed var(--green-line); border-radius: 10px;
  487. }
  488. .rel-flow b { color: var(--ink); font-weight: 650; }
  489. .rel-flow em { font-style: normal; color: var(--green); font-weight: 650; }
  490. .cert-steps {
  491. display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px;
  492. }
  493. .cert-step {
  494. background: #fff; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
  495. display: flex; gap: 10px; align-items: flex-start;
  496. }
  497. .cert-step .n {
  498. width: 22px; height: 22px; border-radius: 99px; flex: none;
  499. background: var(--green-soft); color: var(--green-dark);
  500. display: grid; place-items: center; font-weight: 750; font-size: 12px;
  501. }
  502. .cert-step { cursor: pointer; }
  503. .cert-step:hover { border-color: var(--green-line); }
  504. .cert-step.on { border-color: var(--green); background: #f4faf6; }
  505. .cert-step.warn { border-color: #fdba74; background: #fff7ed; }
  506. .cert-step.warn .n { background: #fff7ed; color: #9a3412; }
  507. .cert-step b { display: block; font-size: 13px; }
  508. .cert-step p { color: var(--muted); font-size: 12px; margin-top: 2px; line-height: 1.45; }
  509. .cert-step .s { margin-top: 4px; font-size: 12px; color: var(--ink); font-weight: 650; }
  510. @media (max-width: 980px) { .cert-steps { grid-template-columns: 1fr; } }
  511. .extra-trans {
  512. margin-top: 12px; padding: 12px; border: 1px dashed #b7cce8; border-radius: 10px; background: #f6f9fd;
  513. }
  514. .extra-trans.drag { outline: 2px dashed var(--blue); outline-offset: 2px; }
  515. .extra-trans h4 { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
  516. .veh-over td { background: #fff5f5; }
  517. .warn-dot { color: #b91c1c; font-weight: 700; }
  518. .veh-board { overflow: auto; }
  519. .link-preview {
  520. margin: 0 0 12px; padding: 10px 12px; border-radius: 10px;
  521. background: #f4faf6; border: 1px solid var(--green-line); font-size: 13px; line-height: 1.55;
  522. }
  523. .link-preview.warn { background: #fff7ed; border-color: #fdba74; }
  524. .link-preview.over { background: #fef2f2; border-color: #fecaca; color: #9f1239; }
  525. .stat.stat-click { cursor: pointer; }
  526. .stat.stat-click:hover { border-color: var(--green-line); }
  527. .wait-link-box { margin-bottom: 12px; }
  528. .wait-link-box .card-h { align-items: flex-start; flex-wrap: wrap; }
  529. .wait-link-box .card-h h2 { flex: none; white-space: nowrap; }
  530. .wait-link-box .card-h .muted { flex: 1; min-width: 180px; }
  531. .match-rec { display: inline-flex; align-items: center; height: 18px; padding: 0 6px; border-radius: 4px; background: var(--green-soft); color: var(--green-dark); font-size: 10px; font-weight: 700; white-space: nowrap; }
  532. .match-rec.veh { background: #eef4fb; color: var(--blue); }
  533. .match-rec.warn { background: #fff7ed; color: var(--amber); }
  534. .match-rec.in { background: #eef4fb; color: var(--blue); }
  535. .match-rec.out { background: var(--green-soft); color: var(--green-dark); }
  536. .match-rec.direct { background: #f5f0ff; color: #6d28d9; }
  537. #certWaitCard { overflow: visible; }
  538. .wait-scene-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
  539. .wait-table-wrap { width: 100%; overflow-x: auto; overflow-y: visible; max-height: none; border: 1px solid var(--line); border-radius: 8px; position: relative; z-index: 2; box-sizing: border-box; }
  540. .wait-table, table.erp-table.wait-table { width: 100%; min-width: 100%; border-collapse: collapse; }
  541. .wait-table th, .wait-table td {
  542. padding: 8px 10px; font-size: 12px; vertical-align: middle;
  543. white-space: nowrap; border-bottom: 1px solid #eef1f4; background: #fff;
  544. }
  545. .wait-table thead th {
  546. position: sticky; top: 0; z-index: 1; background: #f7faf8;
  547. color: #5b6675; font-weight: 600;
  548. }
  549. .wait-table tbody tr:hover td { background: #f8faf9; }
  550. .wait-table tr.warn-row td { background: #fffdf8; }
  551. .wait-table tr.over-row td { background: #fff8f8; }
  552. .wait-table .cell-name, .wait-table td.wrap {
  553. white-space: normal; max-width: 140px; line-height: 1.4;
  554. }
  555. .wait-table .cell-remain, .wait-table td.remain-cell {
  556. white-space: normal; min-width: 140px; max-width: 200px; line-height: 1.4;
  557. }
  558. .wait-table select {
  559. display: block; height: 28px; min-width: 148px; max-width: 210px;
  560. border: 1px solid var(--line); border-radius: 6px; padding: 0 6px; background: #fff;
  561. }
  562. .wait-table .hint { margin-top: 4px; }
  563. .wait-table .op, .wait-table td.ops { white-space: nowrap; }
  564. .erp-table.wait-table .idx,
  565. .erp-table.wait-table .col-no {
  566. position: static; left: auto; box-shadow: none;
  567. }
  568. .erp-table.wait-table .col-op-buy,
  569. .erp-table.wait-table .col-op-trans {
  570. position: sticky; right: auto; min-width: 88px; z-index: 5; text-align: center;
  571. }
  572. .erp-table.wait-table .col-op-buy { left: 0; }
  573. .erp-table.wait-table .col-op-trans { left: 88px; box-shadow: 6px 0 8px -6px rgba(28,36,48,.12); }
  574. .erp-table.wait-table thead .col-op-buy,
  575. .erp-table.wait-table thead .col-op-trans { z-index: 6; background: #f7faf8; }
  576. .erp-table.wait-table tbody .col-op-buy,
  577. .erp-table.wait-table tbody .col-op-trans { background: #fff; }
  578. .wait-table .col-op-buy .btn,
  579. .wait-table .col-op-trans .btn { margin: 0; width: 100%; white-space: nowrap; }
  580. .wait-table .clip { max-width: none; overflow: visible; text-overflow: unset; }
  581. .wait-table th.col-ship,
  582. .wait-table td.col-ship {
  583. white-space: normal; max-width: none; min-width: 196px; width: 18%; line-height: 1.4; overflow: visible;
  584. }
  585. .wait-op-no { font-size: 11px; color: var(--muted); line-height: 1.3; word-break: break-all; }
  586. .btn.wait-btn-buy {
  587. background: #e8f6ee; border-color: var(--green-line); color: var(--green-dark);
  588. }
  589. .btn.wait-btn-buy:hover { background: #d7eee0; }
  590. .btn.wait-btn-trans {
  591. background: #eef4fb; border-color: var(--blue-line); color: var(--blue);
  592. }
  593. .btn.wait-btn-trans:hover { background: #e0ebf7; }
  594. .btn.wait-btn-buy.wait-on,
  595. .btn.wait-btn-trans.wait-on {
  596. background: #f3f4f6; border-color: #e5e7eb; color: #6b7280; font-weight: 500;
  597. }
  598. .btn.wait-btn-buy.wait-on:hover,
  599. .btn.wait-btn-trans.wait-on:hover { background: #eaecef; border-color: #d1d5db; color: #4b5563; }
  600. dialog.wait-link-dlg { max-width: min(1180px, 96vw); width: 1180px; }
  601. .wait-link-dlg .dlg-b { overflow: auto; }
  602. .wait-link-dlg .cert-pane {
  603. margin-bottom: 0; overflow: visible; min-width: 0; height: auto;
  604. box-sizing: border-box;
  605. }
  606. .wait-link-dlg .cert-pane > .card,
  607. .wait-link-dlg .wait-trans-sheet {
  608. margin: 0; box-shadow: none; min-width: 0; flex: none;
  609. overflow: visible; border-radius: 10px; box-sizing: border-box;
  610. }
  611. .wait-link-dlg .wait-trans-sheet + .wait-trans-sheet { margin-top: 12px; }
  612. .wait-link-dlg .wait-trans-sheet.hit { border-color: var(--blue-line); }
  613. .wait-link-dlg .qty-bar { margin-bottom: 2px; }
  614. .wait-link-dlg .wait-cert-pick {
  615. display: flex; align-items: center; gap: 8px; cursor: pointer; min-width: 0;
  616. }
  617. .wait-link-dlg .wait-cert-pick input { margin: 0; }
  618. .wait-link-dlg .card-h .btn { margin-left: auto; }
  619. .wait-link-dlg .kv .val { overflow-wrap: anywhere; word-break: break-word; }
  620. .wait-plan-bar {
  621. display: flex; flex-wrap: wrap; gap: 6px 16px; align-items: center;
  622. padding: 10px 12px; margin-bottom: 12px;
  623. background: #f7faf8; border: 1px solid var(--green-line); border-radius: 10px; font-size: 12px;
  624. }
  625. .wait-plan-bar b { font-weight: 650; }
  626. .wait-dlg-split { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; align-items: stretch; }
  627. .wait-dlg-split > * { min-width: 0; }
  628. @media (max-width: 900px) { .wait-dlg-split { grid-template-columns: 1fr; } }
  629. .wait-info-card {
  630. border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: #fff; margin-bottom: 10px;
  631. }
  632. .wait-info-card.buy { border-color: var(--green-line); background: #f7fbf8; }
  633. .wait-info-card.trans { border-color: #c5d6ee; background: #f6f9fd; }
  634. .wait-info-card.hit { box-shadow: inset 0 0 0 1px var(--green); }
  635. .wait-info-card h4 {
  636. display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 13px;
  637. }
  638. .wait-info-card .kv { grid-template-columns: 78px 1fr 78px 1fr; gap: 4px 8px; }
  639. .wait-info-stack { max-height: 56vh; overflow: auto; padding: 8px 6px; min-width: 0; }
  640. .wait-scene-tabs button {
  641. height: 26px; padding: 0 10px; border-radius: 6px; border: 1px solid var(--line); background: #fff; color: #4b5563;
  642. }
  643. .wait-scene-tabs button.on { background: var(--green-soft); color: var(--green-dark); border-color: var(--green-line); font-weight: 650; }
  644. .qty-formula {
  645. display: flex; flex-wrap: wrap; gap: 8px 18px; margin-bottom: 12px;
  646. padding: 10px 12px; background: #f7faf8; border: 1px dashed var(--green-line); border-radius: 10px; font-size: 13px;
  647. }
  648. .qty-formula b { font-variant-numeric: tabular-nums; }
  649. .qty-formula .bad { color: var(--warn); }
  650. .qty-formula .warn { color: var(--amber); }
  651. .link-sec h4 { font-size: 13px; margin: 4px 0 8px; }
  652. .link-sec + .link-sec { margin-top: 14px; }
  653. .ct-mini { font-size: 11px; color: var(--muted); line-height: 1.45; }
  654. .erp-tabs-row {
  655. display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  656. border-bottom: 1px solid var(--line); margin-bottom: 8px;
  657. }
  658. .plan-tabs { display: flex; gap: 2px; flex-wrap: wrap; }
  659. .plan-tabs button {
  660. height: 36px; padding: 0 14px; border: 0; background: transparent;
  661. color: #4b5563; font-weight: 500; border-bottom: 2px solid transparent; margin-bottom: -1px;
  662. }
  663. .plan-tabs button.on { color: var(--green); font-weight: 650; border-bottom-color: var(--green); }
  664. .plan-table tr.row-wait td { background: #fff8f8; }
  665. .plan-foot {
  666. display: flex; justify-content: space-between; align-items: center;
  667. padding: 8px 14px; border-top: 1px solid var(--green-line); background: #eaf6ef;
  668. }
  669. .erp-toolbar {
  670. display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  671. padding: 4px 0 8px;
  672. }
  673. .erp-toolbar .btn { display: inline-flex; align-items: center; gap: 4px; }
  674. .erp-toolbar .split { width: 1px; height: 16px; background: var(--line); margin: 0 2px; }
  675. .erp-toolbar .right { margin-left: auto; display: flex; align-items: center; gap: 6px; }
  676. .erp-toolbar input[type=search], .erp-toolbar input[type=text] {
  677. height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; width: 168px;
  678. }
  679. .erp-toolbar .btn.on { background: var(--green-soft); border-color: var(--green-line); color: var(--green-dark); }
  680. .btn-danger { background: var(--warn); color: #fff; border-color: var(--warn); }
  681. .btn-danger:hover { background: #c13b35; border-color: #c13b35; }
  682. .plan-action-row { align-items: center; border-bottom: 0; margin-bottom: 0; gap: 10px; }
  683. .plan-action-row .plan-tabs button { height: 32px; padding: 0 10px; }
  684. .quote-search.plan-search { width: 280px; margin-left: 0; }
  685. .plan-toolbar { padding: 6px 0 8px; }
  686. #planKpis { margin-bottom: 12px; grid-template-columns: repeat(4, 1fr); }
  687. #planKpis .stat {
  688. width: 100%; text-align: left;
  689. display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 10px;
  690. min-height: 96px; padding: 18px 18px;
  691. transition: border-color .15s, background .15s;
  692. }
  693. #planKpis .stat .k { color: var(--ink); font-size: 13px; font-weight: 650; white-space: nowrap; }
  694. #planKpis .stat .v {
  695. margin: 0; padding: 0; border: 0; background: none;
  696. font-size: 28px; line-height: 1; font-weight: 700; letter-spacing: -.03em;
  697. cursor: pointer; color: var(--amber);
  698. }
  699. #planKpis .stat .v:hover { opacity: .85; }
  700. #planKpis .today-bits {
  701. display: flex; flex-wrap: nowrap; align-items: baseline; gap: 14px;
  702. white-space: nowrap;
  703. }
  704. #planKpis .today-bits button {
  705. margin: 0; padding: 0; border: 0; background: none; cursor: pointer;
  706. font-size: 13px; font-weight: 500; color: var(--muted); line-height: 1;
  707. }
  708. #planKpis .today-bits button b { font-size: 22px; font-weight: 700; color: var(--green); margin: 0 3px; }
  709. #planKpis .today-bits button.open b { color: var(--amber); }
  710. #planKpis .today-bits button:hover { color: var(--ink); }
  711. #planKpis .today-bits button.on { color: var(--ink); }
  712. #planKpis .stat.tone-ctrl .v { color: var(--warn); }
  713. #planKpis .stat.tone-arch .v { color: var(--blue); }
  714. #planKpis .stat.on.tone-wait { border-color: #e8c56b; background: var(--amber-soft); }
  715. #planKpis .stat.on.tone-today { border-color: var(--green-line); background: var(--green-soft); }
  716. #planKpis .stat.on.tone-ctrl { border-color: #f5c5c2; background: var(--warn-soft); }
  717. #planKpis .stat.on.tone-arch { border-color: var(--blue-line); background: var(--blue-soft); }
  718. .erp-hint { font-size: 12px; color: var(--muted); margin-left: auto; }
  719. .erp-hint b { color: var(--ink); font-weight: 600; }
  720. .erp-card { overflow: hidden; }
  721. .erp-table-wrap { overflow: auto; max-height: calc(100vh - 228px); }
  722. #quoteStats, #ctStats { display: block; margin-bottom: 12px; }
  723. .wait-banner {
  724. width: 100%; display: flex; align-items: center; gap: 10px;
  725. padding: 12px 16px; text-align: left; cursor: pointer;
  726. background: #fff; border: 1px solid #f3ddb0; border-radius: var(--radius);
  727. box-shadow: var(--shadow);
  728. transition: border-color .15s, background .15s, box-shadow .15s;
  729. }
  730. .wait-banner:hover { border-color: #e8c56b; background: var(--amber-soft); }
  731. .wait-banner.on { border-color: #e8c56b; background: var(--amber-soft); }
  732. .wait-banner.empty { border-color: var(--green-line); }
  733. .wait-banner.empty:hover, .wait-banner.empty.on { background: var(--green-soft); border-color: var(--green); }
  734. .wb-ico {
  735. width: 36px; height: 36px; border-radius: 10px; flex: none;
  736. display: grid; place-items: center; background: var(--amber-soft); color: var(--amber);
  737. }
  738. .wait-banner.empty .wb-ico { background: var(--green-soft); color: var(--green-dark); }
  739. .wb-ico svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.8; }
  740. .wb-n { font-size: 28px; font-weight: 700; letter-spacing: -.04em; color: var(--amber); line-height: 1; }
  741. .wait-banner.empty .wb-n { color: var(--green-dark); }
  742. .wb-k { font-size: 14px; font-weight: 700; color: var(--ink); line-height: 1.2; }
  743. .wb-go {
  744. margin-left: auto; flex: none; height: 28px; padding: 0 12px; border-radius: 999px;
  745. font-size: 12px; font-weight: 650; display: grid; place-items: center;
  746. background: var(--amber); color: #fff;
  747. }
  748. .wait-banner.empty .wb-go { background: var(--green); }
  749. #mod-quote .erp-table-wrap, #mod-contract .erp-table-wrap { max-height: calc(100vh - 280px); }
  750. table.erp-table { min-width: 1680px; border-collapse: separate; border-spacing: 0; }
  751. .erp-table th, .erp-table td {
  752. padding: 7px 8px; font-size: 12px; white-space: nowrap; vertical-align: middle;
  753. border-bottom: 1px solid #eef1f4; background: #fff;
  754. }
  755. .erp-table thead th {
  756. position: sticky; top: 0; z-index: 2; background: #f7faf8;
  757. color: #5b6675; font-weight: 600; border-bottom: 1px solid var(--line);
  758. }
  759. .task-tag {
  760. display: inline-flex; align-items: center; height: 20px; padding: 0 7px;
  761. border-radius: 4px; font-size: 11px; font-weight: 650;
  762. }
  763. .task-tag.dilute { background: #e8f1fb; color: var(--blue); }
  764. .task-tag.refine { background: #f3eefb; color: #7c5cbf; }
  765. .task-tag.pack { background: #fff1e0; color: #c47d11; }
  766. .erp-table .num { font-variant-numeric: tabular-nums; }
  767. .erp-table .qty { color: var(--green); font-weight: 650; }
  768. .erp-table .out-qty { color: var(--blue); }
  769. .erp-table .chk, .erp-table .idx { width: 36px; text-align: center; color: var(--muted); padding-left: 8px; padding-right: 4px; }
  770. .erp-table .chk { position: sticky; left: 0; z-index: 3; }
  771. .erp-table .idx { position: sticky; left: 36px; z-index: 3; }
  772. .erp-table .col-no { position: sticky; left: 68px; z-index: 3; min-width: 128px; box-shadow: 6px 0 8px -6px rgba(28,36,48,.12); }
  773. .erp-table thead .chk, .erp-table thead .idx, .erp-table thead .col-no { z-index: 4; background: #f7faf8; }
  774. .erp-table tbody .chk, .erp-table tbody .idx, .erp-table tbody .col-no { background: inherit; }
  775. .erp-table .col-op {
  776. position: sticky; right: 0; z-index: 3; min-width: 88px;
  777. box-shadow: -6px 0 8px -6px rgba(28,36,48,.12);
  778. white-space: nowrap;
  779. }
  780. .erp-table.task-ops .col-op { min-width: 168px; }
  781. .erp-table thead .col-op { z-index: 4; background: #f7faf8; }
  782. .erp-table tbody .col-op { background: inherit; }
  783. .erp-table .clip { max-width: 148px; overflow: hidden; text-overflow: ellipsis; }
  784. .erp-table.wait-table .idx,
  785. .erp-table.wait-table .col-no {
  786. position: static !important; left: auto !important; box-shadow: none;
  787. }
  788. .erp-table.wait-table .col-op-buy,
  789. .erp-table.wait-table .col-op-trans {
  790. position: sticky; right: auto; min-width: 88px; z-index: 8; text-align: center;
  791. pointer-events: auto;
  792. }
  793. .erp-table.wait-table .col-op-buy { left: 0; }
  794. .erp-table.wait-table .col-op-trans { left: 88px; box-shadow: 6px 0 8px -6px rgba(28,36,48,.12); }
  795. .erp-table.wait-table thead .col-op-buy,
  796. .erp-table.wait-table thead .col-op-trans { z-index: 9; background: #f7faf8; }
  797. .erp-table.wait-table tbody .col-op-buy,
  798. .erp-table.wait-table tbody .col-op-trans { background: #fff; }
  799. .wait-table .col-op-buy .btn,
  800. .wait-table .col-op-trans .btn {
  801. position: relative; z-index: 9; pointer-events: auto; cursor: pointer;
  802. }
  803. .kind-tag {
  804. display: inline-flex; align-items: center; height: 22px; padding: 0 8px;
  805. border-radius: 4px; font-size: 12px; font-weight: 650;
  806. }
  807. .kind-tag.dilute { background: #eaf2fb; color: var(--blue); }
  808. .kind-tag.refine { background: var(--green-soft); color: var(--green-dark); }
  809. .kind-tag.pack { background: var(--amber-soft); color: var(--amber); }
  810. .st-pill {
  811. display: inline-flex; align-items: center; height: 22px; padding: 0 8px;
  812. border-radius: 4px; font-size: 12px; font-weight: 600; color: #fff;
  813. }
  814. .st-pill.wait { background: #f59e0b; }
  815. .st-pill.ok { background: #22a05d; }
  816. .st-pill.done { background: #22a05d; }
  817. .st-pill.fin { background: #2a6bb3; }
  818. .st-pill.arch { background: #64748b; }
  819. .wh-act { font-weight: 650; }
  820. .wh-act.in { color: var(--green); }
  821. .wh-act.out { color: var(--blue); }
  822. .wh-act.none { color: var(--muted); font-weight: 500; }
  823. .st-pill.void { background: #ef4444; }
  824. .prod-tag {
  825. display: inline-flex; align-items: center; height: 18px; padding: 0 6px; margin-left: 4px;
  826. border-radius: 3px; font-size: 11px; font-weight: 650;
  827. }
  828. .prod-tag.orange { background: #fff1e0; color: #c47d11; }
  829. .prod-tag.green { background: #e8f6ee; color: var(--green-dark); }
  830. .prod-tag.red { background: #fdecea; color: #d94841; }
  831. .match-tag { font-weight: 650; }
  832. .match-tag.yes { color: #c47d11; }
  833. .match-tag.ok { color: var(--green); }
  834. .match-tag.no { color: #9aa3ad; }
  835. .src-cell { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: #5b6675; line-height: 1.35; }
  836. .src-cell .ico { display: inline-flex; align-items: center; gap: 4px; }
  837. .src-dot { width: 14px; height: 14px; border-radius: 3px; background: #e8f6ee; color: var(--green-dark); font-size: 9px; font-weight: 800; display: grid; place-items: center; }
  838. .op-link { color: var(--green); font-weight: 600; background: none; border: 0; padding: 0 8px 0 0; }
  839. .op-link:hover { text-decoration: underline; }
  840. .op-link.muted { color: #9aa3ad; cursor: default; text-decoration: none; }
  841. .page-size { height: 26px; border: 1px solid var(--line); border-radius: 6px; padding: 0 6px; background: #fff; }
  842. .query-row { display: none; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
  843. .query-row.show { display: flex; }
  844. .query-row select, .query-row input {
  845. height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; background: #fff;
  846. }
  847. .plan-foot .sum { color: var(--green); font-weight: 650; }
  848. .amt-red { color: #e24b4a; font-weight: 700; font-variant-numeric: tabular-nums; }
  849. .sale-tag {
  850. display: inline-flex; align-items: center; height: 22px; padding: 0 8px;
  851. border-radius: 4px; font-size: 12px; font-weight: 650;
  852. background: var(--green-soft); color: var(--green-dark);
  853. }
  854. .quote-table { min-width: 1280px !important; }
  855. .quote-table .quote-expand,
  856. .quote-table .chk,
  857. .quote-table .col-no {
  858. position: sticky;
  859. z-index: 3;
  860. background: #fff;
  861. }
  862. .quote-table .quote-expand {
  863. left: 0;
  864. width: 32px; min-width: 32px; max-width: 32px;
  865. padding-left: 0; padding-right: 0;
  866. }
  867. .quote-table .chk {
  868. left: 32px;
  869. width: 40px; min-width: 40px; max-width: 40px;
  870. padding-left: 0; padding-right: 0;
  871. }
  872. .quote-table .col-no {
  873. left: 72px;
  874. width: 176px; min-width: 176px; max-width: 176px;
  875. }
  876. .quote-table thead .quote-expand,
  877. .quote-table thead .chk,
  878. .quote-table thead .col-no {
  879. z-index: 5;
  880. background: #f7faf8;
  881. }
  882. .quote-table tbody .quote-expand,
  883. .quote-table tbody .chk,
  884. .quote-table tbody .col-no { background: #fff; }
  885. .quote-table tr.row-alert .quote-expand,
  886. .quote-table tr.row-alert .chk,
  887. .quote-table tr.row-alert .col-no { background: #fdecea; }
  888. .quote-table .col-title {
  889. min-width: 168px;
  890. max-width: 240px;
  891. overflow: hidden;
  892. text-overflow: ellipsis;
  893. }
  894. .quote-table .col-title .quote-link {
  895. display: inline-block;
  896. max-width: 100%;
  897. overflow: hidden;
  898. text-overflow: ellipsis;
  899. vertical-align: bottom;
  900. }
  901. .erp-table tr.row-alert td { background: #fdecea; }
  902. .erp-table tr.row-alert .chk, .erp-table tr.row-alert .idx, .erp-table tr.row-alert .col-no,
  903. .erp-table tr.row-alert .quote-expand { background: #fdecea; }
  904. .quote-expand { color: #9aa3ad; text-align: center; cursor: pointer; }
  905. .quote-expand.on { color: var(--green-dark); }
  906. .quote-sub td { background: #f8faf9 !important; font-size: 12px; color: var(--muted); white-space: normal; }
  907. .quote-link { color: var(--green); font-weight: 650; background: none; border: 0; padding: 0; text-align: left; }
  908. .quote-link:hover { text-decoration: underline; }
  909. .th-sort { display: inline-flex; align-items: center; gap: 4px; }
  910. .th-sort::after { content: ""; width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid #c5ccd4; }
  911. .quote-search.wide { width: 280px; }
  912. .erp-drop { position: relative; }
  913. .erp-drop-menu {
  914. display: none; position: absolute; top: calc(100% + 4px); left: 0; z-index: 8;
  915. min-width: 132px; background: #fff; border: 1px solid var(--line); border-radius: 8px;
  916. box-shadow: var(--shadow); padding: 4px;
  917. }
  918. .erp-drop.open .erp-drop-menu { display: block; }
  919. .erp-drop-menu button {
  920. display: block; width: 100%; text-align: left; height: 32px; padding: 0 10px;
  921. border-radius: 6px; color: var(--ink);
  922. }
  923. .erp-drop-menu button:hover { background: var(--green-soft); color: var(--green-dark); }
  924. .ct-table { min-width: 1780px !important; }
  925. .ct-table .quote-expand,
  926. .ct-table .chk,
  927. .ct-table .col-no {
  928. position: sticky; z-index: 3; background: #fff;
  929. }
  930. .ct-table .quote-expand { left: 0; width: 32px; min-width: 32px; max-width: 32px; padding: 7px 0; }
  931. .ct-table .chk { left: 32px; width: 40px; min-width: 40px; max-width: 40px; padding: 7px 0; }
  932. .ct-table .col-no { left: 72px; width: 168px; min-width: 168px; max-width: 168px; }
  933. .ct-table thead .quote-expand,
  934. .ct-table thead .chk,
  935. .ct-table thead .col-no { z-index: 5; background: #f7faf8; }
  936. .ct-table tbody .quote-expand,
  937. .ct-table tbody .chk,
  938. .ct-table tbody .col-no { background: #fff; }
  939. .ct-table tr.row-alert .quote-expand,
  940. .ct-table tr.row-alert .chk,
  941. .ct-table tr.row-alert .col-no { background: #fdecea; }
  942. .ct-name { white-space: normal !important; min-width: 148px; max-width: 186px; line-height: 1.35; }
  943. .ct-name .quote-link { display: block; }
  944. .ct-name .sub { display: block; color: var(--green); font-size: 11px; font-weight: 600; }
  945. .ct-cust { white-space: normal !important; min-width: 132px; max-width: 168px; line-height: 1.35; }
  946. .ct-prod { white-space: normal !important; min-width: 96px; max-width: 118px; line-height: 1.35; }
  947. .ct-att { display: flex; align-items: center; gap: 8px; min-width: 92px; }
  948. .pdf-chip { display: flex; flex-direction: column; align-items: center; gap: 2px; color: #d94841; min-width: 48px; }
  949. .pdf-box {
  950. width: 28px; height: 32px; border-radius: 3px; background: #e24b4a; color: #fff;
  951. font-size: 8px; font-weight: 800; letter-spacing: .2px; display: grid; place-items: center;
  952. box-shadow: 0 1px 2px rgba(226,75,74,.25);
  953. }
  954. .pdf-chip em { font-style: normal; font-size: 10px; font-weight: 650; color: #5b6675; white-space: nowrap; }
  955. .up-chip {
  956. width: 28px; height: 28px; border-radius: 6px; background: var(--green-soft); color: var(--green);
  957. display: grid; place-items: center; flex: none;
  958. }
  959. .up-chip:hover { background: var(--green); color: #fff; }
  960. .up-chip svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 2; }
  961. .ct-st { color: #3d4754; }
  962. .quote-search {
  963. margin-left: auto; display: flex; align-items: center; gap: 6px;
  964. height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; background: #fff; width: 240px;
  965. }
  966. .quote-search svg { width: 14px; height: 14px; stroke: #8a94a0; fill: none; stroke-width: 2; flex: none; }
  967. .quote-search input { border: 0; outline: 0; flex: 1; min-width: 0; background: transparent; }
  968. .btn-ico { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.8; }
  969. .btn .btn-ico { margin-right: 4px; vertical-align: -2px; }
  970. .quote-pager { display: flex; align-items: center; gap: 6px; }
  971. .quote-pager button {
  972. min-width: 28px; height: 28px; padding: 0 8px; border: 1px solid transparent;
  973. border-radius: 6px; color: #4b5563;
  974. }
  975. .quote-pager button.on { background: var(--green); color: #fff; font-weight: 650; }
  976. #mod-product .erp-toolbar { padding-top: 0; }
  977. #mod-product .prod-search-row {
  978. display: flex;
  979. align-items: center;
  980. flex-wrap: nowrap;
  981. gap: 8px;
  982. width: 100%;
  983. padding: 0 0 8px;
  984. }
  985. #mod-product .prod-search-row .quote-search {
  986. width: 280px;
  987. flex: 0 0 280px;
  988. margin: 0;
  989. }
  990. #mod-product .prod-search-actions {
  991. margin-left: auto;
  992. display: flex;
  993. align-items: center;
  994. gap: 6px;
  995. flex: none;
  996. }
  997. #mod-product .erp-table-wrap { max-height: calc(100vh - 260px); }
  998. #mod-staff .erp-toolbar { padding-top: 0; }
  999. #mod-staff .erp-table-wrap { max-height: calc(100vh - 360px); }
  1000. .staff-table { min-width: 1280px !important; }
  1001. .staff-name { display: inline-flex; align-items: center; gap: 8px; }
  1002. .staff-av {
  1003. width: 26px; height: 26px; border-radius: 99px; flex: none;
  1004. background: var(--green-soft); color: var(--green-dark);
  1005. display: grid; place-items: center; font-size: 11px; font-weight: 700;
  1006. }
  1007. .staff-due { color: #b91c1c; font-weight: 650; }
  1008. .staff-dept-sel {
  1009. height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 8px; background: #fff; min-width: 120px;
  1010. }
  1011. .quote-search.prod-search { width: 280px; margin-left: 0; }
  1012. .prod-table { min-width: 1180px !important; }
  1013. .prod-table .chk { width: 36px; }
  1014. .prod-table .col-idx { width: 56px; }
  1015. .prod-hz {
  1016. display: inline-flex; align-items: center; justify-content: center;
  1017. min-width: 16px; height: 16px; padding: 0 4px; margin-left: 6px;
  1018. border-radius: 3px; background: #e24b4a; color: #fff; font-size: 10px; font-weight: 650; vertical-align: 1px;
  1019. }
  1020. .prod-st {
  1021. display: inline-flex; align-items: center; height: 22px; padding: 0 10px; border-radius: 11px;
  1022. background: #22a05d; color: #fff; font-size: 12px; font-weight: 650;
  1023. }
  1024. .prod-st.off { background: #94a3b8; }
  1025. .prod-price { color: #e24b4a; }
  1026. .th-filt {
  1027. display: inline-block; width: 11px; height: 11px; margin-left: 4px; opacity: .38; vertical-align: -1px;
  1028. background: currentColor;
  1029. mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 5h16l-6 7v6l-4 2v-8z'/%3E%3C/svg%3E") center / contain no-repeat;
  1030. }
  1031. .prod-cat-list { display: flex; flex-direction: column; gap: 4px; }
  1032. .prod-cat-list button {
  1033. display: flex; justify-content: space-between; width: 100%; text-align: left;
  1034. padding: 8px 10px; border-radius: 8px; border: 1px solid var(--line); background: #fff;
  1035. }
  1036. .prod-cat-list button.on, .prod-cat-list button:hover { border-color: var(--green-line); background: var(--green-soft); color: var(--green-dark); }
  1037. .quote-pager button:hover:not(.on) { background: #e8f6ee; }
  1038. .quote-goto { height: 28px; width: 48px; border: 1px solid var(--line); border-radius: 6px; padding: 0 6px; text-align: center; }
  1039. .erp-ico-btn {
  1040. width: 32px; height: 32px; border: 1px solid var(--line); border-radius: 8px; background: #fff;
  1041. display: grid; place-items: center; color: #5b6675;
  1042. }
  1043. .erp-ico-btn:hover { border-color: var(--green-line); color: var(--green-dark); }
  1044. .empty { color: var(--muted); padding: 28px 0; text-align: center; }
  1045. dialog.wide-dlg { max-width: min(1080px, 96vw); width: 1080px; }
  1046. dialog.exp-dlg {
  1047. max-width: min(960px, 96vw); width: 960px; border: 0; border-radius: 12px;
  1048. box-shadow: 0 18px 50px rgba(28, 36, 48, .18); overflow: hidden; background: #fff;
  1049. margin: auto;
  1050. }
  1051. dialog.exp-dlg .dlg-h {
  1052. padding: 14px 22px; border-bottom: 0; background: #e8f6ee;
  1053. }
  1054. dialog.exp-dlg .dlg-h h3 { font-size: 15px; font-weight: 650; color: var(--ink); }
  1055. dialog.exp-dlg .dlg-x { font-size: 22px; color: #6a7380; width: 32px; height: 32px; }
  1056. dialog.exp-dlg .dlg-b { max-height: 72vh; padding: 18px 24px 4px; }
  1057. dialog.exp-dlg .dlg-f { padding: 12px 24px 18px; border-top: 0; gap: 10px; }
  1058. dialog.exp-dlg .dlg-f .btn { display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 14px; }
  1059. .exp-new-top {
  1060. display: grid; grid-template-columns: 1fr 1fr 1.25fr; gap: 16px 40px; margin-bottom: 6px;
  1061. }
  1062. .exp-new-top label { display: flex; flex-direction: column; gap: 8px; font-size: 13px; color: #3d4754; font-weight: 500; }
  1063. .exp-new-top input, .exp-new-top select, .exp-field-date {
  1064. height: 38px; border: 1px solid #d8dee6; border-radius: 8px; padding: 0 12px; background: #fff; outline: none; color: var(--ink); font-size: 13px;
  1065. }
  1066. .exp-field-date {
  1067. display: flex; align-items: center; gap: 8px; padding: 0 10px; position: relative;
  1068. }
  1069. .exp-field-date svg { width: 16px; height: 16px; stroke: #8a94a0; fill: none; stroke-width: 1.8; flex: none; }
  1070. .exp-field-date input {
  1071. flex: 1; height: 36px; border: 0 !important; padding: 0 !important; background: transparent; min-width: 0;
  1072. }
  1073. .exp-field-date input::-webkit-calendar-picker-indicator {
  1074. opacity: 0; position: absolute; inset: 0; width: 100%; height: 100%; cursor: pointer;
  1075. }
  1076. .exp-new-top input:focus, .exp-new-top select:focus,
  1077. .exp-line-table input:focus, .exp-line-table select:focus, .exp-note:focus {
  1078. border-color: var(--green-line);
  1079. }
  1080. .exp-block { margin-top: 8px; padding-top: 18px; border-top: 1px dashed #e4e9ed; }
  1081. .exp-block.tight { border-top: 0; padding-top: 18px; margin-top: 4px; }
  1082. .exp-block > h4 { font-size: 13px; font-weight: 650; color: var(--ink); margin-bottom: 10px; }
  1083. .exp-block > h4 .opt { color: var(--muted); font-weight: 400; }
  1084. .exp-line-wrap { overflow: auto; border-radius: 8px; }
  1085. .exp-line-table { width: 100%; min-width: 760px; border-collapse: separate; border-spacing: 0; }
  1086. .exp-line-table th {
  1087. background: #f4f6f8; font-size: 12px; color: #5b6675; font-weight: 600;
  1088. padding: 11px 12px; border-bottom: 1px solid #eef1f4; text-align: left;
  1089. }
  1090. .exp-line-table th:first-child { border-radius: 8px 0 0 0; }
  1091. .exp-line-table th:last-child { text-align: center; border-radius: 0 8px 0 0; }
  1092. .exp-line-table td { padding: 10px 12px; vertical-align: middle; font-size: 13px; color: var(--muted); background: #fff; }
  1093. .exp-line-table td:last-child { text-align: center; }
  1094. .exp-line-table input, .exp-line-table select {
  1095. width: 100%; height: 36px; border: 1px solid #d8dee6; border-radius: 8px; padding: 0 10px; background: #fff; outline: none; color: var(--ink);
  1096. }
  1097. .exp-line-table select:invalid, .exp-line-table select.placeholder { color: #9aa3ad; }
  1098. .exp-line-table input::placeholder { color: #9aa3ad; }
  1099. .exp-datewrap { position: relative; display: flex; align-items: center; }
  1100. .exp-datewrap svg {
  1101. position: absolute; left: 10px; width: 16px; height: 16px;
  1102. stroke: #8a94a0; fill: none; stroke-width: 1.8; pointer-events: none; z-index: 1;
  1103. }
  1104. .exp-datewrap input[type="date"] { color: var(--ink); padding-left: 32px; }
  1105. .exp-datewrap:not(.has) input[type="date"] { color: transparent; }
  1106. .exp-datewrap:not(.has) input[type="date"]::-webkit-datetime-edit { color: transparent; }
  1107. .exp-datewrap input[type="date"]::-webkit-calendar-picker-indicator {
  1108. opacity: 0; position: absolute; inset: 0; width: 100%; height: 100%; cursor: pointer;
  1109. }
  1110. .exp-date-ph {
  1111. position: absolute; left: 32px; top: 50%; transform: translateY(-50%);
  1112. color: #9aa3ad; pointer-events: none; font-size: 13px;
  1113. }
  1114. .exp-datewrap.has .exp-date-ph { display: none; }
  1115. .exp-line-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 14px; gap: 12px; flex-wrap: wrap; }
  1116. .exp-line-foot .btn { height: 34px; padding: 0 12px; }
  1117. .exp-total { font-size: 13px; color: var(--ink); }
  1118. .exp-total b { color: #e24b4a; font-size: 16px; font-weight: 750; margin-left: 4px; }
  1119. .exp-inv-meta { font-size: 13px; color: var(--ink); margin-bottom: 12px; }
  1120. .exp-inv-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; min-height: 0; }
  1121. .exp-up {
  1122. width: 132px; height: 110px; border: 1px dashed #8fd0aa; border-radius: 12px;
  1123. background: #e8f6ee; display: flex; flex-direction: column; align-items: center; justify-content: center;
  1124. color: var(--green); gap: 2px; cursor: pointer; font-weight: 500; font-size: 13px;
  1125. }
  1126. .exp-up:hover { background: #def1e5; }
  1127. .exp-up input { display: none; }
  1128. .exp-up .plus { font-size: 30px; line-height: 1; color: #22a05d; font-weight: 400; }
  1129. .exp-note { width: 100%; min-height: 40px; height: 40px; border: 1px solid #d8dee6; border-radius: 8px; padding: 8px 12px; resize: vertical; outline: none; }
  1130. .btn-ico { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.8; flex: none; }
  1131. .inv-pick-list { display: flex; flex-direction: column; gap: 8px; }
  1132. .inv-pick-item {
  1133. display: flex; align-items: center; gap: 10px; padding: 10px 12px;
  1134. border: 1px solid var(--line); border-radius: 8px; background: #fff; width: 100%; text-align: left;
  1135. }
  1136. .inv-pick-item.on { border-color: var(--green); background: var(--green-soft); }
  1137. .inv-pick-item .amt { margin-left: auto; font-weight: 650; }
  1138. @media (max-width: 1100px) {
  1139. .exp-new-top { grid-template-columns: 1fr; }
  1140. }
  1141. .plate-cell { font-size: 12px; line-height: 1.45; white-space: normal; vertical-align: top; min-width: 360px; }
  1142. .plate-mini {
  1143. width: auto; min-width: 0; border-collapse: collapse; background: transparent;
  1144. }
  1145. .plate-mini th, .plate-mini td {
  1146. padding: 2px 18px 2px 0; border: 0; background: transparent !important;
  1147. white-space: nowrap; font-size: 12px; vertical-align: baseline;
  1148. }
  1149. .plate-mini th {
  1150. color: var(--muted); font-weight: 600; font-size: 11px;
  1151. }
  1152. .plate-mini td:first-child { font-weight: 650; color: var(--ink); padding-right: 22px; }
  1153. .plate-mini th:nth-child(n+2), .plate-mini td:nth-child(n+2) {
  1154. text-align: right; padding-left: 16px; padding-right: 16px;
  1155. }
  1156. .plate-mini th:last-child, .plate-mini td:last-child { padding-right: 0; }
  1157. .attach-mode #buyOcrBox, .attach-mode #buyFormCol { display: none !important; }
  1158. .attach-mode .ocr-pair, .attach-mode .split-h { grid-template-columns: 1fr; }
  1159. .trans-slot {
  1160. margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--line);
  1161. }
  1162. .trans-slot-h { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
  1163. .trans-slot-h h4 { font-size: 13px; }
  1164. .exp-board { margin-bottom: 14px; }
  1165. .exp-map {
  1166. background: #fff; border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
  1167. }
  1168. .exp-map-grid {
  1169. display: grid;
  1170. grid-template-columns: 88px repeat(5, minmax(0, 1fr));
  1171. }
  1172. .exp-map-head, .exp-map-row {
  1173. display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; align-items: stretch;
  1174. }
  1175. .exp-map-head { background: #f7f9f8; border-bottom: 1px solid #eef1f4; }
  1176. .exp-colh {
  1177. padding: 8px 6px; font-size: 11px; color: var(--muted); text-align: center;
  1178. }
  1179. .exp-colh b { color: var(--ink); font-weight: 650; margin-right: 3px; }
  1180. .exp-map-row.bx { background: linear-gradient(90deg, #eef8f2 0, #fff 88px); border-bottom: 1px dashed #e6eaee; }
  1181. .exp-map-row.sq { background: linear-gradient(90deg, #fff6eb 0, #fff 88px); }
  1182. .exp-map-lab {
  1183. display: flex; flex-direction: column; justify-content: center;
  1184. padding: 10px 12px;
  1185. }
  1186. .exp-map-lab b { font-size: 13px; font-weight: 750; color: var(--green); line-height: 1.2; }
  1187. .exp-map-row.sq .exp-map-lab b { color: #c2410c; }
  1188. .exp-map-lab span { font-size: 11px; color: var(--muted); margin-top: 2px; }
  1189. .exp-cell {
  1190. position: relative; display: flex; align-items: center; gap: 8px;
  1191. margin: 8px 8px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
  1192. background: #fff; text-align: left; cursor: pointer; min-width: 0;
  1193. }
  1194. .exp-cell:hover { border-color: var(--green-line); }
  1195. .exp-map-row.sq .exp-cell:hover { border-color: #f6c38b; }
  1196. .exp-cell.on { border-color: var(--green); background: var(--green-soft); }
  1197. .exp-map-row.sq .exp-cell.on { border-color: #ea580c; background: #fff7ed; }
  1198. .exp-cell.ghost {
  1199. border-style: dashed; background: #f4f7f5; cursor: default; color: var(--muted);
  1200. }
  1201. .exp-cell.ghost:hover { border-color: var(--line); }
  1202. .exp-cell .t { min-width: 0; flex: 1; }
  1203. .exp-cell .t b { display: block; font-size: 12.5px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  1204. .exp-cell .t em { display: block; font-style: normal; font-size: 10px; color: var(--muted); }
  1205. .exp-cell .c { flex: none; font-size: 16px; font-weight: 750; letter-spacing: -.03em; }
  1206. .exp-cell:not(.last)::after {
  1207. content: ""; position: absolute; right: -9px; top: 50%; z-index: 1; width: 10px; height: 10px;
  1208. margin-top: -5px; pointer-events: none;
  1209. background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><path d='M4 2l4 4-4 4' fill='none' stroke='%23c9d0d6' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/></svg>") center / 10px no-repeat;
  1210. }
  1211. @media (max-width: 1100px) {
  1212. .exp-map-grid { display: block; }
  1213. .exp-map-head { display: none; }
  1214. .exp-map-row { display: grid; grid-template-columns: 1fr 1fr; }
  1215. .exp-map-lab { grid-column: 1 / -1; padding-bottom: 0; }
  1216. .exp-cell:not(.last)::after { display: none; }
  1217. }
  1218. .fin-sec { margin-bottom: 18px; }
  1219. .fin-sec:last-child { margin-bottom: 0; }
  1220. .fin-sec-h {
  1221. display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 700; margin: 0 0 8px;
  1222. }
  1223. .kind-tag {
  1224. display: inline-flex; align-items: center; height: 18px; padding: 0 6px; border-radius: 4px;
  1225. font-size: 10px; font-weight: 700; margin-right: 4px; vertical-align: middle;
  1226. }
  1227. .kind-tag.bx { background: var(--green-soft); color: var(--green); }
  1228. .kind-tag.sq { background: #fff7ed; color: #c2410c; }
  1229. .exp-log { display: flex; flex-direction: column; gap: 0; }
  1230. .exp-log .row {
  1231. display: grid; grid-template-columns: 118px 88px 1fr; gap: 8px;
  1232. padding: 8px 0; border-bottom: 1px solid #eef1f4; font-size: 12px;
  1233. }
  1234. .exp-log .row:last-child { border: 0; }
  1235. .duty-bar {
  1236. display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px;
  1237. }
  1238. .duty-brand {
  1239. display: inline-flex; align-items: center; gap: 8px; padding-left: 10px;
  1240. border-left: 3px solid var(--green); font-size: 15px; font-weight: 750; color: var(--green-dark);
  1241. }
  1242. .duty-brand svg { width: 18px; height: 18px; stroke: var(--green); fill: none; stroke-width: 1.8; }
  1243. .duty-nav { display: inline-flex; align-items: center; gap: 6px; }
  1244. .duty-nav .m { font-weight: 650; min-width: 92px; text-align: center; }
  1245. .duty-ico {
  1246. width: 28px; height: 28px; border-radius: 8px; border: 1px solid var(--line); background: #fff;
  1247. display: grid; place-items: center; cursor: pointer;
  1248. }
  1249. .duty-ico svg { width: 14px; height: 14px; stroke: #4b5563; fill: none; stroke-width: 1.8; }
  1250. .duty-legend {
  1251. margin-left: auto; display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  1252. font-size: 12px; color: var(--muted);
  1253. }
  1254. .duty-legend i {
  1255. display: inline-block; width: 8px; height: 8px; border-radius: 99px; margin-right: 4px; vertical-align: 1px;
  1256. }
  1257. .duty-legend .am { background: #e67e22; }
  1258. .duty-legend .pm { background: #c45c26; }
  1259. .duty-legend .watch { background: #d94841; }
  1260. .duty-legend .other { background: #8a94a0; }
  1261. .duty-link { color: var(--green); font-weight: 500; background: none; border: 0; cursor: pointer; padding: 0; }
  1262. .duty-stats {
  1263. display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px;
  1264. }
  1265. .duty-pills { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
  1266. .duty-pill {
  1267. display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: 8px;
  1268. background: #fff; border: 1px solid var(--line); font-size: 12.5px;
  1269. }
  1270. .duty-pill i { width: 8px; height: 8px; border-radius: 99px; display: inline-block; }
  1271. .duty-pill i.am { background: #e67e22; }
  1272. .duty-pill i.pm { background: #c45c26; }
  1273. .duty-pill i.watch { background: #d94841; }
  1274. .duty-pill b { font-size: 14px; font-weight: 750; }
  1275. .duty-sum { font-size: 12.5px; color: var(--ink); }
  1276. .duty-sum b { color: var(--green); font-size: 14px; font-weight: 750; }
  1277. .duty-people { display: flex; flex-wrap: wrap; gap: 10px 16px; font-size: 12.5px; color: var(--muted); align-items: center; }
  1278. .duty-people em { font-style: normal; color: var(--ink); }
  1279. .duty-people b { color: var(--green); font-weight: 750; }
  1280. .duty-cal { background: #fff; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
  1281. .duty-week {
  1282. display: grid; grid-template-columns: repeat(7, 1fr); background: #f7f9f8; border-bottom: 1px solid var(--line);
  1283. }
  1284. .duty-week span { padding: 8px; text-align: center; font-size: 12px; color: var(--muted); font-weight: 500; }
  1285. .duty-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
  1286. .duty-day {
  1287. min-height: 124px; padding: 8px; text-align: left; border: 0; border-right: 1px solid #eef1f4; border-bottom: 1px solid #eef1f4;
  1288. background: #fff; vertical-align: top; cursor: pointer; font: inherit; color: inherit; display: block; width: 100%;
  1289. }
  1290. .duty-day:nth-child(7n) { border-right: 0; }
  1291. .duty-day.out { background: #fafbfb; color: #c5cbd1; }
  1292. .duty-day:hover { background: #f8faf9; }
  1293. .duty-day .num { font-weight: 650; margin-bottom: 6px; font-size: 13px; }
  1294. .duty-day.today .num {
  1295. width: 22px; height: 22px; border-radius: 99px; background: var(--green); color: #fff;
  1296. display: grid; place-items: center; font-size: 12px;
  1297. }
  1298. .duty-chip {
  1299. display: block; width: 100%; border-radius: 4px; padding: 3px 6px; margin-bottom: 4px;
  1300. font-size: 11px; color: #fff; text-align: left; line-height: 1.35; font-weight: 500;
  1301. }
  1302. .duty-chip.am { background: #e67e22; }
  1303. .duty-chip.pm { background: #c45c26; }
  1304. .duty-chip.watch { background: #d94841; }
  1305. .duty-chip.other { background: #6b7280; }
  1306. .duty-empty { color: #9aa3ad; font-size: 12px; }
  1307. .chip.pick { background: #fff; color: var(--ink); border: 1px solid var(--line); cursor: pointer; height: 28px; }
  1308. .chip.pick.on { background: var(--green-soft); color: var(--green-dark); border-color: var(--green); }
  1309. .duty-exist { display: flex; flex-direction: column; gap: 8px; }
  1310. .duty-exist .row {
  1311. display: flex; align-items: center; gap: 8px; font-size: 12.5px;
  1312. }
  1313. .wh-tree {
  1314. display: flex;
  1315. flex-direction: column;
  1316. gap: 16px;
  1317. margin-bottom: 16px;
  1318. }
  1319. #view-wh-info, #view-wh-mode, #view-wh-proc, #view-wh-stock {
  1320. display: flex;
  1321. flex-direction: column;
  1322. gap: 16px;
  1323. }
  1324. .wh-nav-card { overflow: visible; }
  1325. .wh-nav {
  1326. padding: 10px 14px;
  1327. display: flex;
  1328. flex-direction: column;
  1329. gap: 0;
  1330. }
  1331. .wh-nav-mains {
  1332. display: flex;
  1333. flex-wrap: wrap;
  1334. align-items: center;
  1335. gap: 8px;
  1336. }
  1337. .wh-nav-lab {
  1338. font-size: 13px; font-weight: 650; color: var(--ink);
  1339. margin-right: 4px;
  1340. }
  1341. .wh-nav-item, .wh-nav-child {
  1342. display: inline-flex; align-items: center; gap: 6px;
  1343. width: auto; text-align: left; border-radius: 99px;
  1344. padding: 6px 12px;
  1345. }
  1346. .wh-nav-item {
  1347. font-weight: 650;
  1348. background: #fff;
  1349. color: #5b6675;
  1350. border: 1.5px solid #c5ccd4;
  1351. }
  1352. .wh-nav-item.acid { color: #2a6bb3; background: #eef4fb; border-color: #b7cde6; }
  1353. .wh-nav-item.alkali { color: #c47d11; background: #fbf4e8; border-color: #e4c792; }
  1354. .wh-nav-item.hcl { color: #c45b5b; background: #fbf0f0; border-color: #e3b4b4; }
  1355. .wh-nav-item.pack { color: #7c5cbf; background: #f4f0fb; border-color: #c9b8e4; }
  1356. .wh-nav-item.solid { color: #5b6675; background: #f2f4f6; border-color: #c5ccd4; }
  1357. .wh-nav-item:hover { filter: none; }
  1358. .wh-nav-item.acid:hover { background: #e3edf8; }
  1359. .wh-nav-item.alkali:hover { background: #f7ebd6; }
  1360. .wh-nav-item.hcl:hover { background: #f6e4e4; }
  1361. .wh-nav-item.pack:hover { background: #ece4f7; }
  1362. .wh-nav-item.solid:hover { background: #e8ebef; }
  1363. .wh-nav-item.on {
  1364. color: #fff;
  1365. box-shadow: 0 2px 8px rgba(28, 36, 48, 0.18);
  1366. }
  1367. .wh-nav-item.on.acid { background: #2a6bb3; border-color: #1d4f86; }
  1368. .wh-nav-item.on.alkali { background: #c47d11; border-color: #8f5a0a; }
  1369. .wh-nav-item.on.hcl { background: #c45b5b; border-color: #8f3e3e; }
  1370. .wh-nav-item.on.pack { background: #7c5cbf; border-color: #5a3f91; }
  1371. .wh-nav-item.on.solid { background: #5b6675; border-color: #3d4754; }
  1372. .wh-nav-item.on .wh-dot { background: #fff; }
  1373. .wh-nav-child {
  1374. font-size: 12.5px;
  1375. font-weight: 500;
  1376. padding: 5px 10px;
  1377. color: #1c2430;
  1378. background: #fff;
  1379. border: 1px solid #c5ccd4;
  1380. }
  1381. .wh-nav-child:hover { background: #f5f7f8; }
  1382. .wh-nav-child.on {
  1383. background: #f3f5f7;
  1384. border-color: #8a94a0;
  1385. font-weight: 650;
  1386. }
  1387. .wh-nav-child .wh-dot { background: #8a94a0; }
  1388. .wh-nav-subs {
  1389. display: flex;
  1390. flex-wrap: wrap;
  1391. align-items: center;
  1392. gap: 6px;
  1393. margin-top: 10px;
  1394. padding-top: 10px;
  1395. border-top: 1px solid #eef1f4;
  1396. }
  1397. .wh-nav-subs-lab {
  1398. font-size: 11px; color: #8a94a0; font-weight: 600; margin-right: 2px;
  1399. }
  1400. .wh-nav-child .mini {
  1401. font-size: 10px; color: #8a94a0; font-weight: 500;
  1402. }
  1403. .wh-dot {
  1404. width: 8px; height: 8px; border-radius: 50%; flex: none;
  1405. background: currentColor;
  1406. }
  1407. .wh-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
  1408. .wh-kpi {
  1409. background: var(--white); border: 1px solid var(--line); border-radius: 10px;
  1410. padding: 12px 14px;
  1411. }
  1412. .wh-kpi .k { color: var(--muted); font-size: 12px; }
  1413. .wh-kpi .v { font-size: 22px; font-weight: 700; letter-spacing: -.03em; margin: 4px 0 2px; }
  1414. .wh-kpi .s { font-size: 11px; color: var(--muted); }
  1415. .wh-cap { height: 6px; background: #eef1f4; border-radius: 99px; overflow: hidden; margin-top: 8px; }
  1416. .wh-cap i { display: block; height: 100%; background: linear-gradient(90deg, #167c4a, #22a05d); }
  1417. .wh-cap i.warn { background: linear-gradient(90deg, #e24b4a, #f97316); }
  1418. .wh-stock-name { font-weight: 650; }
  1419. .wh-path { color: var(--muted); font-size: 11px; }
  1420. .grade {
  1421. display: inline-flex; align-items: center; height: 18px; padding: 0 6px;
  1422. border-radius: 4px; font-size: 11px; font-weight: 700;
  1423. }
  1424. .grade.ind { background: #eaf2fb; color: var(--blue); }
  1425. .grade.rg { background: #f3eefb; color: #7c5cbf; }
  1426. .grade.raw { background: var(--amber-soft); color: var(--amber); }
  1427. .grade.fin { background: var(--green-soft); color: var(--green-dark); }
  1428. .grade.ref { background: #e6f4ef; color: #0f7a5a; }
  1429. .wh-stock-cards {
  1430. display: grid;
  1431. grid-template-columns: repeat(3, 1fr);
  1432. gap: 10px;
  1433. }
  1434. .wh-mini {
  1435. cursor: pointer;
  1436. background: var(--white);
  1437. border: 1px solid var(--line);
  1438. border-radius: 10px;
  1439. padding: 12px 14px;
  1440. min-height: 0;
  1441. box-shadow: none;
  1442. transition: background .15s;
  1443. }
  1444. .wh-mini:hover { background: #f7faf8; }
  1445. .wh-mini-top {
  1446. display: flex;
  1447. align-items: center;
  1448. justify-content: space-between;
  1449. gap: 10px;
  1450. }
  1451. .wh-mini-name {
  1452. display: flex;
  1453. align-items: center;
  1454. gap: 7px;
  1455. min-width: 0;
  1456. }
  1457. .wh-mini-name i {
  1458. width: 8px;
  1459. height: 8px;
  1460. border-radius: 50%;
  1461. flex: none;
  1462. background: currentColor;
  1463. }
  1464. .wh-mini-name b {
  1465. font-size: 13px;
  1466. font-weight: 650;
  1467. color: #1c2430;
  1468. line-height: 1.3;
  1469. }
  1470. .wh-mini .qty {
  1471. flex: none;
  1472. font-size: 18px;
  1473. font-weight: 700;
  1474. letter-spacing: -.03em;
  1475. font-variant-numeric: tabular-nums;
  1476. line-height: 1;
  1477. }
  1478. .wh-mini .qty span {
  1479. font-size: 11px;
  1480. font-weight: 500;
  1481. margin-left: 3px;
  1482. opacity: .75;
  1483. }
  1484. .wh-mini.tone-ind { color: var(--blue); }
  1485. .wh-mini.tone-rg { color: #7c5cbf; }
  1486. .wh-mini.tone-hcl { color: #c45b5b; }
  1487. .wh-mini.tone-ind .qty { color: var(--blue); }
  1488. .wh-mini.tone-rg .qty { color: #7c5cbf; }
  1489. .wh-mini.tone-hcl .qty { color: #c45b5b; }
  1490. .wh-mini .tanks {
  1491. display: flex;
  1492. flex-wrap: wrap;
  1493. gap: 4px;
  1494. margin-top: 8px;
  1495. }
  1496. .wh-mini .tanks span {
  1497. display: inline-flex;
  1498. align-items: center;
  1499. height: 20px;
  1500. padding: 0 7px;
  1501. border-radius: 4px;
  1502. background: #f3f5f4;
  1503. color: #5b6675;
  1504. font-size: 11px;
  1505. font-variant-numeric: tabular-nums;
  1506. }
  1507. .wh-mini.tone-ind i,
  1508. .wh-mini.tone-rg i,
  1509. .wh-mini.tone-hcl i { background: currentColor; }
  1510. .proc-table { min-width: 720px; border-collapse: separate; border-spacing: 5px 0; }
  1511. .proc-table.wide { min-width: 1480px; border-spacing: 4px 3px; }
  1512. .proc-table thead th {
  1513. background: #1a3d73;
  1514. color: #fff;
  1515. font-weight: 600;
  1516. white-space: nowrap;
  1517. text-align: center;
  1518. padding: 9px 8px;
  1519. border-bottom: 0;
  1520. border-radius: 4px;
  1521. }
  1522. .proc-table td {
  1523. text-align: center;
  1524. white-space: nowrap;
  1525. font-variant-numeric: tabular-nums;
  1526. padding: 9px 8px;
  1527. border-bottom: 0;
  1528. }
  1529. .proc-table tfoot td {
  1530. font-weight: 700;
  1531. background: #f3f6f4;
  1532. border-radius: 4px;
  1533. }
  1534. .proc-table.wide td {
  1535. color: #243040;
  1536. font-size: 12.5px;
  1537. padding: 10px 8px;
  1538. }
  1539. .proc-table.wide tbody tr:nth-child(even) td { background: #f4f7fb; }
  1540. .proc-table.wide tbody tr:hover td { background: #e8f0fa; }
  1541. .proc-table.wide .qty-in { color: var(--green); font-weight: 700; }
  1542. .proc-table.wide .qty-out { color: var(--blue); font-weight: 700; }
  1543. .proc-table.wide .qty-bal { color: #1a3d73; font-weight: 700; }
  1544. .proc-table.wide .amt { font-weight: 650; }
  1545. .proc-table.wide .dash { color: #c2c8d0; }
  1546. .proc-table th.col-name, .proc-table td.col-name {
  1547. text-align: left;
  1548. padding-left: 12px;
  1549. min-width: 148px;
  1550. }
  1551. .wh-mode {
  1552. border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; background: #fbfefc;
  1553. }
  1554. .wh-mode h3 { font-size: 13px; margin-bottom: 8px; }
  1555. .wh-mode p { color: var(--muted); font-size: 12px; line-height: 1.55; }
  1556. .wh-proc {
  1557. display: grid; grid-template-columns: 1fr auto 1fr auto 1fr; gap: 8px;
  1558. align-items: center; margin-top: 10px;
  1559. }
  1560. .wh-proc .box {
  1561. background: #fff; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
  1562. }
  1563. .wh-proc .box b { display: block; font-size: 13px; }
  1564. .wh-proc .box span { display: block; color: var(--muted); font-size: 11px; margin-top: 4px; }
  1565. .wh-proc .arr { color: #4a7fd4; font-weight: 700; font-size: 16px; text-align: center; }
  1566. .tank-map {
  1567. display: grid; grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
  1568. gap: 8px;
  1569. }
  1570. .tank-card {
  1571. border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px; background: #fff;
  1572. min-height: 74px;
  1573. }
  1574. .tank-card .id { font-weight: 750; font-size: 13px; letter-spacing: 0.02em; }
  1575. .tank-card .p { color: var(--muted); font-size: 11px; margin: 3px 0 6px; line-height: 1.35; }
  1576. .tank-card .q { font-size: 11px; font-variant-numeric: tabular-nums; font-weight: 650; }
  1577. .tank-card.sulfur { border-color: #f3ddb0; background: #fffdf6; }
  1578. .tank-group { margin-top: 10px; }
  1579. .tank-group .tg-lab { font-size: 11px; color: var(--muted); margin-bottom: 4px; font-weight: 650; }
  1580. .tank-group .wh-tank { margin-top: 0; }
  1581. .wh-prod-row { display: flex; justify-content: space-between; gap: 8px; padding: 8px 0; border-bottom: 1px solid #eef1f4; font-size: 12px; }
  1582. .wh-prod-row:last-child { border: 0; }
  1583. .calc-box { background: #f7fbf8; border: 1px dashed var(--green-line); border-radius: 10px; padding: 12px; margin-top: 10px; font-size: 12px; }
  1584. .calc-box b { font-weight: 650; }
  1585. .tank-use { display: flex; align-items: center; gap: 8px; }
  1586. .tank-use .bar { flex: 1; height: 6px; background: #eef1f4; border-radius: 99px; overflow: hidden; min-width: 64px; }
  1587. .tank-use .bar i { display: block; height: 100%; background: linear-gradient(90deg, #167c4a, #22a05d); }
  1588. .tank-use .bar i.warn { background: linear-gradient(90deg, #e24b4a, #f97316); }
  1589. .print-slip { max-width: 720px; margin: 0 auto; padding: 8px 4px; }
  1590. .print-slip h1 { font-size: 18px; text-align: center; margin-bottom: 4px; }
  1591. .print-slip .sub { text-align: center; color: var(--muted); font-size: 12px; margin-bottom: 16px; }
  1592. .print-slip table { width: 100%; }
  1593. .print-slip th, .print-slip td { border-bottom: 1px solid var(--line); padding: 8px 6px; }
  1594. .print-sign { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; margin-top: 28px; font-size: 12px; color: var(--muted); }
  1595. .wh-dir { width: 100%; }
  1596. .op-chip {
  1597. display: inline-flex; align-items: center; height: 20px; padding: 0 7px; margin: 0 4px 4px 0;
  1598. border-radius: 4px; font-size: 11px; font-weight: 650;
  1599. background: var(--green-soft); color: var(--green-dark);
  1600. }
  1601. .op-chip.out { background: #eaf2fb; color: var(--blue); }
  1602. .op-chip.mix { background: #fff6e5; color: var(--amber); }
  1603. .wh-tree-box {
  1604. display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;
  1605. }
  1606. .wh-branch {
  1607. border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: #fff;
  1608. }
  1609. .wh-branch h4 { font-size: 12px; color: var(--muted); margin-bottom: 8px; font-weight: 650; }
  1610. .wh-branch button {
  1611. display: flex; justify-content: space-between; width: 100%;
  1612. padding: 6px 0; border-bottom: 1px solid #eef1f4; text-align: left; font-size: 12.5px;
  1613. }
  1614. .wh-branch button:last-child { border: 0; }
  1615. .wh-branch button:hover { color: var(--green-dark); }
  1616. @media (max-width: 1100px) {
  1617. .wh-info-grid, .wh-proc, .wh-tree-box, .wh-stock-cards { grid-template-columns: 1fr; }
  1618. }
  1619. @media (max-width: 1100px) {
  1620. .app { grid-template-columns: 1fr; }
  1621. .stats, .stats.five, .grid, .quick, .form, .apply, .ocr-pair, .split-h, .sheet, .wh-tree, .wh-kpis { grid-template-columns: 1fr; }
  1622. .cert-duo { grid-template-columns: minmax(0, 1fr) minmax(0, 3fr); }
  1623. .sidebar, .search { display: none; }
  1624. .form .span2 { grid-column: auto; }
  1625. }
  1626. #detailStats { grid-template-columns: repeat(4, 1fr); }
  1627. </style>
  1628. </head>
  1629. <body>
  1630. <div class="app">
  1631. <aside class="sidebar">
  1632. <div class="brand">
  1633. <div class="logo" aria-hidden="true">峡</div>
  1634. <div>
  1635. <strong>山峡代工</strong>
  1636. <span class="sub">一体化平台</span>
  1637. </div>
  1638. </div>
  1639. <nav class="nav">
  1640. <div class="nav-label">工作台</div>
  1641. <button class="side-item" type="button" data-page="fav">
  1642. <svg viewBox="0 0 24 24"><path d="M12 4 4 19h16L12 4z"/><path d="M12 10v4M12 16h.01"/></svg>
  1643. 收藏
  1644. </button>
  1645. <button class="side-item" type="button" data-page="todo">
  1646. <svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="M8 9h8M8 13h5"/></svg>
  1647. 待办列表
  1648. </button>
  1649. <div class="nav-group" data-group="crm">
  1650. <button class="side-item" type="button" data-toggle="crm">
  1651. <svg viewBox="0 0 24 24"><circle cx="9" cy="8" r="3"/><path d="M4 19c.5-3 2.8-5 5-5s4.5 2 5 5"/><circle cx="17" cy="9" r="2.4"/><path d="M16 19c.3-2 1.6-3.4 3-3.8"/></svg>
  1652. CRM管理
  1653. <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
  1654. </button>
  1655. <div class="nav-sub">
  1656. <button class="side-item side-sub" type="button" data-mod="product">产品管理</button>
  1657. <button class="side-item side-sub" type="button" data-mod="quote">报价单管理 <span class="badge-n" id="quoteBadge">0</span></button>
  1658. <button class="side-item side-sub" type="button" data-mod="contract">合同管理 <span class="badge-n" id="ctBadge">0</span></button>
  1659. <button class="side-item side-sub" type="button" data-page="crm-customer">客户管理</button>
  1660. </div>
  1661. </div>
  1662. <div class="nav-group open" data-group="transport">
  1663. <button class="side-item" type="button" data-toggle="transport">
  1664. <svg viewBox="0 0 24 24"><path d="M3 16h13l5-6H12"/><path d="M5 16V8h7"/><circle cx="7" cy="18" r="2"/><circle cx="16" cy="18" r="2"/></svg>
  1665. 运输管理
  1666. <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
  1667. </button>
  1668. <div class="nav-sub">
  1669. <button class="side-item side-sub active" type="button" data-mod="plan">运输计划 <span class="badge-n" id="planBadge">0</span></button>
  1670. <button class="side-item side-sub" type="button" data-mod="archive">运输归档 <span class="badge-n" id="archiveBadge">0</span></button>
  1671. <button class="side-item side-sub" type="button" data-mod="task">任务单管理 <span class="badge-n" id="taskBadge">0</span></button>
  1672. <button class="side-item side-sub" type="button" data-mod="cert">购买证管理 <span class="badge-n" id="certBadge">0</span></button>
  1673. <button class="side-item side-sub" type="button" data-page="weigh">地磅管理</button>
  1674. <button class="side-item side-sub" type="button" data-page="freight">运费结算</button>
  1675. </div>
  1676. </div>
  1677. <div class="nav-group" data-group="fleet">
  1678. <button class="side-item" type="button" data-toggle="fleet">
  1679. <svg viewBox="0 0 24 24"><path d="M3 16h13l5-6H12"/><path d="M5 16V8h7"/><circle cx="7" cy="18" r="2"/><circle cx="16" cy="18" r="2"/></svg>
  1680. 车辆管理
  1681. <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
  1682. </button>
  1683. <div class="nav-sub">
  1684. <button class="side-item side-sub" type="button" data-mod="fleet" data-fleet-view="etc">ETC充值卡管理</button>
  1685. <button class="side-item side-sub" type="button" data-mod="fleet" data-fleet-view="fuel">加油登记管理</button>
  1686. <button class="side-item side-sub" type="button" data-mod="fleet" data-fleet-view="oilcard">加油充值卡管理</button>
  1687. <button class="side-item side-sub" type="button" data-mod="fleet" data-fleet-view="maint">车辆维保管理</button>
  1688. </div>
  1689. </div>
  1690. <div class="nav-group" data-group="warehouse">
  1691. <button class="side-item" type="button" data-toggle="warehouse">
  1692. <svg viewBox="0 0 24 24"><path d="M3 10h18v9H3zM5 10V7l3-3h8l3 3v3"/></svg>
  1693. 仓储管理
  1694. <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
  1695. </button>
  1696. <div class="nav-sub">
  1697. <button class="side-item side-sub" type="button" data-mod="warehouse" data-wh-view="info">库存信息表</button>
  1698. <button class="side-item side-sub" type="button" data-mod="warehouse" data-wh-view="mode">仓库信息表</button>
  1699. <button class="side-item side-sub" type="button" data-mod="warehouse" data-wh-view="proc">加工单明细表</button>
  1700. <button class="side-item side-sub" type="button" data-mod="warehouse" data-wh-view="stock">库存成本明细表</button>
  1701. </div>
  1702. </div>
  1703. <div class="nav-group" data-group="finance">
  1704. <button class="side-item" type="button" data-toggle="finance">
  1705. <svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="M8 9h8M8 13h5"/></svg>
  1706. 财务管理
  1707. <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
  1708. </button>
  1709. <div class="nav-sub">
  1710. <button class="side-item side-sub" type="button" data-page="fin-ar">应收应付</button>
  1711. </div>
  1712. </div>
  1713. <div class="nav-group" data-group="hr">
  1714. <button class="side-item" type="button" data-toggle="hr">
  1715. <svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="3"/><path d="M5 19c.6-3.2 3.2-5 7-5s6.4 1.8 7 5"/></svg>
  1716. 人事管理
  1717. <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
  1718. </button>
  1719. <div class="nav-sub">
  1720. <button class="side-item side-sub" type="button" data-mod="expense">报销管理 <span class="badge-n" id="expBadge">0</span></button>
  1721. <button class="side-item side-sub" type="button" data-mod="duty">值班管理</button>
  1722. <button class="side-item side-sub" type="button" data-mod="staff">员工档案</button>
  1723. </div>
  1724. </div>
  1725. <div class="nav-group" data-group="system">
  1726. <button class="side-item" type="button" data-toggle="system">
  1727. <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a7.8 7.8 0 0 0 .1-6l-2.1.4M6.6 9a7.8 7.8 0 0 0-.1 6l2.1-.4M9 4.6a7.8 7.8 0 0 1 6 0l-.4 2.1M15 19.4a7.8 7.8 0 0 1-6 0l.4-2.1"/></svg>
  1728. 系统管理
  1729. <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
  1730. </button>
  1731. <div class="nav-sub">
  1732. <button class="side-item side-sub" type="button" data-page="sys-dict">字典管理</button>
  1733. </div>
  1734. </div>
  1735. </nav>
  1736. </aside>
  1737. <section class="main">
  1738. <header class="topbar">
  1739. <div class="crumb">工作台 / <b id="crumbMod">运输管理</b><span id="crumbTail"> / <span id="crumb">运输计划</span></span></div>
  1740. <label class="search">
  1741. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#8a94a0" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
  1742. <input class="top-search" id="globalSearch" placeholder="搜索车牌、单号、证号" />
  1743. </label>
  1744. <div class="user">
  1745. <div class="avatar">王</div>
  1746. 调度员
  1747. </div>
  1748. </header>
  1749. <div class="page">
  1750. <div id="mod-placeholder" class="hidden">
  1751. <div class="page-head">
  1752. <div>
  1753. <h1 id="phTitle">功能建设中</h1>
  1754. <p id="phDesc">该模块尚未接入本演示,菜单结构已按一体化平台一级 / 二级导航铺好。</p>
  1755. </div>
  1756. </div>
  1757. <div class="coming">
  1758. <h2 id="phCardTitle">敬请期待</h2>
  1759. <p id="phComing">当前演示重点在运输计划与购买证管理。</p>
  1760. </div>
  1761. </div>
  1762. <div id="mod-product" class="hidden">
  1763. <section class="erp-page">
  1764. <div class="prod-search-row">
  1765. <label class="quote-search prod-search">
  1766. <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
  1767. <input id="prodQ" type="search" placeholder="产品名称/编号/拼音首字母" oninput="prodPage=1;renderProducts()" />
  1768. </label>
  1769. <div class="prod-search-actions">
  1770. <button class="erp-ico-btn" type="button" title="刷新" onclick="prodPage=1;renderProducts()">↻</button>
  1771. <button class="erp-ico-btn" type="button" title="列设置" onclick="toast('演示:列设置')">☰</button>
  1772. </div>
  1773. </div>
  1774. <div class="erp-toolbar">
  1775. <button class="btn btn-primary" type="button" onclick="openProdDlg()">+ 新增产品</button>
  1776. <button class="btn" type="button" onclick="editSelectedProd()">
  1777. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M4 20h4l11-11-4-4L4 16v4z"/><path d="m13 7 4 4"/></svg>
  1778. 编辑
  1779. </button>
  1780. <button class="btn" type="button" onclick="deleteProds()">
  1781. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M5 7h14M9 7V5h6v2M8 7l1 12h6l1-12"/></svg>
  1782. 删除
  1783. </button>
  1784. <button class="btn" type="button" onclick="shelfProds('上架')">
  1785. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 19V7M8 11l4-4 4 4"/></svg>
  1786. 批量上架
  1787. </button>
  1788. <button class="btn" type="button" onclick="shelfProds('下架')">
  1789. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 5v12M8 13l4 4 4-4"/></svg>
  1790. 批量下架
  1791. </button>
  1792. <button class="btn" type="button" onclick="toast('演示:已打开导入模板')">
  1793. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 16V6M8 10l4-4 4 4"/><path d="M5 18h14"/></svg>
  1794. 导入
  1795. </button>
  1796. <div class="erp-drop" id="prodExportDrop">
  1797. <button class="btn" type="button" onclick="toggleProdExport(event)">
  1798. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 4v10M8 10l4 4 4-4M5 18h14"/></svg>
  1799. 导出
  1800. </button>
  1801. <div class="erp-drop-menu">
  1802. <button type="button" onclick="exportProducts('page')">导出当前页</button>
  1803. <button type="button" onclick="exportProducts('all')">导出全部</button>
  1804. </div>
  1805. </div>
  1806. <button class="btn" type="button" onclick="openProdCatDlg()">
  1807. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M4 6h7l2 2h7v10H4z"/></svg>
  1808. 产品分类
  1809. </button>
  1810. </div>
  1811. <article class="card erp-card">
  1812. <div class="erp-table-wrap" id="prodTable"></div>
  1813. <div class="plan-foot">
  1814. <span class="muted" id="prodSelHint">已选 0 项</span>
  1815. <span class="quote-pager" id="prodPager"></span>
  1816. </div>
  1817. </article>
  1818. </section>
  1819. </div>
  1820. <div id="mod-quote" class="hidden">
  1821. <section class="erp-page">
  1822. <div class="stats" id="quoteStats"></div>
  1823. <div class="erp-tabs-row">
  1824. <div class="plan-tabs" id="quoteTabs">
  1825. <button class="on" type="button" data-tab="全部">全部</button>
  1826. <button type="button" data-tab="昨日">昨日</button>
  1827. <button type="button" data-tab="今日">今日</button>
  1828. <button type="button" data-tab="明日">明日</button>
  1829. <button type="button" data-tab="本周">本周</button>
  1830. </div>
  1831. <label class="quote-search">
  1832. <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
  1833. <input id="quoteQ" type="search" placeholder="报价主题/客户名称" oninput="quotePage=1;renderQuotes()" />
  1834. </label>
  1835. </div>
  1836. <div class="erp-toolbar">
  1837. <button class="btn btn-primary" type="button" onclick="openQuoteDlg()">+ 新增</button>
  1838. <button class="btn" type="button" onclick="copyQuote()">
  1839. <svg class="btn-ico" viewBox="0 0 24 24"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M4 16V6a2 2 0 0 1 2-2h10"/></svg>
  1840. 复制新增
  1841. </button>
  1842. <button class="btn" type="button" onclick="submitQuotes()">
  1843. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M6 4h9l5 5v11H6z"/><path d="M14 4v5h5"/></svg>
  1844. 提交审批
  1845. </button>
  1846. <button class="btn" type="button" onclick="genQuotePdf()">
  1847. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M8 7V4h8v3M8 17H6a2 2 0 0 1-2-2V9h16v6a2 2 0 0 1-2 2h-2"/><rect x="8" y="14" width="8" height="6" rx="1"/></svg>
  1848. 生成报价单
  1849. </button>
  1850. <button class="btn" type="button" onclick="voidQuotes()">
  1851. <svg class="btn-ico" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><path d="M8 12h8"/></svg>
  1852. 作废
  1853. </button>
  1854. <button class="btn" type="button" onclick="toast('演示:已导出当前列表')">
  1855. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 4v10M8 10l4 4 4-4M5 18h14"/></svg>
  1856. 导出
  1857. </button>
  1858. <div class="right">
  1859. <button class="erp-ico-btn" type="button" title="刷新" onclick="renderQuotes()">↻</button>
  1860. <button class="erp-ico-btn" type="button" title="列设置" onclick="toast('演示:列设置')">☰</button>
  1861. </div>
  1862. </div>
  1863. <article class="card erp-card">
  1864. <div class="erp-table-wrap" id="quoteTable"></div>
  1865. <div class="plan-foot">
  1866. <span class="muted" id="quoteSelHint">已选 0 项</span>
  1867. <span class="quote-pager" id="quotePager"></span>
  1868. </div>
  1869. </article>
  1870. </section>
  1871. </div>
  1872. <div id="mod-contract" class="hidden">
  1873. <section class="erp-page">
  1874. <div class="stats" id="ctStats"></div>
  1875. <div class="erp-tabs-row">
  1876. <div class="plan-tabs" id="ctTabs">
  1877. <button class="on" type="button" data-tab="全部">全部</button>
  1878. <button type="button" data-tab="昨日">昨日</button>
  1879. <button type="button" data-tab="今日">今日</button>
  1880. <button type="button" data-tab="明日">明日</button>
  1881. <button type="button" data-tab="本周">本周</button>
  1882. </div>
  1883. <label class="quote-search wide">
  1884. <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
  1885. <input id="ctQ" type="search" placeholder="合同编号/合同名称/客户名称" oninput="ctPage=1;renderContracts()" />
  1886. </label>
  1887. </div>
  1888. <div class="erp-toolbar">
  1889. <button class="btn btn-primary" type="button" onclick="openCtDlg()">+ 新增</button>
  1890. <button class="btn" type="button" onclick="copyContract()">
  1891. <svg class="btn-ico" viewBox="0 0 24 24"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M4 16V6a2 2 0 0 1 2-2h10"/></svg>
  1892. 合同复制
  1893. </button>
  1894. <button class="btn" type="button" onclick="submitContracts()">
  1895. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M6 4h9l5 5v11H6z"/><path d="M14 4v5h5"/></svg>
  1896. 提交审批
  1897. </button>
  1898. <button class="btn" type="button" onclick="openCtScan()">
  1899. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 16V6M8 10l4-4 4 4"/><rect x="4" y="16" width="16" height="4" rx="1"/></svg>
  1900. 上传扫描件
  1901. </button>
  1902. <div class="erp-drop" id="ctExportDrop">
  1903. <button class="btn" type="button" onclick="toggleCtExport(event)">
  1904. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 4v10M8 10l4 4 4-4M5 18h14"/></svg>
  1905. 导出
  1906. </button>
  1907. <div class="erp-drop-menu">
  1908. <button type="button" onclick="exportContracts('page')">导出当前页</button>
  1909. <button type="button" onclick="exportContracts('all')">导出全部</button>
  1910. </div>
  1911. </div>
  1912. <div class="right">
  1913. <button class="erp-ico-btn" type="button" title="刷新" onclick="renderContracts()">↻</button>
  1914. <button class="erp-ico-btn" type="button" title="列设置" onclick="toast('演示:列设置')">☰</button>
  1915. </div>
  1916. </div>
  1917. <article class="card erp-card">
  1918. <div class="erp-table-wrap" id="ctTable"></div>
  1919. <div class="plan-foot">
  1920. <span class="muted" id="ctSelHint">已选 0 项</span>
  1921. <span class="quote-pager" id="ctPager"></span>
  1922. </div>
  1923. </article>
  1924. </section>
  1925. </div>
  1926. <div id="mod-fleet" class="hidden">
  1927. <nav class="subnav" id="subnav">
  1928. <button class="on" type="button" data-view="dash">总览</button>
  1929. <button type="button" data-view="fleet">车辆台账</button>
  1930. <button type="button" data-view="fuel">加油登记管理</button>
  1931. <button type="button" data-view="oilcard">加油充值卡管理</button>
  1932. <button type="button" data-view="fuelwarn">油耗预警</button>
  1933. <button type="button" data-view="etc">ETC 申请登记</button>
  1934. <button type="button" data-view="maint">维修保养</button>
  1935. <button type="button" data-view="stats">统计</button>
  1936. </nav>
  1937. <section id="view-dash">
  1938. <div class="page-head">
  1939. <div>
  1940. <h1>危化品车辆作业</h1>
  1941. <p>公司自有危货车辆的加油、ETC 申请登记、维修保养,以及油耗与费用统计。</p>
  1942. </div>
  1943. <div class="actions">
  1944. <button class="btn" type="button" onclick="showView('fuel')">登记加油</button>
  1945. <button class="btn" type="button" onclick="showView('etc')">ETC 申请</button>
  1946. <button class="btn btn-primary" type="button" onclick="showView('maint')">登记维保</button>
  1947. </div>
  1948. </div>
  1949. <div class="alerts" id="dashAlerts"></div>
  1950. <div class="stats" id="dashStats"></div>
  1951. <div class="grid">
  1952. <div class="stack">
  1953. <article class="card">
  1954. <div class="card-h"><h2>日常登记</h2><span style="color:var(--muted);font-size:12px">同一车辆贯穿三类业务</span></div>
  1955. <div class="card-b">
  1956. <div class="quick">
  1957. <button class="qbtn" type="button" onclick="showView('fuel')"><div class="ico">油</div><h3>加油</h3><p>油品、升数、金额、里程、附件</p></button>
  1958. <button class="qbtn" type="button" onclick="showView('fuelwarn')"><div class="ico">警</div><h3>油耗预警</h3><p>超过 0.33 L/km 即预警</p></button>
  1959. <button class="qbtn" type="button" onclick="showView('etc')"><div class="ico">E</div><h3>ETC 申请</h3><p>自有车牌、期初公里、附件</p></button>
  1960. <button class="qbtn" type="button" onclick="showView('maint')"><div class="ico">保</div><h3>维修保养</h3><p>进厂、项目、费用、下次保养</p></button>
  1961. </div>
  1962. </div>
  1963. </article>
  1964. <article class="card">
  1965. <div class="card-h"><h2>车辆运行状态</h2><button class="btn" type="button" onclick="showView('fleet')">台账</button></div>
  1966. <div class="card-b" style="padding-top:4px"><div id="dashFleet"></div></div>
  1967. </article>
  1968. </div>
  1969. <div class="stack">
  1970. <article class="card">
  1971. <div class="card-h"><h2>最近登记</h2></div>
  1972. <div class="card-b"><div class="timeline" id="dashTl"></div></div>
  1973. </article>
  1974. <article class="card">
  1975. <div class="card-h"><h2>本月单车油耗(升)</h2><button class="btn" type="button" onclick="showView('fuelwarn')">油耗预警</button></div>
  1976. <div class="card-b"><div class="bars" id="dashBars"></div></div>
  1977. </article>
  1978. </div>
  1979. </div>
  1980. </section>
  1981. <section id="view-fleet" class="hidden">
  1982. <div class="page-head">
  1983. <div><h1>车辆台账</h1><p>危货牵引车 / 罐车基础信息与资质到期。</p></div>
  1984. </div>
  1985. <div class="filters">
  1986. <input id="fleetQ" placeholder="车牌 / 驾驶员" oninput="renderFleet()" />
  1987. <select id="fleetSt" onchange="renderFleet()">
  1988. <option value="">全部状态</option>
  1989. <option>运行</option>
  1990. <option>维保中</option>
  1991. <option>停运</option>
  1992. </select>
  1993. </div>
  1994. <article class="card"><div class="card-b" style="overflow:auto" id="fleetTable"></div></article>
  1995. </section>
  1996. <section id="view-fuel" class="hidden">
  1997. <div class="page-head">
  1998. <div><h1>加油登记管理</h1><p>按自有车辆查看当月加油汇总,点击「加油登记」录入小票、升数、金额与里程。</p></div>
  1999. <div class="actions">
  2000. <button class="btn btn-primary" type="button" onclick="openFuelDlg()">加油登记</button>
  2001. </div>
  2002. </div>
  2003. <div class="fuel-plate-grid" id="fuelPlateCards"></div>
  2004. <article class="card"><div class="card-h"><h2>加油记录</h2></div><div class="card-b" id="fuelTable"></div></article>
  2005. </section>
  2006. <section id="view-fuelwarn" class="hidden">
  2007. <div class="page-head">
  2008. <div>
  2009. <h1>油耗预警</h1>
  2010. <p>按相邻两次加油核对每公里油耗。标准:33 L / 100 km = <b>0.33 L/km</b>,超过即预警。</p>
  2011. </div>
  2012. </div>
  2013. <div class="stats" id="fuelwarnKpis"></div>
  2014. <div class="filters">
  2015. <select id="fuelwarnSt" onchange="renderFuelWarn()">
  2016. <option value="">全部区间</option>
  2017. <option value="预警">仅超标预警</option>
  2018. <option value="正常">仅正常</option>
  2019. <option value="不足">数据不足 / 异常</option>
  2020. </select>
  2021. <input id="fuelwarnQ" placeholder="车牌" oninput="renderFuelWarn()" />
  2022. </div>
  2023. <article class="card" style="margin-bottom:12px">
  2024. <div class="card-h"><h2>单车油耗对照(L/km)</h2><span style="color:var(--muted);font-size:12px">红线为 0.33</span></div>
  2025. <div class="card-b"><div class="bars" id="fuelwarnBars"></div></div>
  2026. </article>
  2027. <article class="card">
  2028. <div class="card-h"><h2>区间明细</h2></div>
  2029. <div class="card-b" id="fuelwarnTable"></div>
  2030. </article>
  2031. </section>
  2032. <section id="view-etc" class="hidden">
  2033. <div class="page-head">
  2034. <div><h1>ETC充值卡管理</h1><p>按自有车辆查看当月 ETC 充值次数、公里与金额,点击卡片或「新增申请」登记。</p></div>
  2035. <div class="actions">
  2036. <button class="btn btn-primary" type="button" onclick="openEtcDlg()">新增申请</button>
  2037. </div>
  2038. </div>
  2039. <div class="fuel-plate-grid" id="etcPlateCards"></div>
  2040. <article class="card"><div class="card-h"><h2>申请记录</h2></div><div class="card-b" id="etcTable"></div></article>
  2041. </section>
  2042. <section id="view-oilcard" class="hidden">
  2043. <div class="page-head">
  2044. <div><h1>加油充值卡管理</h1><p>按自有车辆查看当月油卡充值次数、公里与金额,点击卡片或「新增申请」登记。</p></div>
  2045. <div class="actions">
  2046. <button class="btn btn-primary" type="button" onclick="openOilCardDlg()">新增申请</button>
  2047. </div>
  2048. </div>
  2049. <div class="fuel-plate-grid" id="oilCardPlateCards"></div>
  2050. <article class="card"><div class="card-h"><h2>申请记录</h2></div><div class="card-b" id="oilCardTable"></div></article>
  2051. </section>
  2052. <section id="view-maint" class="hidden">
  2053. <div class="page-head">
  2054. <div><h1>车辆维保管理</h1><p>按自有车辆查看本年度维修次数、维修金额,以及本月 / 本年度合计。点击「新增申请」弹出表单登记。</p></div>
  2055. <div class="actions">
  2056. <button class="btn btn-primary" type="button" onclick="openMaintDlg()">新增申请</button>
  2057. </div>
  2058. </div>
  2059. <div class="fuel-plate-grid" id="maintPlateCards"></div>
  2060. <article class="card"><div class="card-h"><h2>维保记录</h2></div><div class="card-b" id="maintTable"></div></article>
  2061. </section>
  2062. <section id="view-stats" class="hidden">
  2063. <div class="page-head">
  2064. <div><h1>统计</h1><p>本月油耗、维保费用与 ETC 申请笔数,辅助单车成本对照。</p></div>
  2065. </div>
  2066. <div class="stats" id="statKpis"></div>
  2067. <div class="grid">
  2068. <article class="card">
  2069. <div class="card-h"><h2>单车加油量</h2></div>
  2070. <div class="card-b"><div class="bars" id="statBars"></div></div>
  2071. </article>
  2072. <article class="card">
  2073. <div class="card-h"><h2>本月费用构成</h2></div>
  2074. <div class="card-b">
  2075. <div class="pie-wrap">
  2076. <svg width="140" height="140" viewBox="0 0 32 32" id="pie"></svg>
  2077. <div class="legend" id="pieLegend"></div>
  2078. </div>
  2079. </div>
  2080. </article>
  2081. </div>
  2082. <article class="card" style="margin-top:12px">
  2083. <div class="card-h"><h2>资质到期一览</h2></div>
  2084. <div class="card-b" id="certTable"></div>
  2085. </article>
  2086. </section>
  2087. </div>
  2088. <div id="mod-plan" class="hidden">
  2089. <section id="view-plan" class="erp-page">
  2090. <div class="alerts" id="planAlerts"></div>
  2091. <div class="rel-flow">易制毒 / 易制爆计划须先 <em>上传购买证及运输证</em>,再在证件页 <em>人工点击关联对应计划</em>。系统按购买证、运输证数量计算各车运输计划量;计划完成后扣减已完成量;购买证或运输证余量 ≤ 32 吨则预警。</div>
  2092. <div class="stats" id="planKpis"></div>
  2093. <div class="erp-tabs-row">
  2094. <div class="plan-tabs" id="planTabs">
  2095. <button type="button" data-tab="待安排">待安排</button>
  2096. <button type="button" data-tab="已安排">已安排</button>
  2097. <button type="button" data-tab="草稿中">草稿中</button>
  2098. <button class="on" type="button" data-tab="全部">全部</button>
  2099. <button type="button" data-tab="入库单">入库单</button>
  2100. <button type="button" data-tab="出库单">出库单</button>
  2101. <button type="button" data-tab="直销单">直销单</button>
  2102. </div>
  2103. </div>
  2104. <div class="erp-tabs-row plan-action-row">
  2105. <div class="plan-tabs" id="planDateTabs">
  2106. <button class="on" type="button" data-tab="全部">全部</button>
  2107. <button type="button" data-tab="昨日">昨日</button>
  2108. <button type="button" data-tab="今日">今日</button>
  2109. <button type="button" data-tab="明日">明日</button>
  2110. <button type="button" data-tab="本周">本周</button>
  2111. </div>
  2112. <label class="quote-search plan-search">
  2113. <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
  2114. <input id="planQ" type="search" placeholder="计划编号/供方/需方/产品名称/车牌号" oninput="renderPlans()" />
  2115. </label>
  2116. </div>
  2117. <div class="erp-toolbar plan-toolbar">
  2118. <button class="btn btn-primary" type="button" onclick="openPlanDlg()">+ 新增计划</button>
  2119. <button class="btn" type="button" onclick="copyPlan()">
  2120. <svg class="btn-ico" viewBox="0 0 24 24"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M4 16V6a2 2 0 0 1 2-2h10"/></svg>
  2121. 复制
  2122. </button>
  2123. <button class="btn" type="button" onclick="openPlanTaskDlg()">
  2124. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M8 6h12M8 12h12M8 18h8"/><path d="M4 6h.01M4 12h.01M4 18h.01"/></svg>
  2125. 下发任务单
  2126. </button>
  2127. <button class="btn" type="button" onclick="printPlanDocs()">
  2128. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M8 7V4h8v3M8 17H6a2 2 0 0 1-2-2V9h16v6a2 2 0 0 1-2 2h-2"/><rect x="8" y="14" width="8" height="6" rx="1"/></svg>
  2129. 打印单据
  2130. </button>
  2131. <button class="btn" type="button" onclick="voidSelectedPlans()">
  2132. <svg class="btn-ico" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><path d="M8 12h8"/></svg>
  2133. 作废
  2134. </button>
  2135. <button class="btn btn-danger" type="button" onclick="createTriParty()">三方人车快速创建</button>
  2136. <button class="btn" type="button" onclick="toast('演示:已导出当前列表')">
  2137. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 4v10M8 10l4 4 4-4M5 18h14"/></svg>
  2138. 导出
  2139. </button>
  2140. <button class="btn" type="button" onclick="goLinkBuyTransCert()">去关联购买&amp;运输证</button>
  2141. </div>
  2142. <article class="card erp-card">
  2143. <div class="erp-table-wrap" id="planTable"></div>
  2144. <div class="plan-foot">
  2145. <span class="muted" id="planCount"></span>
  2146. <span class="muted">搜索合计 
  2147. <select class="page-size" id="planPageSize" onchange="renderPlans()">
  2148. <option>20条/页</option>
  2149. <option>50条/页</option>
  2150. </select>
  2151. </span>
  2152. </div>
  2153. </article>
  2154. </section>
  2155. </div>
  2156. <div id="mod-archive" class="hidden">
  2157. <section id="view-archive" class="erp-page">
  2158. <div class="page-head">
  2159. <div>
  2160. <h1>运输归档</h1>
  2161. <p>运输计划完成后进入本页待审。人工审核通过后归档:出库单 / 入库单按品名自动入对应仓库;直销单无中转仓库,不记库存。</p>
  2162. </div>
  2163. <div class="actions">
  2164. <button class="btn" type="button" onclick="showMod('plan')">返回运输计划</button>
  2165. <button class="btn btn-primary" type="button" onclick="showMod('warehouse')">查看仓储</button>
  2166. </div>
  2167. </div>
  2168. <div class="stats" id="archiveStats"></div>
  2169. <div class="erp-tabs-row">
  2170. <div class="plan-tabs" id="archiveTabs">
  2171. <button class="on" type="button" data-tab="待审核">待审核</button>
  2172. <button type="button" data-tab="已归档">已归档</button>
  2173. <button type="button" data-tab="全部">全部</button>
  2174. </div>
  2175. <div class="erp-hint" id="archiveHintBar"></div>
  2176. </div>
  2177. <div class="erp-toolbar">
  2178. <button class="btn btn-primary" type="button" onclick="batchReviewArchive()">审核归档</button>
  2179. <button class="btn" type="button" onclick="toast('演示:打印 / 导出')">打印 / 导出</button>
  2180. <input id="archiveQ" type="search" placeholder="单号 / 品名 / 客户 / 车牌" oninput="renderArchive()" />
  2181. <select id="archiveType" onchange="renderArchive()">
  2182. <option value="">全部类型</option>
  2183. <option value="出库单">出库单</option>
  2184. <option value="入直单">入库单</option>
  2185. <option value="直销单">直销单</option>
  2186. </select>
  2187. <select id="archiveWh" onchange="renderArchive()">
  2188. <option value="">全部仓库</option>
  2189. </select>
  2190. </div>
  2191. <article class="card erp-card">
  2192. <div class="erp-table-wrap" id="archiveTable"></div>
  2193. <div class="plan-foot">
  2194. <span class="muted" id="archiveCount"></span>
  2195. <span class="muted">出库 / 入库按品名入仓 直销无中转仓</span>
  2196. </div>
  2197. </article>
  2198. </section>
  2199. </div>
  2200. <div id="mod-task" class="hidden">
  2201. <section id="view-task" class="erp-page">
  2202. <div class="page-head">
  2203. <div>
  2204. <h1>任务单管理</h1>
  2205. <p>运输计划下发给现场后进入本页。现场人员接收任务后可回传完成:稀释、精制、分装。</p>
  2206. </div>
  2207. <div class="actions">
  2208. <button class="btn" type="button" onclick="showMod('plan')">返回运输计划</button>
  2209. <button class="btn btn-primary" type="button" onclick="showMod('plan');toast('勾选运输计划后点「下发任务单」')">从计划下发</button>
  2210. </div>
  2211. </div>
  2212. <div class="stats" id="taskStats"></div>
  2213. <div class="erp-tabs-row">
  2214. <div class="plan-tabs" id="taskTabs">
  2215. <button class="on" type="button" data-tab="全部">全部</button>
  2216. <button type="button" data-tab="稀释">稀释任务单</button>
  2217. <button type="button" data-tab="精制">精制任务单</button>
  2218. <button type="button" data-tab="分装">分装任务单</button>
  2219. <button type="button" data-tab="待执行">待执行</button>
  2220. <button type="button" data-tab="已完成">已完成</button>
  2221. </div>
  2222. <div class="erp-hint" id="taskHint"></div>
  2223. </div>
  2224. <div class="erp-toolbar">
  2225. <button class="btn" type="button" onclick="batchReceiveTask()">接收任务</button>
  2226. <button class="btn btn-primary" type="button" onclick="batchReportTask()">回传完成</button>
  2227. <span class="split"></span>
  2228. <input id="taskQ" type="search" placeholder="单号 / 品名 / 执行人 / 计划号" oninput="renderTasks()" />
  2229. <select id="taskSt" onchange="renderTasks()">
  2230. <option value="">全部状态</option>
  2231. <option>待执行</option>
  2232. <option>执行中</option>
  2233. <option>已完成</option>
  2234. </select>
  2235. </div>
  2236. <article class="card erp-card">
  2237. <div class="erp-table-wrap" id="taskTable"></div>
  2238. <div class="plan-foot">
  2239. <span class="muted" id="taskCount"></span>
  2240. <span class="muted">现场接收后回传完成</span>
  2241. </div>
  2242. </article>
  2243. </section>
  2244. </div>
  2245. <div id="mod-cert" class="hidden">
  2246. <nav class="subnav" id="certSubnav">
  2247. <button class="on" type="button" data-view="wait">待关联计划</button>
  2248. <button type="button" data-view="list">证件列表</button>
  2249. <button type="button" data-view="detail" id="navDetail" disabled>证件信息</button>
  2250. </nav>
  2251. <section id="view-cert-wait">
  2252. <div class="page-head">
  2253. <div>
  2254. <h1>待关联计划</h1>
  2255. <p>已安排、易制毒且已选车号的运输计划,在此关联购买证和运输证。</p>
  2256. </div>
  2257. </div>
  2258. <div class="stats three" id="waitStats"></div>
  2259. <article class="card wait-link-box" id="certWaitCard">
  2260. <div class="card-h">
  2261. <h2>待关联计划</h2>
  2262. </div>
  2263. <div class="card-b" id="certWaitPlans"></div>
  2264. </article>
  2265. </section>
  2266. <section id="view-cert-list" class="hidden">
  2267. <div class="page-head">
  2268. <div>
  2269. <h1>购买证 / 运输证</h1>
  2270. <p>流程固定三步:先上传购买证和运输证,再人工点击关联对应计划;系统按两证数量计算各车运输计划量,计划完成后扣减已完成量;购买证或运输证余量 ≤ 自设预警量时预警(默认核准总量的 10%,可改)。</p>
  2271. </div>
  2272. <div class="actions">
  2273. <button class="btn btn-primary" type="button" onclick="openCertDlg(false)">上传购买证及运输证</button>
  2274. </div>
  2275. </div>
  2276. <div class="cert-steps hidden" id="certSteps"></div>
  2277. <div class="alerts hidden" id="listAlerts"></div>
  2278. <div class="stats" id="listStats"></div>
  2279. <div class="filters">
  2280. <input id="certQ" placeholder="证号 / 品名 / 购买单位 / 销售单位 / 车号" oninput="renderCertList()" />
  2281. <select id="certSt" onchange="renderCertList()">
  2282. <option value="">全部状态</option>
  2283. <option>有余量</option>
  2284. <option>余量预警</option>
  2285. <option>运输证预警</option>
  2286. <option>已用尽</option>
  2287. <option>即将到期</option>
  2288. <option>超证</option>
  2289. <option>车辆预警</option>
  2290. <option>已核销</option>
  2291. </select>
  2292. <button class="btn" type="button" onclick="openCertDlg(false)">上传购买证及运输证</button>
  2293. </div>
  2294. <article class="card">
  2295. <div class="card-h"><h2>购买证及运输证</h2><span class="muted" id="listCount"></span></div>
  2296. <div class="card-b" style="overflow:auto" id="certListTable"></div>
  2297. </article>
  2298. </section>
  2299. <section id="view-cert-detail" class="hidden">
  2300. <div class="page-head">
  2301. <div>
  2302. <h1 id="detailTitle">证件信息</h1>
  2303. <p id="detailSub">由购买证与运输证识别合并生成</p>
  2304. </div>
  2305. <div class="actions">
  2306. <button class="btn" type="button" onclick="showCertView('list')">返回列表</button>
  2307. <button class="btn" type="button" onclick="openCertLinkPlanDlg(currentCertId)">关联计划</button>
  2308. <button class="btn btn-primary" type="button" onclick="openCertDlg(false, currentCertId)">追加运输证</button>
  2309. </div>
  2310. </div>
  2311. <div class="rel-flow" id="certRelHint"></div>
  2312. <div class="stats" id="detailStats"></div>
  2313. <article class="card" id="certWarnQtyCard" style="margin-top:12px"></article>
  2314. <div class="alerts" id="detailWarns"></div>
  2315. <article class="card" style="margin-top:12px">
  2316. <div class="card-h">
  2317. <h2>关联车辆计划量预警</h2>
  2318. </div>
  2319. <div class="card-b veh-board">
  2320. <div class="qty-formula" id="certQtyFormula"></div>
  2321. <div id="certVehicleWarn"></div>
  2322. </div>
  2323. </article>
  2324. <div id="detailSheet"></div>
  2325. <article class="card" style="margin-top:12px">
  2326. <div class="card-h">
  2327. <h2>已关联运输计划</h2>
  2328. </div>
  2329. <div class="card-b" id="certLinkedPlans"></div>
  2330. </article>
  2331. <article class="card" style="margin-top:12px">
  2332. <div class="card-h">
  2333. <h2>按运输证分派车辆</h2>
  2334. <div class="actions">
  2335. <button class="btn btn-primary" type="button" onclick="openCertDlg(false, currentCertId)">追加运输证</button>
  2336. </div>
  2337. </div>
  2338. <div class="card-b">
  2339. <p class="muted" id="dispatchHint" style="margin-bottom:10px"></p>
  2340. <div id="detailDispatch"></div>
  2341. </div>
  2342. </article>
  2343. </section>
  2344. </div>
  2345. <div id="mod-expense" class="hidden">
  2346. <section id="view-expense" class="erp-page">
  2347. <div class="page-head">
  2348. <div>
  2349. <h1>报销单据管理</h1>
  2350. </div>
  2351. <div class="actions">
  2352. <button class="btn" type="button" onclick="openPreExpDlg()">备用金申请</button>
  2353. <button class="btn btn-primary" type="button" onclick="openExpenseDlg()">填报报销单</button>
  2354. </div>
  2355. </div>
  2356. <div class="exp-board" id="expFlow"></div>
  2357. <div class="alerts" id="expAlerts"></div>
  2358. <div class="erp-tabs-row">
  2359. <div class="plan-tabs" id="expTabs">
  2360. <button class="on" type="button" data-tab="全部">全部</button>
  2361. <button type="button" data-tab="我的报销">我的报销</button>
  2362. <button type="button" data-tab="备用金申请">我的备用金</button>
  2363. <button type="button" data-tab="待财务初审">财务初审</button>
  2364. <button type="button" data-tab="待终审">领导审核</button>
  2365. <button type="button" data-tab="待付款">出纳付款</button>
  2366. <button type="button" data-tab="待归档">凭证归档</button>
  2367. <button type="button" data-tab="借款">借款 / 冲销</button>
  2368. </div>
  2369. <div class="erp-hint" id="expHint"></div>
  2370. </div>
  2371. <div class="erp-toolbar" id="expToolbar"></div>
  2372. <article class="card erp-card">
  2373. <div class="erp-table-wrap" id="expTable"></div>
  2374. <div class="plan-foot">
  2375. <span class="muted" id="expCount"></span>
  2376. <span class="muted" id="expFootHint">报销:财务核票 → 领导审核 → 出纳付款 备用金:财务核额 → 先支出 → 事后报销冲销</span>
  2377. </div>
  2378. </article>
  2379. </section>
  2380. </div>
  2381. <div id="mod-duty" class="hidden">
  2382. <section id="view-duty" class="erp-page">
  2383. <div class="duty-bar">
  2384. <div class="duty-brand">
  2385. <svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="15" rx="2"/><path d="M8 3v4M16 3v4M4 10h16"/></svg>
  2386. 值班排班
  2387. </div>
  2388. <div class="duty-nav">
  2389. <button class="duty-ico" type="button" onclick="shiftDutyMonth(-1)" aria-label="上一月"><svg viewBox="0 0 24 24"><path d="M14 6 8 12l6 6"/></svg></button>
  2390. <div class="m" id="dutyMonthLab">2026年10月</div>
  2391. <button class="duty-ico" type="button" onclick="shiftDutyMonth(1)" aria-label="下一月"><svg viewBox="0 0 24 24"><path d="m10 6 6 6-6 6"/></svg></button>
  2392. </div>
  2393. <button class="btn" type="button" onclick="goDutyToday()">今天</button>
  2394. <div class="duty-legend">
  2395. <span><i class="am"></i>早班</span>
  2396. <span><i class="pm"></i>晚班</span>
  2397. <span><i class="watch"></i>值班</span>
  2398. <span><i class="other"></i>其他</span>
  2399. <span>点击日期配置</span>
  2400. <button class="duty-link" type="button" onclick="copyDutyLink()">复制链接</button>
  2401. </div>
  2402. </div>
  2403. <div class="duty-stats">
  2404. <div class="duty-pills" id="dutyPills"></div>
  2405. <div class="duty-people" id="dutyPeople"></div>
  2406. </div>
  2407. <div class="duty-cal">
  2408. <div class="duty-week">
  2409. <span>日</span><span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span>
  2410. </div>
  2411. <div class="duty-grid" id="dutyGrid"></div>
  2412. </div>
  2413. </section>
  2414. </div>
  2415. <div id="mod-staff" class="hidden">
  2416. <section id="view-staff" class="erp-page">
  2417. <div class="stats" id="staffStats"></div>
  2418. <div class="erp-tabs-row">
  2419. <div class="plan-tabs" id="staffTabs">
  2420. <button class="on" type="button" data-tab="全部" onclick="setStaffTab('全部')">全部</button>
  2421. <button type="button" data-tab="在职" onclick="setStaffTab('在职')">在职</button>
  2422. <button type="button" data-tab="试用" onclick="setStaffTab('试用')">试用</button>
  2423. <button type="button" data-tab="离职" onclick="setStaffTab('离职')">离职</button>
  2424. </div>
  2425. <select class="staff-dept-sel" id="staffDept" onchange="staffPage=1;renderStaff()">
  2426. <option value="">全部部门</option>
  2427. </select>
  2428. <label class="quote-search wide">
  2429. <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
  2430. <input id="staffQ" type="search" placeholder="姓名 / 工号 / 岗位 / 手机" oninput="staffPage=1;renderStaff()" />
  2431. </label>
  2432. </div>
  2433. <div class="erp-toolbar">
  2434. <button class="btn btn-primary" type="button" onclick="openStaffDlg()">+ 新增员工</button>
  2435. <button class="btn" type="button" onclick="editSelectedStaff()">
  2436. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M4 20h4l11-11-4-4L4 16v4z"/><path d="m13 7 4 4"/></svg>
  2437. 编辑
  2438. </button>
  2439. <button class="btn" type="button" onclick="leaveSelectedStaff()">
  2440. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M15 8v8M9 12h12M9 12l3-3M9 12l3 3M5 5v14"/></svg>
  2441. 办理离职
  2442. </button>
  2443. <button class="btn" type="button" onclick="toast('演示:已导出员工档案')">
  2444. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 4v10M8 10l4 4 4-4M5 18h14"/></svg>
  2445. 导出
  2446. </button>
  2447. <div class="right">
  2448. <button class="erp-ico-btn" type="button" title="刷新" onclick="staffPage=1;renderStaff()">↻</button>
  2449. <button class="erp-ico-btn" type="button" title="列设置" onclick="toast('演示:列设置')">☰</button>
  2450. </div>
  2451. </div>
  2452. <article class="card erp-card">
  2453. <div class="erp-table-wrap" id="staffTable"></div>
  2454. <div class="plan-foot">
  2455. <span class="muted" id="staffSelHint">已选 0 项</span>
  2456. <span class="quote-pager" id="staffPager"></span>
  2457. </div>
  2458. </article>
  2459. </section>
  2460. </div>
  2461. <div id="mod-warehouse" class="hidden">
  2462. <nav class="subnav" id="whSubnav">
  2463. <button class="on" type="button" data-view="info" onclick="showWhView('info')">库存信息表</button>
  2464. <button type="button" data-view="mode" onclick="showWhView('mode')">仓库信息表</button>
  2465. <button type="button" data-view="proc" onclick="showWhView('proc')">加工单明细表</button>
  2466. <button type="button" data-view="stock" onclick="showWhView('stock')">库存成本明细表</button>
  2467. </nav>
  2468. <div class="page-head">
  2469. <div>
  2470. <h1 id="whPageTitle">库存信息表</h1>
  2471. <p id="whPageDesc">98% 工业硫酸可直销、加水稀释。试剂硫酸单独成线。工业 / 试剂分罐。</p>
  2472. </div>
  2473. <div class="actions" id="whPageActions"></div>
  2474. </div>
  2475. <div class="wh-tree">
  2476. <article class="card wh-nav-card">
  2477. <div class="wh-nav" id="whNav"></div>
  2478. </article>
  2479. <div class="stack">
  2480. <div id="view-wh-info"></div>
  2481. <div id="view-wh-mode" class="hidden"></div>
  2482. <div id="view-wh-proc" class="hidden"></div>
  2483. <div id="view-wh-stock" class="hidden">
  2484. <div class="wh-kpis" id="whKpis"></div>
  2485. <article class="card" id="whLedgerCard">
  2486. <div class="card-h">
  2487. <h2 id="whTableTitle">库存成本明细账</h2>
  2488. <span class="muted" id="whTableCount"></span>
  2489. </div>
  2490. <div class="filters" style="padding: 10px 14px 0">
  2491. <input id="whQ" placeholder="单号 / 物料 / 往来单位 / 单据类型" oninput="renderWhTable()" />
  2492. <select id="whDocFilter" onchange="onWhDocFilter()">
  2493. <option value="">全部单据</option>
  2494. <option>进货单</option>
  2495. <option>产成品入库单</option>
  2496. <option>其他入库单</option>
  2497. <option>调拨单</option>
  2498. <option>组装单</option>
  2499. <option>形态转换单</option>
  2500. <option>拆卸单</option>
  2501. <option>盘点单</option>
  2502. <option>销货单</option>
  2503. <option>材料出库单</option>
  2504. <option>其他出库单</option>
  2505. </select>
  2506. </div>
  2507. <div class="card-b" style="overflow:auto" id="whTable"></div>
  2508. </article>
  2509. </div>
  2510. </div>
  2511. </div>
  2512. </div>
  2513. </div>
  2514. </section>
  2515. </div>
  2516. <div class="toast" id="toast">已保存</div>
  2517. <dialog class="apply-dlg" id="prodDialog">
  2518. <div class="dlg-h green">
  2519. <h3 id="prodDlgTitle">新增产品</h3>
  2520. <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('prodDialog').close()">×</button>
  2521. </div>
  2522. <form id="prodForm" onsubmit="return false">
  2523. <input type="hidden" name="id" />
  2524. <div class="dlg-b">
  2525. <div class="apply">
  2526. <div class="row"><div class="lab"><span class="star">*</span>产品名称</div><input name="name" required placeholder="如 工业硫酸" /></div>
  2527. <div class="row"><div class="lab">产品编号</div><input name="no" placeholder="留空则自动生成" /></div>
  2528. <div class="row"><div class="lab">产品分类</div>
  2529. <select name="cat" id="prodCatSelect"></select>
  2530. </div>
  2531. <div class="row"><div class="lab">品牌</div><input name="brand" placeholder="如 益泉" /></div>
  2532. <div class="row"><div class="lab">别名</div><input name="alias" /></div>
  2533. <div class="row"><div class="lab">规格型号</div><input name="spec" placeholder="如 98%" /></div>
  2534. <div class="row"><div class="lab">标准价(元)</div><input name="price" type="number" step="0.01" min="0" placeholder="选填" /></div>
  2535. <div class="row"><div class="lab">拼音首字母</div><input name="py" placeholder="如 GYSF" /></div>
  2536. <div class="row"><div class="lab">危化标识</div>
  2537. <select name="hz"><option value="">无</option><option>毒</option><option>爆</option></select>
  2538. </div>
  2539. <div class="row"><div class="lab">状态</div>
  2540. <select name="status"><option>上架</option><option>下架</option></select>
  2541. </div>
  2542. </div>
  2543. </div>
  2544. <div class="dlg-f">
  2545. <button class="btn" type="button" onclick="document.getElementById('prodDialog').close()">取消</button>
  2546. <button class="btn btn-primary" type="button" onclick="saveProd()">保存</button>
  2547. </div>
  2548. </form>
  2549. </dialog>
  2550. <dialog class="apply-dlg" id="prodCatDialog" style="width:480px;max-width:92vw">
  2551. <div class="dlg-h green">
  2552. <h3>产品分类</h3>
  2553. <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('prodCatDialog').close()">×</button>
  2554. </div>
  2555. <div class="dlg-b">
  2556. <div class="prod-cat-list" id="prodCatList"></div>
  2557. <div style="display:flex;gap:8px;margin-top:12px">
  2558. <input id="prodCatNew" placeholder="新分类名称" style="flex:1;height:32px;border:1px solid var(--line);border-radius:8px;padding:0 10px" />
  2559. <button class="btn btn-primary" type="button" onclick="addProdCat()">新增</button>
  2560. </div>
  2561. </div>
  2562. <div class="dlg-f">
  2563. <button class="btn" type="button" onclick="document.getElementById('prodCatDialog').close()">关闭</button>
  2564. </div>
  2565. </dialog>
  2566. <dialog class="apply-dlg" id="quoteDialog">
  2567. <div class="dlg-h green">
  2568. <h3 id="quoteDlgTitle">新增报价单</h3>
  2569. <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('quoteDialog').close()">×</button>
  2570. </div>
  2571. <form id="quoteForm" onsubmit="return false">
  2572. <div class="dlg-b">
  2573. <div class="apply">
  2574. <div class="row"><div class="lab"><span class="star">*</span>客户名称</div><input name="customer" required placeholder="如 安庆汇辰科技有限公司" /></div>
  2575. <div class="row"><div class="lab"><span class="star">*</span>报价主题</div><input name="title" required placeholder="如 山峡-安庆汇辰" /></div>
  2576. <div class="row"><div class="lab">报价类型</div>
  2577. <select name="kind"><option>销售</option><option>代工</option><option>询价</option></select>
  2578. </div>
  2579. <div class="row"><div class="lab">报价日期</div><input name="date" type="date" required /></div>
  2580. <div class="row"><div class="lab"><span class="star">*</span>产品信息</div><input name="product" required placeholder="如 片碱 2吨" /></div>
  2581. <div class="row"><div class="lab"><span class="star">*</span>产品金额</div><input name="prodAmt" type="number" step="0.01" min="0" required /></div>
  2582. <div class="row"><div class="lab">运费</div><input name="freight" type="number" step="0.01" min="0" value="0" /></div>
  2583. </div>
  2584. </div>
  2585. <div class="dlg-f">
  2586. <button class="btn" type="button" onclick="document.getElementById('quoteDialog').close()">取消</button>
  2587. <button class="btn btn-primary" type="button" onclick="saveQuote()">保存草稿</button>
  2588. </div>
  2589. </form>
  2590. </dialog>
  2591. <dialog class="apply-dlg" id="ctDialog">
  2592. <div class="dlg-h green">
  2593. <h3 id="ctDlgTitle">新增合同</h3>
  2594. <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('ctDialog').close()">×</button>
  2595. </div>
  2596. <form id="ctForm" onsubmit="return false">
  2597. <div class="dlg-b">
  2598. <div class="apply">
  2599. <div class="row"><div class="lab"><span class="star">*</span>客户名称</div><input name="customer" required placeholder="如 安庆汇辰科技有限公司" /></div>
  2600. <div class="row"><div class="lab"><span class="star">*</span>合同名称</div><input name="title" required placeholder="如 山峡-安庆汇辰" /></div>
  2601. <div class="row"><div class="lab">合同类型</div>
  2602. <select name="kind"><option>销售合同</option><option>框架销售合同</option><option>采购合同</option></select>
  2603. </div>
  2604. <div class="row"><div class="lab">签订日期</div><input name="signDate" type="date" required /></div>
  2605. <div class="row"><div class="lab"><span class="star">*</span>产品信息</div><input name="product" required placeholder="如 片碱 2吨" /></div>
  2606. <div class="row"><div class="lab"><span class="star">*</span>合同金额</div><input name="amount" type="number" step="0.01" min="0" required /></div>
  2607. <div class="row"><div class="lab">运费</div><input name="freight" type="number" step="0.01" min="0" value="0" /></div>
  2608. <div class="row"><div class="lab">桶押金</div><input name="deposit" type="number" step="0.01" min="0" value="0" /></div>
  2609. <div class="row"><div class="lab">负责人</div><input name="owner" value="万金强" /></div>
  2610. </div>
  2611. </div>
  2612. <div class="dlg-f">
  2613. <button class="btn" type="button" onclick="document.getElementById('ctDialog').close()">取消</button>
  2614. <button class="btn btn-primary" type="button" onclick="saveContract()">保存草稿</button>
  2615. </div>
  2616. </form>
  2617. </dialog>
  2618. <dialog class="apply-dlg" id="ctScanDialog">
  2619. <div class="dlg-h green">
  2620. <h3>上传扫描件</h3>
  2621. <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('ctScanDialog').close()">×</button>
  2622. </div>
  2623. <form id="ctScanForm" onsubmit="return false">
  2624. <div class="dlg-b">
  2625. <p class="muted" id="ctScanHint" style="margin-bottom:12px">为所选合同上传双章 / 单章扫描件。</p>
  2626. <div class="apply">
  2627. <div class="row"><div class="lab">附件类型</div>
  2628. <select name="att"><option>双章</option><option>单章</option></select>
  2629. </div>
  2630. <div class="row"><div class="lab"><span class="star">*</span>扫描件</div>
  2631. <input name="file" type="file" accept="application/pdf,image/*" required />
  2632. </div>
  2633. </div>
  2634. </div>
  2635. <div class="dlg-f">
  2636. <button class="btn" type="button" onclick="document.getElementById('ctScanDialog').close()">取消</button>
  2637. <button class="btn btn-primary" type="button" onclick="saveCtScan()">保存</button>
  2638. </div>
  2639. </form>
  2640. </dialog>
  2641. <dialog class="exp-dlg" id="expenseDialog">
  2642. <div class="dlg-h green">
  2643. <h3 id="expDlgTitle">新增报销申请</h3>
  2644. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeExpenseDlg()">×</button>
  2645. </div>
  2646. <form id="expenseForm" onsubmit="return false">
  2647. <div class="dlg-b">
  2648. <div class="exp-new-top">
  2649. <label>申请人
  2650. <select name="user" required onchange="onExpUserChange()"></select>
  2651. </label>
  2652. <label>报销日期
  2653. <div class="exp-field-date">
  2654. <svg viewBox="0 0 24 24"><rect x="4" y="6" width="16" height="14" rx="2"/><path d="M8 4v4M16 4v4M4 11h16"/></svg>
  2655. <input name="date" type="date" required onchange="syncExpTitle()" />
  2656. </div>
  2657. </label>
  2658. <label>报销标题
  2659. <input name="title" required placeholder="自动生成,可修改" />
  2660. </label>
  2661. </div>
  2662. <div class="exp-block tight">
  2663. <h4>报销明细</h4>
  2664. <div class="exp-line-wrap">
  2665. <table class="exp-line-table">
  2666. <thead>
  2667. <tr>
  2668. <th style="width:52px">序号</th>
  2669. <th>费用项目 <span class="star">*</span></th>
  2670. <th style="width:120px">金额(元) <span class="star">*</span></th>
  2671. <th>报销事由 <span class="star">*</span></th>
  2672. <th style="width:150px">发生时间</th>
  2673. </tr>
  2674. </thead>
  2675. <tbody id="expLineBody"></tbody>
  2676. </table>
  2677. </div>
  2678. <div class="exp-line-foot">
  2679. <button class="btn" type="button" onclick="addExpLine()">+ 新增明细行</button>
  2680. <div class="exp-total">合计金额(元):<b id="expLineSum">¥0.00</b></div>
  2681. </div>
  2682. </div>
  2683. <div class="exp-block">
  2684. <h4>关联发票 <span class="opt">(可选)</span></h4>
  2685. <div class="exp-inv-meta">当前发票余额:<b id="expInvBal">¥0.00</b> ;本次合计:<b id="expInvSum">¥0.00</b></div>
  2686. <div class="exp-inv-chips" id="expInvChips"></div>
  2687. <button class="btn" type="button" onclick="openInvPick()">
  2688. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M4 7h5l2 2h9v10H4z"/></svg>
  2689. 从发票夹选择
  2690. </button>
  2691. </div>
  2692. <div class="exp-block">
  2693. <h4>申请附件 <span class="opt">(可选)</span></h4>
  2694. <label class="exp-up">
  2695. <input name="attach" type="file" multiple accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.zip,.rar" onchange="previewExpFiles(this)" />
  2696. <span class="plus">+</span>
  2697. <span>上传附件</span>
  2698. </label>
  2699. <div class="chips" id="expFileChips" style="margin-top:8px"></div>
  2700. <p class="hint">补充行程单、说明截图、合同等;与发票夹无关。支持图片、视频、PDF、Office、压缩包等。</p>
  2701. </div>
  2702. <div class="exp-block">
  2703. <h4>备注说明</h4>
  2704. <textarea class="exp-note" name="remark" placeholder="请输入报销说明"></textarea>
  2705. </div>
  2706. </div>
  2707. <div class="dlg-f">
  2708. <button class="btn" type="button" onclick="closeExpenseDlg()">× 取消</button>
  2709. <button class="btn" type="button" onclick="saveExpense(false)">
  2710. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M6 4h9l5 5v11H6z"/><path d="M14 4v5h5M8 13h8M8 17h6"/></svg>
  2711. 保存草稿
  2712. </button>
  2713. <button class="btn btn-primary" type="button" onclick="saveExpense(true)">
  2714. <svg class="btn-ico" viewBox="0 0 24 24"><path d="M4 12 20 4l-6 16-2-7z"/></svg>
  2715. 提交审批
  2716. </button>
  2717. </div>
  2718. </form>
  2719. </dialog>
  2720. <dialog class="apply-dlg" id="invPickDialog">
  2721. <div class="dlg-h green">
  2722. <h3>从发票夹选择</h3>
  2723. <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('invPickDialog').close()">×</button>
  2724. </div>
  2725. <div class="dlg-b">
  2726. <p class="muted" style="margin-bottom:10px">勾选未核销发票,关联到本张报销单。可选。</p>
  2727. <div class="inv-pick-list" id="invPickList"></div>
  2728. </div>
  2729. <div class="dlg-f">
  2730. <button class="btn" type="button" onclick="document.getElementById('invPickDialog').close()">取消</button>
  2731. <button class="btn btn-primary" type="button" onclick="confirmInvPick()">确认选择</button>
  2732. </div>
  2733. </dialog>
  2734. <dialog class="apply-dlg" id="preExpDialog">
  2735. <div class="dlg-h green">
  2736. <h3>备用金申请</h3>
  2737. <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('preExpDialog').close()">×</button>
  2738. </div>
  2739. <form id="preExpForm" onsubmit="return false">
  2740. <div class="dlg-b">
  2741. <p class="muted" style="margin-bottom:12px">事前申请,无需发票。财务核额通过后再支出,事后凭票报销冲销。与报销单(事后贴票结算)不是同一条流程。</p>
  2742. <div class="apply">
  2743. <div class="row"><div class="lab"><span class="star">*</span>申请人</div><input name="user" required value="周建军" /></div>
  2744. <div class="row"><div class="lab"><span class="star">*</span>部门</div>
  2745. <select name="dept" required>
  2746. <option>运输部</option><option>生产部</option><option>行政人事</option><option>销售部</option><option>仓储部</option>
  2747. </select>
  2748. </div>
  2749. <div class="row"><div class="lab"><span class="star">*</span>事项</div>
  2750. <select name="type" required>
  2751. <option>出差</option><option>采购</option><option>招待</option><option>其他</option>
  2752. </select>
  2753. </div>
  2754. <div class="row"><div class="lab"><span class="star">*</span>预估金额</div><input name="amount" type="number" min="0" step="0.01" required /></div>
  2755. <div class="row full"><div class="lab"><span class="star">*</span>申请事由</div><textarea name="remark" required placeholder="为何支出、预计发生时间"></textarea></div>
  2756. </div>
  2757. </div>
  2758. <div class="dlg-f">
  2759. <button class="btn" type="button" onclick="document.getElementById('preExpDialog').close()">取消</button>
  2760. <button class="btn btn-primary" type="button" onclick="savePreExp()">提交申请</button>
  2761. </div>
  2762. </form>
  2763. </dialog>
  2764. <dialog class="wide-dlg" id="expDetailDialog">
  2765. <div class="dlg-h green">
  2766. <h3 id="expDetailTitle">报销单</h3>
  2767. <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('expDetailDialog').close()">×</button>
  2768. </div>
  2769. <div class="dlg-b" id="expDetailBody"></div>
  2770. <div class="dlg-f" id="expDetailFoot"></div>
  2771. </dialog>
  2772. <dialog class="apply-dlg" id="expRejectDlg">
  2773. <div class="dlg-h">
  2774. <h3>驳回 / 退回补资料</h3>
  2775. <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('expRejectDlg').close()">×</button>
  2776. </div>
  2777. <form id="expRejectForm" onsubmit="return false">
  2778. <div class="dlg-b">
  2779. <div class="apply">
  2780. <div class="row full"><div class="lab"><span class="star">*</span>原因</div>
  2781. <textarea name="reason" required placeholder="须写明原因,便于经办人补资料后重新发起"></textarea>
  2782. </div>
  2783. </div>
  2784. </div>
  2785. <div class="dlg-f">
  2786. <button class="btn" type="button" onclick="document.getElementById('expRejectDlg').close()">取消</button>
  2787. <button class="btn btn-primary" type="button" onclick="confirmExpReject()">确认退回</button>
  2788. </div>
  2789. </form>
  2790. </dialog>
  2791. <dialog class="apply-dlg" id="dutyDialog" style="width:520px;max-width:94vw">
  2792. <div class="dlg-h green">
  2793. <h3 id="dutyDlgTitle">值班配置</h3>
  2794. <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('dutyDialog').close()">×</button>
  2795. </div>
  2796. <form id="dutyForm" onsubmit="return false">
  2797. <div class="dlg-b">
  2798. <div class="apply">
  2799. <div class="row"><div class="lab">班次</div>
  2800. <select name="type">
  2801. <option value="am">早班</option>
  2802. <option value="pm">晚班</option>
  2803. <option value="watch">值班</option>
  2804. <option value="other">其他</option>
  2805. </select>
  2806. </div>
  2807. <div class="row"><div class="lab">岗位</div>
  2808. <input name="post" value="配酸" />
  2809. </div>
  2810. <div class="row full"><div class="lab">人员</div>
  2811. <div id="dutyStaffPick" class="chips" style="gap:8px"></div>
  2812. </div>
  2813. </div>
  2814. <div id="dutyExist" style="margin-top:12px"></div>
  2815. </div>
  2816. <div class="dlg-f">
  2817. <button class="btn" type="button" onclick="document.getElementById('dutyDialog').close()">取消</button>
  2818. <button class="btn btn-primary" type="button" onclick="saveDutyShift()">保存排班</button>
  2819. </div>
  2820. </form>
  2821. </dialog>
  2822. <dialog class="apply-dlg" id="staffDialog">
  2823. <div class="dlg-h green">
  2824. <h3 id="staffDlgTitle">新增员工</h3>
  2825. <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('staffDialog').close()">×</button>
  2826. </div>
  2827. <form id="staffForm" onsubmit="return false">
  2828. <input type="hidden" name="id" />
  2829. <div class="dlg-b">
  2830. <div class="apply">
  2831. <div class="row"><div class="lab"><span class="star">*</span>姓名</div><input name="name" required placeholder="如 吴九华" /></div>
  2832. <div class="row"><div class="lab">工号</div><input name="no" placeholder="留空则自动生成" /></div>
  2833. <div class="row"><div class="lab">性别</div>
  2834. <select name="gender"><option>男</option><option>女</option></select>
  2835. </div>
  2836. <div class="row"><div class="lab"><span class="star">*</span>部门</div>
  2837. <select name="dept" id="staffDeptSelect"></select>
  2838. </div>
  2839. <div class="row"><div class="lab"><span class="star">*</span>岗位</div><input name="post" required placeholder="如 驾驶员" /></div>
  2840. <div class="row"><div class="lab">手机</div><input name="phone" placeholder="11 位手机号" /></div>
  2841. <div class="row"><div class="lab">入职日期</div><input name="hire" type="date" /></div>
  2842. <div class="row"><div class="lab">合同到期</div><input name="contractEnd" type="date" /></div>
  2843. <div class="row"><div class="lab">用工类型</div>
  2844. <select name="kind"><option>劳动合同</option><option>劳务合同</option><option>实习</option></select>
  2845. </div>
  2846. <div class="row"><div class="lab">状态</div>
  2847. <select name="status"><option>在职</option><option>试用</option><option>离职</option></select>
  2848. </div>
  2849. <div class="row"><div class="lab">学历</div>
  2850. <select name="edu"><option>本科</option><option>大专</option><option>高中</option><option>中专</option><option>初中</option></select>
  2851. </div>
  2852. <div class="row"><div class="lab">籍贯</div><input name="native" placeholder="如 安徽安庆" /></div>
  2853. <div class="row"><div class="lab">紧急联系人</div><input name="emergency" /></div>
  2854. <div class="row"><div class="lab">联系人电话</div><input name="emergPhone" /></div>
  2855. <div class="row full"><div class="lab">备注</div><textarea name="remark" placeholder="证件、资质、特殊说明等"></textarea></div>
  2856. </div>
  2857. </div>
  2858. <div class="dlg-f">
  2859. <button class="btn" type="button" onclick="document.getElementById('staffDialog').close()">取消</button>
  2860. <button class="btn btn-primary" type="button" onclick="saveStaff()">保存</button>
  2861. </div>
  2862. </form>
  2863. </dialog>
  2864. <dialog class="apply-dlg" id="staffDetailDialog">
  2865. <div class="dlg-h green">
  2866. <h3 id="staffDetailTitle">员工档案</h3>
  2867. <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('staffDetailDialog').close()">×</button>
  2868. </div>
  2869. <div class="dlg-b" id="staffDetailBody"></div>
  2870. <div class="dlg-f">
  2871. <button class="btn" type="button" onclick="document.getElementById('staffDetailDialog').close()">关闭</button>
  2872. <button class="btn btn-primary" type="button" onclick="editStaffFromDetail()">编辑</button>
  2873. </div>
  2874. </dialog>
  2875. <dialog class="apply-dlg" id="acidDlg">
  2876. <div class="dlg-h green">
  2877. <h3>加水 / 加工转换</h3>
  2878. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeAcidDlg()">×</button>
  2879. </div>
  2880. <form id="acidForm" onsubmit="return false">
  2881. <div class="dlg-b">
  2882. <p class="muted" style="margin-bottom:12px" id="acidDlgHint">98% 工业硫酸可加水稀释。工业 / 试剂分线,不得混兑。</p>
  2883. <div class="apply">
  2884. <div class="row"><div class="lab"><span class="star">*</span>原料酸</div>
  2885. <select name="rawId" required onchange="onAcidRawChange()"></select>
  2886. </div>
  2887. <div class="row"><div class="lab"><span class="star">*</span>加工去向</div>
  2888. <select name="target" required onchange="onAcidTarget()">
  2889. <option value="30">加水 · 30%</option>
  2890. <option value="50">加水 · 50%</option>
  2891. <option value="custom">加水 · 其他定制浓度</option>
  2892. </select>
  2893. </div>
  2894. <div class="row" id="acidCustomRow" style="display:none"><div class="lab"><span class="star">*</span>定制浓度</div>
  2895. <input name="customPct" type="number" min="1" max="97" step="0.1" placeholder="如 40、70、80" oninput="updateAcidCalc()" />
  2896. </div>
  2897. <div class="row"><div class="lab"><span class="star">*</span>成品数量</div>
  2898. <input name="outQty" type="number" min="0.01" step="0.01" value="20" required oninput="updateAcidCalc()" />
  2899. </div>
  2900. </div>
  2901. <div class="calc-box" id="acidCalc"></div>
  2902. </div>
  2903. <div class="dlg-f">
  2904. <button class="btn" type="button" onclick="closeAcidDlg()">取消</button>
  2905. <button class="btn" type="button" onclick="saveAcidConvert(false)">确认转换</button>
  2906. <button class="btn btn-primary" type="button" onclick="saveAcidConvert(true)">确认并打印加工单</button>
  2907. </div>
  2908. </form>
  2909. </dialog>
  2910. <dialog id="attachDialog">
  2911. <div class="dlg-h">
  2912. <h3 id="attachTitle">附件</h3>
  2913. <button class="btn" type="button" onclick="closeAttach()">关闭</button>
  2914. </div>
  2915. <div class="dlg-b" id="attachBody"></div>
  2916. </dialog>
  2917. <dialog class="apply-dlg wide-dlg" id="fuelDialog">
  2918. <div class="dlg-h green">
  2919. <h3>加油登记</h3>
  2920. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeFuelDlg()">×</button>
  2921. </div>
  2922. <form class="form" id="fuelForm" onsubmit="return addFuel(event)" style="grid-template-columns:1fr;padding:0;display:flex;flex-direction:column;gap:0">
  2923. <div class="dlg-b" style="display:grid;grid-template-columns:1fr 1fr;gap:10px;width:100%">
  2924. <label>车辆<select name="plate" required></select></label>
  2925. <label>日期<input name="date" type="date" required /></label>
  2926. <label>加油站<input name="station" required placeholder="如 中石化猇亭站" /></label>
  2927. <label>油品<select name="oil"><option>0# 柴油</option><option>-10# 柴油</option><option>LNG</option></select></label>
  2928. <label>升数<input name="liters" type="number" step="0.01" required /></label>
  2929. <label>金额(元)<input name="amount" type="number" step="0.01" required /></label>
  2930. <label>当前里程(km)<input name="km" type="number" required /></label>
  2931. <label>备注<input name="note" placeholder="可选" /></label>
  2932. <label class="full" style="grid-column:1 / -1">附件(必传)
  2933. <div class="file-box">
  2934. <input name="attach" type="file" required multiple accept="image/*,.pdf,application/pdf" onchange="previewFuelFiles(this)" />
  2935. <span class="file-hint">须上传加油小票、发票或加油凭证(图片或 PDF,可多选)</span>
  2936. <div class="chips" id="fuelFileChips"></div>
  2937. </div>
  2938. </label>
  2939. </div>
  2940. <div class="dlg-f" style="grid-column:1 / -1">
  2941. <button class="btn" type="button" onclick="closeFuelDlg()">取消</button>
  2942. <button class="btn btn-primary" type="submit">保存登记</button>
  2943. </div>
  2944. </form>
  2945. </dialog>
  2946. <dialog class="apply-dlg wide-dlg" id="maintDialog">
  2947. <div class="dlg-h green">
  2948. <h3>新增申请</h3>
  2949. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeMaintDlg()">×</button>
  2950. </div>
  2951. <form id="maintForm" onsubmit="return addMaint(event)">
  2952. <div class="dlg-b">
  2953. <div class="apply">
  2954. <div class="row">
  2955. <div class="lab"><span class="star">*</span>申请人</div>
  2956. <select name="applicant" required></select>
  2957. </div>
  2958. <div class="row">
  2959. <div class="lab"><span class="star">*</span>车牌号</div>
  2960. <select name="plate" required>
  2961. <option value="">请选择车牌</option>
  2962. </select>
  2963. </div>
  2964. <div class="row">
  2965. <div class="lab"><span class="star">*</span>维保类型</div>
  2966. <select name="type" required>
  2967. <option value="">请选择或输入维保类型</option>
  2968. <option>保养</option>
  2969. <option>维修</option>
  2970. <option>年检</option>
  2971. </select>
  2972. </div>
  2973. <div class="row">
  2974. <div class="lab"><span class="star">*</span>申请日期</div>
  2975. <input name="applyDate" type="date" required />
  2976. </div>
  2977. <div class="row">
  2978. <div class="lab"><span class="star">*</span>开始日期</div>
  2979. <input name="inDate" type="date" required />
  2980. </div>
  2981. <div class="row">
  2982. <div class="lab">预计取车日期</div>
  2983. <input name="outDate" type="date" />
  2984. </div>
  2985. <div class="row full">
  2986. <div class="lab">维保内容</div>
  2987. <textarea name="item" placeholder="请输入维保内容"></textarea>
  2988. </div>
  2989. <div class="row full">
  2990. <div class="lab">维修店地址</div>
  2991. <input name="shop" placeholder="请输入维修店地址" />
  2992. </div>
  2993. <div class="row full">
  2994. <div class="lab">附件</div>
  2995. <div>
  2996. <label class="upload">
  2997. <input name="attach" type="file" multiple accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,application/pdf" onchange="previewMaintFiles(this)" />
  2998. <span class="plus">+</span>
  2999. <span>上传附件</span>
  3000. </label>
  3001. <div class="hint">支持图片、视频、PDF、Word、Excel 等文件;可拖拽或在此区域粘贴上传</div>
  3002. <div class="chips" id="maintFileChips" style="margin-top:8px"></div>
  3003. </div>
  3004. </div>
  3005. <div class="row full">
  3006. <div class="lab">备注</div>
  3007. <textarea name="note" placeholder="备注"></textarea>
  3008. </div>
  3009. </div>
  3010. </div>
  3011. <div class="dlg-f">
  3012. <button class="btn" type="button" onclick="closeMaintDlg()">× 取消</button>
  3013. <button class="btn btn-primary" type="submit">✓ 确定</button>
  3014. </div>
  3015. </form>
  3016. </dialog>
  3017. <dialog class="apply-dlg" id="etcDialog">
  3018. <div class="dlg-h green">
  3019. <h3>新增申请</h3>
  3020. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeEtcDlg()">×</button>
  3021. </div>
  3022. <form id="etcForm" onsubmit="return false">
  3023. <div class="dlg-b">
  3024. <div class="apply">
  3025. <div class="row">
  3026. <div class="lab"><span class="star">*</span>申请人</div>
  3027. <select name="applicant" required></select>
  3028. </div>
  3029. <div class="row">
  3030. <div class="lab"><span class="star">*</span>车牌号</div>
  3031. <select name="plate" required onchange="syncEtcKm()"></select>
  3032. </div>
  3033. <div class="row">
  3034. <div class="lab"><span class="star">*</span>充值类型</div>
  3035. <select name="type" required>
  3036. <option>ETC</option>
  3037. </select>
  3038. </div>
  3039. <div class="row">
  3040. <div class="lab"><span class="star">*</span>申请日期</div>
  3041. <input name="date" type="date" required />
  3042. </div>
  3043. <div class="row">
  3044. <div class="lab"><span class="star">*</span>充值金额</div>
  3045. <input name="amount" type="number" min="0" step="0.01" required placeholder="元" />
  3046. </div>
  3047. <div class="row">
  3048. <div class="lab">使用金额</div>
  3049. <input name="used" type="number" min="0" step="0.01" placeholder="元" />
  3050. </div>
  3051. <div class="row full">
  3052. <div class="lab"><span class="star">*</span>期初公里数</div>
  3053. <div class="stepper">
  3054. <button type="button" onclick="stepKm(-1)">−</button>
  3055. <input name="km" type="number" min="0" required placeholder="请输入期初公里数" />
  3056. <button type="button" onclick="stepKm(1)">+</button>
  3057. </div>
  3058. </div>
  3059. <div class="row full">
  3060. <div class="lab"><span class="star">*</span>附件</div>
  3061. <div>
  3062. <label class="upload">
  3063. <input name="attach" type="file" required multiple accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,application/pdf" onchange="previewEtcFiles(this)" />
  3064. <span class="plus">+</span>
  3065. <span>上传附件</span>
  3066. </label>
  3067. <div class="hint">支持图片、视频、PDF、Word、Excel 等文件;可拖拽或在此区域粘贴上传</div>
  3068. <div class="chips" id="etcFileChips" style="margin-top:8px"></div>
  3069. </div>
  3070. </div>
  3071. <div class="row full">
  3072. <div class="lab">备注</div>
  3073. <textarea name="note" placeholder="备注"></textarea>
  3074. </div>
  3075. </div>
  3076. </div>
  3077. <div class="dlg-f">
  3078. <button class="btn" type="button" onclick="closeEtcDlg()">× 取消</button>
  3079. <button class="btn" type="button" onclick="saveEtc(false)">✓ 保存</button>
  3080. <button class="btn btn-primary" type="button" onclick="saveEtc(true)">↑ 保存并提交</button>
  3081. </div>
  3082. </form>
  3083. </dialog>
  3084. <dialog class="apply-dlg" id="oilCardDialog">
  3085. <div class="dlg-h green">
  3086. <h3>新增申请</h3>
  3087. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeOilCardDlg()">×</button>
  3088. </div>
  3089. <form id="oilCardForm" onsubmit="return false">
  3090. <div class="dlg-b">
  3091. <div class="apply">
  3092. <div class="row">
  3093. <div class="lab"><span class="star">*</span>申请人</div>
  3094. <select name="applicant" required></select>
  3095. </div>
  3096. <div class="row">
  3097. <div class="lab"><span class="star">*</span>车牌号</div>
  3098. <select name="plate" required onchange="syncOilKm()"></select>
  3099. </div>
  3100. <div class="row">
  3101. <div class="lab"><span class="star">*</span>充值类型</div>
  3102. <select name="type" required>
  3103. <option>油卡</option>
  3104. </select>
  3105. </div>
  3106. <div class="row">
  3107. <div class="lab"><span class="star">*</span>申请日期</div>
  3108. <input name="date" type="date" required />
  3109. </div>
  3110. <div class="row">
  3111. <div class="lab"><span class="star">*</span>充值金额</div>
  3112. <input name="amount" type="number" min="0" step="0.01" required placeholder="元" />
  3113. </div>
  3114. <div class="row">
  3115. <div class="lab">使用金额</div>
  3116. <input name="used" type="number" min="0" step="0.01" placeholder="元" />
  3117. </div>
  3118. <div class="row full">
  3119. <div class="lab"><span class="star">*</span>期初公里数</div>
  3120. <div class="stepper">
  3121. <button type="button" onclick="stepOilKm(-1)">−</button>
  3122. <input name="km" type="number" min="0" required placeholder="请输入期初公里数" />
  3123. <button type="button" onclick="stepOilKm(1)">+</button>
  3124. </div>
  3125. </div>
  3126. <div class="row full">
  3127. <div class="lab"><span class="star">*</span>附件</div>
  3128. <div>
  3129. <label class="upload">
  3130. <input name="attach" type="file" required multiple accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,application/pdf" onchange="previewOilCardFiles(this)" />
  3131. <span class="plus">+</span>
  3132. <span>上传附件</span>
  3133. </label>
  3134. <div class="hint">支持图片、视频、PDF、Word、Excel 等文件;可拖拽或在此区域粘贴上传</div>
  3135. <div class="chips" id="oilCardFileChips" style="margin-top:8px"></div>
  3136. </div>
  3137. </div>
  3138. <div class="row full">
  3139. <div class="lab">备注</div>
  3140. <textarea name="note" placeholder="备注"></textarea>
  3141. </div>
  3142. </div>
  3143. </div>
  3144. <div class="dlg-f">
  3145. <button class="btn" type="button" onclick="closeOilCardDlg()">× 取消</button>
  3146. <button class="btn" type="button" onclick="saveOilCard(false)">✓ 保存</button>
  3147. <button class="btn btn-primary" type="button" onclick="saveOilCard(true)">↑ 保存并提交</button>
  3148. </div>
  3149. </form>
  3150. </dialog>
  3151. <dialog class="apply-dlg" id="planDialog">
  3152. <div class="dlg-h green">
  3153. <h3>新增计划</h3>
  3154. <button class="dlg-x" type="button" aria-label="关闭" onclick="closePlanDlg()">×</button>
  3155. </div>
  3156. <form id="planForm" onsubmit="return false">
  3157. <div class="dlg-b">
  3158. <div class="apply">
  3159. <div class="row"><div class="lab"><span class="star">*</span>单据类型</div>
  3160. <select name="type" required><option>出库单</option><option value="入直单">入库单</option><option>直销单</option></select>
  3161. </div>
  3162. <div class="row"><div class="lab"><span class="star">*</span>单据日期</div><input name="date" type="date" required /></div>
  3163. <div class="row"><div class="lab">交货日期</div><input name="deliverDate" type="date" /></div>
  3164. <div class="row"><div class="lab"><span class="star">*</span>品名</div><input name="product" required placeholder="如 30%以上烧碱" /></div>
  3165. <div class="row"><div class="lab">产品规格</div><input name="spec" placeholder="如 32%" /></div>
  3166. <div class="row"><div class="lab">管控</div>
  3167. <select name="ctrl"><option value="">普通危化</option><option>易制毒</option><option>易制爆</option></select>
  3168. </div>
  3169. <div class="row"><div class="lab">运输方式</div>
  3170. <select name="shipMode"><option>槽车运输</option><option>车辆运输</option></select>
  3171. </div>
  3172. <div class="row"><div class="lab"><span class="star">*</span>计划数量</div><input name="planQty" type="number" step="0.01" required /></div>
  3173. <div class="row"><div class="lab">单位</div><input name="unit" value="吨" /></div>
  3174. <div class="row"><div class="lab">包装规格</div><input name="pack" value="槽罐车" /></div>
  3175. <div class="row"><div class="lab">业务员</div><input name="salesman" placeholder="如 杨磊" /></div>
  3176. <div class="row"><div class="lab">业务部门</div><input name="dept" value="运输调度组" /></div>
  3177. <div class="row"><div class="lab">发货单位</div><input name="loadFrom" /></div>
  3178. <div class="row"><div class="lab">结算单位</div><input name="settler" /></div>
  3179. <div class="row full"><div class="lab">客户 / 收货</div><input name="customer" /></div>
  3180. <div class="row full"><div class="lab">合同</div>
  3181. <div>
  3182. <div class="chips" id="planCtPick" style="gap:8px"></div>
  3183. <div class="muted" style="margin-top:6px">可多选。同一客户多份合同可并入一条运输计划。</div>
  3184. </div>
  3185. </div>
  3186. </div>
  3187. </div>
  3188. <div class="dlg-f">
  3189. <button class="btn" type="button" onclick="closePlanDlg()">取消</button>
  3190. <button class="btn btn-primary" type="button" onclick="savePlan()">保存待安排</button>
  3191. </div>
  3192. </form>
  3193. </dialog>
  3194. <dialog class="apply-dlg wide-dlg" id="planTaskDialog">
  3195. <div class="dlg-h green">
  3196. <h3>下发任务单</h3>
  3197. <button class="dlg-x" type="button" aria-label="关闭" onclick="closePlanTaskDlg()">×</button>
  3198. </div>
  3199. <form id="planTaskForm" onsubmit="return false">
  3200. <div class="dlg-b">
  3201. <p class="muted" id="planTaskHint" style="margin-bottom:12px"></p>
  3202. <div class="apply">
  3203. <div class="row"><div class="lab">任务单号</div><input name="no" readonly /></div>
  3204. <div class="row"><div class="lab">任务类型</div>
  3205. <select name="kind" onchange="onPlanTaskKindChange()">
  3206. <option value="分装">分装任务单</option>
  3207. <option value="稀释">稀释任务单</option>
  3208. <option value="精制">精制任务单</option>
  3209. </select>
  3210. </div>
  3211. <div class="row"><div class="lab"><span class="star">*</span>下单日期</div><input name="orderDate" type="date" required /></div>
  3212. <div class="row"><div class="lab"><span class="star">*</span>交货日期</div><input name="deliverDate" type="date" required /></div>
  3213. <div class="row"><div class="lab"><span class="star">*</span>下单人</div><input name="orderBy" required /></div>
  3214. <div class="row"><div class="lab">经手人</div><select name="handler"></select></div>
  3215. <div class="row"><div class="lab">制单人</div><input name="maker" /></div>
  3216. <div class="row"><div class="lab">审核</div><input name="auditor" placeholder="待审核" /></div>
  3217. <div class="row"><div class="lab">生产主管</div><input name="supervisor" /></div>
  3218. <div class="row"><div class="lab">桶型说明</div>
  3219. <select name="bucket">
  3220. <option>桶循环</option>
  3221. <option>全新桶</option>
  3222. <option>是否回收桶</option>
  3223. </select>
  3224. </div>
  3225. <div class="row full"><div class="lab">产品明细</div>
  3226. <div class="task-line-wrap" id="planTaskLines"></div>
  3227. </div>
  3228. <div class="row full"><div class="lab">备注</div>
  3229. <textarea name="remark" placeholder="如客户简称、现场说明"></textarea>
  3230. </div>
  3231. </div>
  3232. </div>
  3233. <div class="dlg-f">
  3234. <button class="btn" type="button" onclick="closePlanTaskDlg()">取消</button>
  3235. <button class="btn btn-primary" type="button" onclick="savePlanTask()">下发任务单</button>
  3236. </div>
  3237. </form>
  3238. </dialog>
  3239. <dialog class="apply-dlg wide-dlg" id="taskSheetDialog">
  3240. <div class="dlg-h green">
  3241. <h3>任务单</h3>
  3242. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeTaskSheetDlg()">×</button>
  3243. </div>
  3244. <div class="dlg-b" id="taskSheetBody"></div>
  3245. <div class="dlg-f">
  3246. <button class="btn" type="button" onclick="closeTaskSheetDlg()">关闭</button>
  3247. <button class="btn btn-primary" type="button" onclick="toast('演示:已打印任务单')">打印</button>
  3248. </div>
  3249. </dialog>
  3250. <dialog class="apply-dlg" id="taskReportDialog">
  3251. <div class="dlg-h green">
  3252. <h3 id="taskReportTitle">回传完成任务</h3>
  3253. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeTaskReportDlg()">×</button>
  3254. </div>
  3255. <form id="taskReportForm" onsubmit="return false">
  3256. <div class="dlg-b">
  3257. <p class="muted" id="taskReportHint" style="margin-bottom:12px"></p>
  3258. <div class="apply">
  3259. <div class="row"><div class="lab">任务单号</div><input name="no" readonly /></div>
  3260. <div class="row"><div class="lab">指派人员</div><input name="assignee" readonly /></div>
  3261. <div class="row"><div class="lab"><span class="star">*</span>完成时间</div>
  3262. <input name="doneTime" type="datetime-local" required />
  3263. </div>
  3264. <div class="row"><div class="lab"><span class="star">*</span>完成数量</div>
  3265. <input name="doneQty" type="number" min="0" step="0.01" required />
  3266. </div>
  3267. <div class="row full"><div class="lab"><span class="star">*</span>回传说明</div>
  3268. <textarea name="report" required placeholder="现场完成情况、异常或交接说明"></textarea>
  3269. </div>
  3270. <div class="row full">
  3271. <div class="lab"><span class="star">*</span>纸质分装单</div>
  3272. <div>
  3273. <div class="task-photo-row">
  3274. <label class="upload task-photo-up" id="taskSheetPhotoBtn">
  3275. <input name="sheetPhotos" type="file" accept="image/*" capture="environment" multiple onchange="previewTaskSheetPhotos(this)" />
  3276. <span class="plus">📷</span>
  3277. <span>拍照 / 上传</span>
  3278. </label>
  3279. <div class="task-photo-thumbs" id="taskSheetPhotoThumbs"></div>
  3280. </div>
  3281. <p class="file-hint" id="taskSheetPhotoHint" style="margin-top:8px">须拍摄已签字完成的纸质分装任务单,可多张。</p>
  3282. <div class="chips" id="taskSheetPhotoChips"></div>
  3283. </div>
  3284. </div>
  3285. </div>
  3286. </div>
  3287. <div class="dlg-f">
  3288. <button class="btn" type="button" onclick="closeTaskReportDlg()">取消</button>
  3289. <button class="btn btn-primary" type="button" id="taskReportSave" onclick="saveTaskReport()">确认回传完成</button>
  3290. </div>
  3291. </form>
  3292. </dialog>
  3293. <dialog class="apply-dlg" id="archiveDialog">
  3294. <div class="dlg-h green">
  3295. <h3>运输归档</h3>
  3296. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeArchiveDlg()">×</button>
  3297. </div>
  3298. <form id="archiveForm" onsubmit="return false">
  3299. <div class="dlg-b">
  3300. <p class="muted" id="archiveHint" style="margin-bottom:12px"></p>
  3301. <div class="apply">
  3302. <div class="row"><div class="lab">单号</div><input id="archiveNo" readonly /></div>
  3303. <div class="row"><div class="lab">类型 / 品名</div><input id="archiveProd" readonly /></div>
  3304. <div class="row"><div class="lab"><span class="star">*</span>实发数量</div>
  3305. <input name="realQty" type="number" step="0.01" min="0.01" required />
  3306. </div>
  3307. <div class="row"><div class="lab">单位</div><input id="archiveUnit" readonly /></div>
  3308. <div class="row full" id="archiveWhRow">
  3309. <div class="lab">对应仓库</div>
  3310. <input id="archiveWhName" readonly />
  3311. <input type="hidden" name="warehouseId" />
  3312. </div>
  3313. </div>
  3314. <p class="hint" id="archiveWhHint">出库单 / 入库单按品名自动对应仓库;直销单无中转仓库。</p>
  3315. </div>
  3316. <div class="dlg-f">
  3317. <button class="btn" type="button" onclick="closeArchiveDlg()">取消</button>
  3318. <button class="btn btn-primary" type="button" onclick="confirmArchive()">确认归档</button>
  3319. </div>
  3320. </form>
  3321. </dialog>
  3322. <dialog class="apply-dlg" id="dispatchDialog">
  3323. <div class="dlg-h green">
  3324. <h3>分派车辆运输量</h3>
  3325. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeDispatchDlg()">×</button>
  3326. </div>
  3327. <form id="dispatchForm" onsubmit="return false">
  3328. <div class="dlg-b">
  3329. <p class="muted" id="dispatchDlgHint" style="margin-bottom:12px"></p>
  3330. <div class="apply">
  3331. <div class="row full"><div class="lab">运输公司</div><input name="company" readonly /></div>
  3332. <div class="row"><div class="lab"><span class="star">*</span>车辆</div>
  3333. <select name="plate" required onchange="syncDispatchDriver()"></select>
  3334. </div>
  3335. <div class="row"><div class="lab">驾驶员</div><input name="driver" /></div>
  3336. <div class="row full hidden" id="dispatchNewPlateRow">
  3337. <div class="lab"><span class="star">*</span>新车牌</div>
  3338. <input name="newPlate" placeholder="同公司其他车辆,共用本运输证" />
  3339. </div>
  3340. <div class="row full"><div class="lab"><span class="star">*</span>分派运量</div>
  3341. <input name="qty" type="number" step="0.01" min="0.01" required placeholder="按运输证未分派量填写" />
  3342. </div>
  3343. </div>
  3344. </div>
  3345. <div class="dlg-f">
  3346. <button class="btn" type="button" onclick="closeDispatchDlg()">取消</button>
  3347. <button class="btn btn-primary" type="button" onclick="saveDispatch()">确认分派</button>
  3348. </div>
  3349. </form>
  3350. </dialog>
  3351. <dialog class="apply-dlg wide-dlg" id="linkCertDialog">
  3352. <div class="dlg-h green">
  3353. <h3>从计划关联证件</h3>
  3354. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeLinkCertDlg()">×</button>
  3355. </div>
  3356. <div class="dlg-b">
  3357. <p class="muted" id="linkCertHint" style="margin-bottom:10px"></p>
  3358. <div class="link-sec">
  3359. <h4>选择购买证下的运输证</h4>
  3360. <div id="linkCertTable"></div>
  3361. </div>
  3362. </div>
  3363. <div class="dlg-f">
  3364. <button class="btn" type="button" onclick="closeLinkCertDlg()">取消</button>
  3365. <button class="btn" type="button" onclick="showMod('cert');closeLinkCertDlg()">去上传新证</button>
  3366. </div>
  3367. </dialog>
  3368. <dialog class="apply-dlg wide-dlg wait-link-dlg" id="waitBuyDialog">
  3369. <div class="dlg-h green">
  3370. <h3>关联购买证</h3>
  3371. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeWaitBuyDlg()">×</button>
  3372. </div>
  3373. <div class="dlg-b">
  3374. <p class="muted" id="waitBuyHint" style="margin-bottom:10px"></p>
  3375. <div id="waitBuyBody"></div>
  3376. </div>
  3377. <div class="dlg-f">
  3378. <button class="btn" type="button" onclick="closeWaitBuyDlg()">取消</button>
  3379. <button class="btn btn-primary" type="button" onclick="saveWaitBuyCert()">确认关联购买证</button>
  3380. </div>
  3381. </dialog>
  3382. <dialog class="apply-dlg wide-dlg wait-link-dlg" id="waitTransDialog">
  3383. <div class="dlg-h green">
  3384. <h3>关联运输证</h3>
  3385. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeWaitTransDlg()">×</button>
  3386. </div>
  3387. <div class="dlg-b">
  3388. <p class="muted" id="waitTransHint" style="margin-bottom:10px"></p>
  3389. <div id="waitTransBody"></div>
  3390. </div>
  3391. <div class="dlg-f">
  3392. <button class="btn" type="button" onclick="closeWaitTransDlg()">取消</button>
  3393. </div>
  3394. </dialog>
  3395. <dialog class="apply-dlg wide-dlg" id="vehPlanDialog">
  3396. <div class="dlg-h green">
  3397. <h3 id="vehPlanTitle">关联运输计划</h3>
  3398. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeVehPlanDlg()">×</button>
  3399. </div>
  3400. <div class="dlg-b" id="vehPlanBody"></div>
  3401. <div class="dlg-f">
  3402. <button class="btn" type="button" onclick="closeVehPlanDlg()">关闭</button>
  3403. </div>
  3404. </dialog>
  3405. <dialog class="cert-img-dlg" id="certImgDialog" onclick="closeCertImg(event)">
  3406. <img id="certImgView" alt="证件原图" />
  3407. </dialog>
  3408. <dialog class="apply-dlg" id="voidCertDialog">
  3409. <div class="dlg-h green">
  3410. <h3 id="voidCertTitle">核销证件</h3>
  3411. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeVoidCertDlg()">×</button>
  3412. </div>
  3413. <div class="dlg-b">
  3414. <p class="muted" id="voidCertHint"></p>
  3415. </div>
  3416. <div class="dlg-f">
  3417. <button class="btn" type="button" onclick="closeVoidCertDlg()">取消</button>
  3418. <button class="btn btn-danger" type="button" onclick="confirmVoidCert()">确认核销</button>
  3419. </div>
  3420. </dialog>
  3421. <dialog class="apply-dlg wide-dlg" id="certLinkPlanDialog">
  3422. <div class="dlg-h green">
  3423. <h3>人工关联运输计划</h3>
  3424. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeCertLinkPlanDlg()">×</button>
  3425. </div>
  3426. <div class="dlg-b">
  3427. <p class="muted" id="certLinkPlanHint" style="margin-bottom:10px"></p>
  3428. <div id="certLinkPreview" class="link-preview"></div>
  3429. <label class="quote-search plan-search" style="margin:0 0 10px;max-width:360px">
  3430. <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
  3431. <input id="certLinkPlanQ" type="search" placeholder="筛选计划编号 / 品名 / 客户 / 车牌" oninput="filterCertLinkPlanRows()" />
  3432. </label>
  3433. <div class="erp-table-wrap" id="certLinkPlanTable"></div>
  3434. </div>
  3435. <div class="dlg-f">
  3436. <button class="btn" type="button" onclick="closeCertLinkPlanDlg()">取消</button>
  3437. <button class="btn btn-primary" type="button" onclick="saveCertLinkPlans()">确认关联所选计划</button>
  3438. </div>
  3439. </dialog>
  3440. <dialog class="wide-dlg" id="certDialog">
  3441. <div class="dlg-h green">
  3442. <h3 id="certDlgTitle">识别购买证 + 运输证</h3>
  3443. <button class="dlg-x" type="button" aria-label="关闭" onclick="closeCertDlg()">×</button>
  3444. </div>
  3445. <form id="certForm" onsubmit="return false">
  3446. <div class="dlg-b">
  3447. <p class="hint" id="certDlgHint" style="margin-bottom:12px">一张购买证可同时识别、挂多张运输证;也可保存后再继续追加。</p>
  3448. <div class="ocr-pair">
  3449. <div class="ocr-box buy" id="buyOcrBox">
  3450. <h4><span class="cert-tag buy">购买证</span> 第二类、第三类易制毒化学品购买备案证明</h4>
  3451. <p class="hint">上传或拖拽购买证照片或扫描件,读取编号、购买单位、品名与数量。</p>
  3452. <label class="upload">
  3453. <input id="buyFile" type="file" accept="image/*,.pdf" onchange="recognize('buy', this)" />
  3454. <span class="plus">+</span>
  3455. <span>上传购买证</span>
  3456. </label>
  3457. <img class="ocr-preview hidden" id="buyPreview" alt="购买证预览" />
  3458. <div class="ocr-status" id="buyStatus">待上传</div>
  3459. </div>
  3460. <div class="ocr-box trans" id="transOcrBox">
  3461. <h4><span class="cert-tag trans">运输证</span> 第三类易制毒化学品运输备案证明</h4>
  3462. <p class="hint">上传或拖拽运输证。同一购买证可识别多张运输证。</p>
  3463. <label class="upload">
  3464. <input id="transFile" type="file" accept="image/*,.pdf" onchange="recognize('trans', this)" />
  3465. <span class="plus">+</span>
  3466. <span>上传运输证</span>
  3467. </label>
  3468. <img class="ocr-preview hidden" id="transPreview" alt="运输证预览" />
  3469. <div class="ocr-status" id="transStatus">待上传</div>
  3470. </div>
  3471. </div>
  3472. <div class="split-h">
  3473. <div id="buyFormCol" class="col-buy">
  3474. <h4>购买证信息</h4>
  3475. <div class="form" style="grid-template-columns:1fr 1fr">
  3476. <label><span><span class="star">*</span>编号</span><input name="buyNo" required /></label>
  3477. <label>核发日期<input name="buyDate" type="date" /></label>
  3478. <label class="span2"><span><span class="star">*</span>购买单位</span><input name="buyer" required /></label>
  3479. <label>法定代表人<input name="buyerLegal" /></label>
  3480. <label>电话<input name="buyerTel" /></label>
  3481. <label class="span2">住所 / 住址<input name="buyerAddr" /></label>
  3482. <label class="span2">销售单位<input name="seller" /></label>
  3483. <label>销售方法人<input name="sellerLegal" /></label>
  3484. <label>销售方电话<input name="sellerTel" /></label>
  3485. <label class="span2">销售方住所<input name="sellerAddr" /></label>
  3486. <label><span><span class="star">*</span>购买物品</span><input name="product" required /></label>
  3487. <label>用途<input name="usage" /></label>
  3488. <label><span><span class="star">*</span>数量</span><input name="buyQty" type="number" step="0.01" required /></label>
  3489. <label>单位<input name="unit" value="吨" /></label>
  3490. <label>余量预警<input name="warnQty" type="number" step="0.01" min="0" placeholder="核准总量 10%" title="默认按证件核准总量的 10% 预警,可改" /></label>
  3491. <label>有效次数<input name="buyTimes" /></label>
  3492. <label>有效开始<input name="buyFrom" type="date" /></label>
  3493. <label>有效截止<input name="buyDue" type="date" /></label>
  3494. <label class="span2">公安机关<input name="buyPolice" /></label>
  3495. <label>经办人<input name="buyHandler" /></label>
  3496. <label>联系电话<input name="buyPoliceTel" /></label>
  3497. </div>
  3498. </div>
  3499. <div class="col-trans">
  3500. <h4>运输证信息(第 1 张)</h4>
  3501. <div class="form" id="transForm0" style="grid-template-columns:1fr 1fr">
  3502. <label><span><span class="star">*</span>编号</span><input name="transNo" required /></label>
  3503. <label>核发日期<input name="transDate" type="date" /></label>
  3504. <label class="span2">货主<input name="owner" /></label>
  3505. <label>法定代表人<input name="ownerLegal" /></label>
  3506. <label>电话<input name="ownerTel" /></label>
  3507. <label class="span2">货主住所<input name="ownerAddr" /></label>
  3508. <label>包装<input name="pack" /></label>
  3509. <label>购买许可/备案证号<input name="permitNo" /></label>
  3510. <label class="span2">运输单位<input name="carrier" /></label>
  3511. <label>住所/电话<input name="carrierAddr" /></label>
  3512. <label>运输方式<input name="mode" /></label>
  3513. <label>车号(次)<input name="plates" /></label>
  3514. <label class="span2">运输路线<input name="route" /></label>
  3515. <label class="span2">收货单位<input name="consignee" /></label>
  3516. <label>运输数量<input name="transQty" type="number" step="0.01" /></label>
  3517. <label>余量预警<input name="warnQty" type="number" step="0.01" min="0" placeholder="核准总量 10%" title="默认按证件核准总量的 10% 预警,可改" /></label>
  3518. <label>有效次数<input name="transTimes" /></label>
  3519. <label>有效开始<input name="transFrom" type="date" /></label>
  3520. <label>有效截止<input name="transDue" type="date" /></label>
  3521. <label class="span2">申请单位<input name="applicantUnit" /></label>
  3522. <label class="span2">公安机关<input name="transPolice" /></label>
  3523. <label>经办人<input name="transHandler" /></label>
  3524. </div>
  3525. </div>
  3526. </div>
  3527. <div id="extraTransSlots"></div>
  3528. <div style="margin-top:12px">
  3529. <button class="btn" type="button" onclick="addTransSlot()">+ 再识别一张运输证</button>
  3530. </div>
  3531. </div>
  3532. <div class="dlg-f">
  3533. <button class="btn" type="button" onclick="closeCertDlg()">取消</button>
  3534. <button class="btn btn-primary" type="button" id="certSaveBtn" onclick="saveCert()">生成证件信息</button>
  3535. </div>
  3536. </form>
  3537. </dialog>
  3538. <script>
  3539. const today = "2026-10-01";
  3540. const DUTY_TODAY = "2026-10-07";
  3541. const DUTY_STAFF = ["吴自国", "刘茂生", "吴自泰", "黄天宝", "刘丹", "汪璇", "吴良芹"];
  3542. const DUTY_TYPE = { am: "早班", pm: "晚班", watch: "值班", other: "其他" };
  3543. let dutyYM = { y: 2026, m: 10 };
  3544. let dutyPickDate = "";
  3545. let dutyPickPeople = [];
  3546. let dutySeq = 20;
  3547. let dutyShifts = [
  3548. { id: 1, date: "2026-10-01", type: "am", people: ["刘茂生", "吴自国"], post: "配酸" },
  3549. { id: 2, date: "2026-10-01", type: "watch", people: ["黄天宝"], post: "配酸" },
  3550. { id: 3, date: "2026-10-02", type: "am", people: ["吴自泰", "黄天宝"], post: "" },
  3551. { id: 4, date: "2026-10-02", type: "watch", people: ["吴自国"], post: "配酸" },
  3552. { id: 5, date: "2026-10-03", type: "am", people: ["刘茂生", "吴自国"], post: "配酸" },
  3553. { id: 6, date: "2026-10-03", type: "watch", people: ["吴自泰"], post: "配酸" },
  3554. { id: 7, date: "2026-10-04", type: "am", people: ["黄天宝", "吴自泰"], post: "配酸" },
  3555. { id: 8, date: "2026-10-04", type: "watch", people: ["吴自国"], post: "配酸" },
  3556. { id: 9, date: "2026-10-05", type: "am", people: ["吴自泰", "刘茂生"], post: "配酸" },
  3557. { id: 10, date: "2026-10-06", type: "am", people: ["黄天宝", "刘茂生"], post: "配酸" },
  3558. { id: 11, date: "2026-10-06", type: "watch", people: ["吴自国"], post: "配酸" },
  3559. { id: 12, date: "2026-10-07", type: "am", people: ["刘茂生", "吴自泰"], post: "配酸" },
  3560. { id: 13, date: "2026-10-07", type: "watch", people: ["吴自国"], post: "配酸" },
  3561. { id: 14, date: "2026-10-11", type: "watch", people: ["吴良芹"], post: "" },
  3562. { id: 15, date: "2026-10-18", type: "watch", people: ["汪璇"], post: "" },
  3563. { id: 16, date: "2026-10-25", type: "watch", people: ["刘丹"], post: "" }
  3564. ];
  3565. const dayShift = (d, n) => {
  3566. const x = new Date(d + "T00:00:00");
  3567. x.setDate(x.getDate() + n);
  3568. return x.toISOString().slice(0, 10);
  3569. };
  3570. const quoteMoney = (n) => "¥" + Number(n || 0).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  3571. let quoteTab = "全部";
  3572. let quoteStatusFilter = "";
  3573. let quotePage = 1;
  3574. let quotePageSize = 20;
  3575. let quoteSeq = 12;
  3576. let selectedQuoteIds = new Set();
  3577. let expandedQuoteIds = new Set();
  3578. let quotes = [
  3579. { id: 1, no: "SXDD-20260930002", title: "山峡-安庆汇辰 (SXDD-20260930002)", kind: "销售", customer: "安庆汇辰科技有限公司", product: "一水柠檬酸 99.50% 0.125 吨", prodAmt: 1375, freight: 0, date: "2026-09-30", status: "已生效" },
  3580. { id: 2, no: "SXDD-20260930001", title: "山峡-安徽金兽 (SXDD-20260930001)", kind: "销售", customer: "安徽金兽医药科技有限公司", product: "片碱 2 吨", prodAmt: 7400, freight: 0, date: "2026-09-30", status: "已生效" },
  3581. { id: 3, no: "SXDD-20260929004", title: "山峡-厦门圣益 (SXDD-20260929004)", kind: "销售", customer: "厦门圣益石化有限公司", product: "30%液碱 30% 65 吨", prodAmt: 46800, freight: 0, date: "2026-09-29", status: "已生效" },
  3582. { id: 4, no: "SXDD-20260929003", title: "山峡-安徽中禹 (SXDD-20260929003)", kind: "销售", customer: "安徽省中禹商贸有限公司", product: "30% 工业硫酸 30% 21 吨", prodAmt: 25200, freight: 0, date: "2026-09-29", status: "已生效" },
  3583. { id: 5, no: "SXDD-20260929002", title: "山峡-安徽恒星 (SXDD-20260929002)", kind: "销售", customer: "安徽恒星新材料科技有限公司", product: "50%液碱 50% 400 吨", prodAmt: 508000, freight: 0, date: "2026-09-29", status: "待审批", alert: true },
  3584. { id: 6, no: "SXDD-20260929001", title: "山峡-安徽金兽 (SXDD-20260929001)", kind: "销售", customer: "安徽金兽医药科技有限公司", product: "片碱 2 吨", prodAmt: 7400, freight: 0, date: "2026-09-29", status: "已生效" },
  3585. { id: 7, no: "SXDD-20260928001", title: "山峡-安徽紫荆花 (SXDD-20260928001)", kind: "销售", customer: "安徽紫荆花新型材料科技有限公司", product: "聚合氯化铝 5 吨", prodAmt: 10000, freight: 0, date: "2026-09-28", status: "已生效" },
  3586. { id: 8, no: "SXDD-20260924003", title: "山峡-桐城金涛 (SXDD-20260924003)", kind: "销售", customer: "桐城市金涛表面处理有限公司", product: "次氯酸钠 13% 30 吨", prodAmt: 20400, freight: 0, date: "2026-09-24", status: "已生效" },
  3587. { id: 9, no: "SXDD-20260924002", title: "山峡-安庆飞霖 (SXDD-20260924002)", kind: "销售", customer: "安庆飞霖新材料有限公司", product: "32%液碱 32% 30 吨", prodAmt: 25200, freight: 0, date: "2026-09-24", status: "已生效" },
  3588. { id: 10, no: "SXDD-20260924001", title: "山峡-安庆百谊生物 (SXDD-20260924001)", kind: "销售", customer: "安庆百谊生物科技有限公司", product: "工业盐酸 31% 0.025 吨", prodAmt: 50, freight: 0, date: "2026-09-24", status: "已生效" },
  3589. { id: 11, no: "SXDD-20260923001", title: "山峡-三土 (SXDD-20260923001)", kind: "销售", customer: "三土新材料科技(上海)有限公司", product: "氯化钠 国标 0.65 吨", prodAmt: 910, freight: 0, date: "2026-09-23", status: "已生效" },
  3590. { id: 12, no: "SXDD-20261001001", title: "山峡-宜昌安达 (SXDD-20261001001)", kind: "销售", customer: "宜昌安达危运有限公司", product: "98% 工业硫酸 32 吨", prodAmt: 28800, freight: 0, date: "2026-10-01", status: "草稿" },
  3591. { id: 13, no: "SXDD-20261002001", title: "山峡-上饶敏达 (SXDD-20261002001)", kind: "销售", customer: "上饶市敏达精细化工有限公司", product: "硫酸 98% 30 吨", prodAmt: 27000, freight: 0, date: "2026-10-02", status: "草稿" }
  3592. ];
  3593. function quoteWeekRange() {
  3594. const d = new Date(today + "T00:00:00");
  3595. const day = d.getDay() || 7;
  3596. const start = dayShift(today, 1 - day);
  3597. const end = dayShift(start, 6);
  3598. return [start, end];
  3599. }
  3600. function filteredQuotes() {
  3601. const q = (document.getElementById("quoteQ")?.value || "").trim();
  3602. let rows = quotes.filter((x) => x.status !== "已作废" || quoteTab === "全部");
  3603. if (quoteTab === "昨日") rows = rows.filter((x) => x.date === dayShift(today, -1));
  3604. else if (quoteTab === "今日") rows = rows.filter((x) => x.date === today);
  3605. else if (quoteTab === "明日") rows = rows.filter((x) => x.date === dayShift(today, 1));
  3606. else if (quoteTab === "本周") {
  3607. const [a, b] = quoteWeekRange();
  3608. rows = rows.filter((x) => x.date >= a && x.date <= b);
  3609. }
  3610. if (quoteStatusFilter === "待审核") rows = rows.filter((x) => x.status === "待审批");
  3611. else if (quoteStatusFilter) rows = rows.filter((x) => x.status === quoteStatusFilter);
  3612. if (q) rows = rows.filter((x) => [x.no, x.title, x.customer, x.product].join(" ").includes(q));
  3613. return rows;
  3614. }
  3615. function toggleQuote(id, on) {
  3616. if (on) selectedQuoteIds.add(id); else selectedQuoteIds.delete(id);
  3617. renderQuotes();
  3618. }
  3619. function toggleQuoteAll(on) {
  3620. filteredQuotes().forEach((x) => on ? selectedQuoteIds.add(x.id) : selectedQuoteIds.delete(x.id));
  3621. renderQuotes();
  3622. }
  3623. function toggleQuoteExpand(id) {
  3624. if (expandedQuoteIds.has(id)) expandedQuoteIds.delete(id); else expandedQuoteIds.add(id);
  3625. renderQuotes();
  3626. }
  3627. function selectedQuotes() {
  3628. return quotes.filter((x) => selectedQuoteIds.has(x.id));
  3629. }
  3630. function setQuoteStatus(st) {
  3631. quoteStatusFilter = quoteStatusFilter === st ? "" : st;
  3632. quotePage = 1;
  3633. renderQuotes();
  3634. }
  3635. function waitReviewBanner(n, on, onclick, goText) {
  3636. const empty = !n;
  3637. return `<button class="wait-banner ${on ? "on" : ""} ${empty ? "empty" : ""}" type="button" onclick="${onclick}">
  3638. <span class="wb-ico" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><path d="M12 8v5l3 2"/></svg></span>
  3639. <span class="wb-n">${n}</span>
  3640. <span class="wb-k">待审核</span>
  3641. <span class="wb-go">${empty ? "暂无待审" : goText}</span>
  3642. </button>`;
  3643. }
  3644. function renderQuotes() {
  3645. const rows = filteredQuotes();
  3646. const pages = Math.max(1, Math.ceil(rows.length / quotePageSize));
  3647. if (quotePage > pages) quotePage = pages;
  3648. const start = (quotePage - 1) * quotePageSize;
  3649. const pageRows = rows.slice(start, start + quotePageSize);
  3650. const allOn = pageRows.length && pageRows.every((x) => selectedQuoteIds.has(x.id));
  3651. document.getElementById("quoteSelHint").textContent = `已选 ${selectedQuoteIds.size} 项`;
  3652. const waitN = quotes.filter((x) => x.status === "待审批").length;
  3653. const qStats = document.getElementById("quoteStats");
  3654. if (qStats) {
  3655. if (quoteStatusFilter === "草稿" || quoteStatusFilter === "已生效") quoteStatusFilter = "";
  3656. qStats.innerHTML = waitReviewBanner(waitN, quoteStatusFilter === "待审核", "setQuoteStatus('待审核')", "查看待审报价");
  3657. }
  3658. const qBadge = document.getElementById("quoteBadge");
  3659. if (qBadge) {
  3660. const n = quotes.filter((x) => x.status === "待审批" || x.status === "草稿").length;
  3661. qBadge.textContent = n;
  3662. qBadge.style.display = n ? "" : "none";
  3663. }
  3664. document.getElementById("quoteTable").innerHTML = pageRows.length ? `<table class="erp-table quote-table">
  3665. <thead><tr>
  3666. <th class="quote-expand"></th>
  3667. <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="toggleQuoteAll(this.checked)" /></th>
  3668. <th class="col-no"><span class="th-sort">报价单号</span></th>
  3669. <th class="col-title"><span class="th-sort">报价主题</span></th>
  3670. <th><span class="th-sort">报价类型</span></th>
  3671. <th><span class="th-sort">客户名称</span></th>
  3672. <th><span class="th-sort">产品信息</span></th>
  3673. <th><span class="th-sort">最终金额</span></th>
  3674. <th><span class="th-sort">产品总金额</span></th>
  3675. <th><span class="th-sort">运费</span></th>
  3676. </tr></thead>
  3677. <tbody>${pageRows.map((x) => {
  3678. const open = expandedQuoteIds.has(x.id);
  3679. const finalAmt = x.prodAmt + (x.freight || 0);
  3680. const title = String((x.title || "").replace(/\s*\([^)]*\)\s*$/, "") || x.title).replace(/"/g, "&quot;");
  3681. return `<tr class="${x.alert || x.status === "待审批" ? "row-alert" : ""}">
  3682. <td class="quote-expand ${open ? "on" : ""}" onclick="toggleQuoteExpand(${x.id})">${open ? "▾" : "▸"}</td>
  3683. <td class="chk"><input type="checkbox" ${selectedQuoteIds.has(x.id) ? "checked" : ""} onchange="toggleQuote(${x.id}, this.checked)" /></td>
  3684. <td class="cell-no col-no"><button class="quote-link" type="button" onclick="toggleQuoteExpand(${x.id})">${x.no}</button></td>
  3685. <td class="col-title" title="${title}"><button class="quote-link" type="button" onclick="toggleQuoteExpand(${x.id})">${title}</button></td>
  3686. <td><span class="sale-tag">${x.kind}</span></td>
  3687. <td>${x.customer}</td>
  3688. <td>${x.product}</td>
  3689. <td class="amt-red">${quoteMoney(finalAmt)}</td>
  3690. <td class="amt-red">${quoteMoney(x.prodAmt)}</td>
  3691. <td class="amt-red">${quoteMoney(x.freight)}</td>
  3692. </tr>${open ? `<tr class="quote-sub"><td></td><td></td><td colspan="8">日期 ${x.date} 状态 ${x.status} 产品 ${x.product} 金额 ${quoteMoney(x.prodAmt)} 运费 ${quoteMoney(x.freight)}</td></tr>` : ""}`;
  3693. }).join("")}</tbody></table>` : `<div class="empty">暂无报价单</div>`;
  3694. const nums = [];
  3695. const maxBtn = Math.min(pages, 6);
  3696. for (let i = 1; i <= maxBtn; i++) nums.push(i);
  3697. document.getElementById("quotePager").innerHTML = `
  3698. <span class="muted">共 ${rows.length} 条</span>
  3699. <select class="page-size" onchange="quotePageSize=+this.value;quotePage=1;renderQuotes()">
  3700. <option value="20" ${quotePageSize === 20 ? "selected" : ""}>20条/页</option>
  3701. <option value="50" ${quotePageSize === 50 ? "selected" : ""}>50条/页</option>
  3702. </select>
  3703. ${nums.map((n) => `<button type="button" class="${n === quotePage ? "on" : ""}" onclick="quotePage=${n};renderQuotes()">${n}</button>`).join("")}
  3704. ${pages > 6 ? `<span class="muted">… ${pages}</span>` : ""}
  3705. <span class="muted">前往</span>
  3706. <input class="quote-goto" type="number" min="1" max="${pages}" value="${quotePage}" onchange="quotePage=Math.min(${pages}, Math.max(1, +this.value||1));renderQuotes()" />
  3707. <span class="muted">页</span>`;
  3708. }
  3709. function openQuoteDlg(copyFrom) {
  3710. const f = document.getElementById("quoteForm");
  3711. f.reset();
  3712. f.date.value = today;
  3713. document.getElementById("quoteDlgTitle").textContent = copyFrom ? "复制新增报价单" : "新增报价单";
  3714. if (copyFrom) {
  3715. f.customer.value = copyFrom.customer;
  3716. f.title.value = copyFrom.title.replace(/\(SXDD-[^)]+\)/, "").trim();
  3717. f.kind.value = copyFrom.kind;
  3718. f.product.value = copyFrom.product;
  3719. f.prodAmt.value = copyFrom.prodAmt;
  3720. f.freight.value = copyFrom.freight || 0;
  3721. }
  3722. document.getElementById("quoteDialog").showModal();
  3723. }
  3724. function saveQuote() {
  3725. const f = document.getElementById("quoteForm");
  3726. if (!f.reportValidity()) return;
  3727. quoteSeq += 1;
  3728. const no = "SXDD-" + today.replaceAll("-", "") + String(quoteSeq).padStart(3, "0");
  3729. const titleBase = f.title.value.trim();
  3730. quotes.unshift({
  3731. id: Date.now(), no,
  3732. title: `${titleBase} (${no})`,
  3733. kind: f.kind.value, customer: f.customer.value.trim(),
  3734. product: f.product.value.trim(), prodAmt: +f.prodAmt.value, freight: +f.freight.value || 0,
  3735. date: f.date.value, status: "草稿"
  3736. });
  3737. document.getElementById("quoteDialog").close();
  3738. toast("已保存草稿 " + no);
  3739. renderQuotes();
  3740. }
  3741. function copyQuote() {
  3742. const rows = selectedQuotes();
  3743. if (rows.length !== 1) { toast("请勾选一张报价单后再复制新增"); return; }
  3744. openQuoteDlg(rows[0]);
  3745. }
  3746. function submitQuotes() {
  3747. const rows = selectedQuotes().filter((x) => x.status === "草稿");
  3748. if (!rows.length) { toast("请勾选草稿状态的报价单"); return; }
  3749. rows.forEach((x) => { x.status = "待审批"; x.alert = true; });
  3750. toast(`已提交 ${rows.length} 张报价单审批`);
  3751. renderQuotes();
  3752. }
  3753. function genQuotePdf() {
  3754. const rows = selectedQuotes();
  3755. if (!rows.length) { toast("请先勾选报价单"); return; }
  3756. toast("演示:已生成 " + rows.map((x) => x.no).join("、"));
  3757. }
  3758. function voidQuotes() {
  3759. const rows = selectedQuotes();
  3760. if (!rows.length) { toast("请先勾选报价单"); return; }
  3761. rows.forEach((x) => { x.status = "已作废"; x.alert = false; });
  3762. selectedQuoteIds.clear();
  3763. toast("已作废所选报价单");
  3764. renderQuotes();
  3765. }
  3766. let ctTab = "全部";
  3767. let ctStatusFilter = "";
  3768. let ctPage = 1;
  3769. let ctPageSize = 20;
  3770. let ctSeq = 12;
  3771. let selectedCtIds = new Set();
  3772. let expandedCtIds = new Set();
  3773. let scanCtIds = [];
  3774. let contracts = [
  3775. { id: 1, no: "SX/X-JT20260930002", title: "山峡-合肥健宇环保", customer: "合肥健宇环保科技有限公司", kind: "框架销售合同", att: "双章", product: "98%工业硫酸 98% 100吨", amount: 0, freight: 0, deposit: 0, status: "执行中", signDate: "2026-09-30", owner: "万金强", created: "2026-09-30 10:42:57", alert: true },
  3776. { id: 2, no: "SX/X-JT20260930003", title: "山峡-安庆汇辰", customer: "安庆汇辰科技有限公司", kind: "销售合同", att: "", product: "一水柠檬酸 99.50% 0.125吨", amount: 1375, freight: 0, deposit: 0, status: "执行中", signDate: "2026-09-30", owner: "万金强", created: "2026-09-30 10:23:41" },
  3777. { id: 3, no: "SX/X-JT20260930001", title: "山峡-安徽金兽", customer: "安徽金兽医药科技有限公司", kind: "销售合同", att: "双章", product: "片碱 2吨", amount: 7400, freight: 0, deposit: 0, status: "已完成", signDate: "2026-09-30", owner: "万金强", created: "2026-09-30 10:16:21" },
  3778. { id: 4, no: "SG-GX2026-01-0031", title: "安庆曙光-山峡", customer: "安庆市曙光供销运输有限责任公司", kind: "采购合同", att: "单章", product: "32%液碱 32% 800吨<br>50%液碱 50% 600吨", amount: 1108000, freight: 0, deposit: 0, status: "执行中", signDate: "2026-09-30", owner: "万金强", created: "2026-09-30 10:15:44" },
  3779. { id: 5, no: "CG10120260930002", title: "山峡-安徽昊帆", customer: "安徽昊帆生物有限公司", kind: "销售合同", att: "双章", product: "98%工业硫酸 98% 500千克", amount: 1250, freight: 0, deposit: 0, status: "执行中", signDate: "2026-09-30", owner: "万金强", created: "2026-09-30 14:44:56" },
  3780. { id: 6, no: "SX/X-JT20261003001", title: "山峡-上饶敏达", customer: "上饶市敏达精细化工有限公司", kind: "销售合同", att: "双章", product: "硫酸 98% 30吨", amount: 27000, freight: 0, deposit: 0, status: "待审批", signDate: "2026-10-03", owner: "万金强", created: "2026-10-03 09:18:20", alert: true },
  3781. { id: 7, no: "SX/X-JT20261004001", title: "山峡-宜昌安达", customer: "宜昌安达危运有限公司", kind: "销售合同", att: "", product: "98%工业硫酸 32吨", amount: 28800, freight: 0, deposit: 0, status: "草稿", signDate: "2026-10-04", owner: "万金强", created: "2026-10-04 11:06:12" },
  3782. { id: 8, no: "SX/X-JT20260928001", title: "山峡-安徽紫荆花", customer: "安徽紫荆花新型材料科技有限公司", kind: "销售合同", att: "单章", product: "聚合氯化铝 5吨", amount: 10000, freight: 0, deposit: 0, status: "执行中", signDate: "2026-09-28", owner: "万金强", created: "2026-09-28 16:02:08" }
  3783. ];
  3784. function ctDateOf(x) { return (x.signDate || x.created || "").slice(0, 10); }
  3785. function filteredContracts() {
  3786. const q = (document.getElementById("ctQ")?.value || "").trim();
  3787. let rows = contracts.filter((x) => x.status !== "已作废" || ctTab === "全部");
  3788. if (ctTab === "昨日") rows = rows.filter((x) => ctDateOf(x) === dayShift(today, -1));
  3789. else if (ctTab === "今日") rows = rows.filter((x) => ctDateOf(x) === today);
  3790. else if (ctTab === "明日") rows = rows.filter((x) => ctDateOf(x) === dayShift(today, 1));
  3791. else if (ctTab === "本周") {
  3792. const [a, b] = quoteWeekRange();
  3793. rows = rows.filter((x) => { const d = ctDateOf(x); return d >= a && d <= b; });
  3794. }
  3795. if (ctStatusFilter === "待审核") rows = rows.filter((x) => x.status === "待审批");
  3796. if (q) rows = rows.filter((x) => [x.no, x.title, x.customer, x.product].join(" ").includes(q));
  3797. return rows;
  3798. }
  3799. function toggleCt(id, on) {
  3800. if (on) selectedCtIds.add(id); else selectedCtIds.delete(id);
  3801. renderContracts();
  3802. }
  3803. function toggleCtAll(on) {
  3804. filteredContracts().forEach((x) => on ? selectedCtIds.add(x.id) : selectedCtIds.delete(x.id));
  3805. renderContracts();
  3806. }
  3807. function toggleCtExpand(id) {
  3808. if (expandedCtIds.has(id)) expandedCtIds.delete(id); else expandedCtIds.add(id);
  3809. renderContracts();
  3810. }
  3811. function selectedContracts() { return contracts.filter((x) => selectedCtIds.has(x.id)); }
  3812. function setCtStatus(st) {
  3813. ctStatusFilter = ctStatusFilter === st ? "" : st;
  3814. ctPage = 1;
  3815. renderContracts();
  3816. }
  3817. function ctAttHtml(x) {
  3818. const pdf = x.att ? `<span class="pdf-chip"><span class="pdf-box">PDF</span><em>${x.att}附件</em></span>` : "";
  3819. return `<div class="ct-att">${pdf}<button class="up-chip" type="button" title="上传扫描件" onclick="openCtScan(${x.id})"><svg viewBox="0 0 24 24"><path d="M12 16V6M8 10l4-4 4 4"/><path d="M5 18h14"/></svg></button></div>`;
  3820. }
  3821. function renderContracts() {
  3822. const rows = filteredContracts();
  3823. const pages = Math.max(1, Math.ceil(rows.length / ctPageSize));
  3824. if (ctPage > pages) ctPage = pages;
  3825. const start = (ctPage - 1) * ctPageSize;
  3826. const pageRows = rows.slice(start, start + ctPageSize);
  3827. const allOn = pageRows.length && pageRows.every((x) => selectedCtIds.has(x.id));
  3828. document.getElementById("ctSelHint").textContent = `已选 ${selectedCtIds.size} 项`;
  3829. const waitN = contracts.filter((x) => x.status === "待审批").length;
  3830. const ctStats = document.getElementById("ctStats");
  3831. if (ctStats) {
  3832. if (ctStatusFilter === "已生效" || ctStatusFilter === "本月") ctStatusFilter = "";
  3833. ctStats.innerHTML = waitReviewBanner(waitN, ctStatusFilter === "待审核", "setCtStatus('待审核')", "查看待审合同");
  3834. }
  3835. const badge = document.getElementById("ctBadge");
  3836. if (badge) {
  3837. const n = contracts.filter((x) => x.status === "待审批" || x.status === "草稿").length;
  3838. badge.textContent = n;
  3839. badge.style.display = n ? "" : "none";
  3840. }
  3841. document.getElementById("ctTable").innerHTML = pageRows.length ? `<table class="erp-table ct-table">
  3842. <thead><tr>
  3843. <th class="quote-expand"></th>
  3844. <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="toggleCtAll(this.checked)" /></th>
  3845. <th class="col-no"><span class="th-sort">合同编号</span></th>
  3846. <th><span class="th-sort">合同名称</span></th>
  3847. <th><span class="th-sort">客户名称</span></th>
  3848. <th><span class="th-sort">合同类型</span></th>
  3849. <th><span class="th-sort">合同附件</span></th>
  3850. <th><span class="th-sort">产品信息</span></th>
  3851. <th><span class="th-sort">合同总金额 (元)</span></th>
  3852. <th><span class="th-sort">合同金额</span></th>
  3853. <th><span class="th-sort">运费</span></th>
  3854. <th><span class="th-sort">桶押金</span></th>
  3855. <th><span class="th-sort">合同状态</span></th>
  3856. <th><span class="th-sort">签订日期</span></th>
  3857. <th><span class="th-sort">负责人</span></th>
  3858. <th><span class="th-sort">创建时间</span></th>
  3859. </tr></thead>
  3860. <tbody>${pageRows.map((x) => {
  3861. const open = expandedCtIds.has(x.id);
  3862. const total = Number(x.amount || 0) + Number(x.freight || 0) + Number(x.deposit || 0);
  3863. const alert = x.alert || x.status === "待审批";
  3864. return `<tr class="${alert ? "row-alert" : ""}">
  3865. <td class="quote-expand ${open ? "on" : ""}" onclick="toggleCtExpand(${x.id})">${open ? "▾" : "▸"}</td>
  3866. <td class="chk"><input type="checkbox" ${selectedCtIds.has(x.id) ? "checked" : ""} onchange="toggleCt(${x.id}, this.checked)" /></td>
  3867. <td class="cell-no col-no"><button class="quote-link" type="button" onclick="toggleCtExpand(${x.id})">${x.no}</button></td>
  3868. <td class="ct-name"><button class="quote-link" type="button" onclick="toggleCtExpand(${x.id})">${x.title}<span class="sub">(${x.no})</span></button></td>
  3869. <td class="ct-cust">${x.customer}</td>
  3870. <td>${x.kind}</td>
  3871. <td>${ctAttHtml(x)}</td>
  3872. <td class="ct-prod">${x.product}</td>
  3873. <td class="amt-red">${quoteMoney(total)}</td>
  3874. <td class="amt-red">${quoteMoney(x.amount)}</td>
  3875. <td class="amt-red">${quoteMoney(x.freight)}</td>
  3876. <td class="amt-red">${quoteMoney(x.deposit)}</td>
  3877. <td class="ct-st">${x.status}</td>
  3878. <td>${x.signDate}</td>
  3879. <td>${x.owner}</td>
  3880. <td>${x.created}</td>
  3881. </tr>${open ? `<tr class="quote-sub"><td></td><td></td><td colspan="14">合同 ${x.no} ${x.kind} ${x.status} 签订 ${x.signDate} 负责人 ${x.owner} 产品 ${String(x.product).replace(/<br>/g, " / ")}</td></tr>` : ""}`;
  3882. }).join("")}</tbody></table>` : `<div class="empty">暂无合同</div>`;
  3883. const nums = [];
  3884. const maxBtn = Math.min(pages, 6);
  3885. for (let i = 1; i <= maxBtn; i++) nums.push(i);
  3886. document.getElementById("ctPager").innerHTML = `
  3887. <span class="muted">共 ${rows.length} 条</span>
  3888. <select class="page-size" onchange="ctPageSize=+this.value;ctPage=1;renderContracts()">
  3889. <option value="20" ${ctPageSize === 20 ? "selected" : ""}>20条/页</option>
  3890. <option value="50" ${ctPageSize === 50 ? "selected" : ""}>50条/页</option>
  3891. </select>
  3892. ${nums.map((n) => `<button type="button" class="${n === ctPage ? "on" : ""}" onclick="ctPage=${n};renderContracts()">${n}</button>`).join("")}
  3893. ${pages > 6 ? `<span class="muted">… ${pages}</span>` : ""}
  3894. <span class="muted">前往</span>
  3895. <input class="quote-goto" type="number" min="1" max="${pages}" value="${ctPage}" onchange="ctPage=Math.min(${pages}, Math.max(1, +this.value||1));renderContracts()" />
  3896. <span class="muted">页</span>`;
  3897. }
  3898. function openCtDlg(copyFrom) {
  3899. const f = document.getElementById("ctForm");
  3900. f.reset();
  3901. f.signDate.value = today;
  3902. f.owner.value = "万金强";
  3903. document.getElementById("ctDlgTitle").textContent = copyFrom ? "复制新增合同" : "新增合同";
  3904. if (copyFrom) {
  3905. f.customer.value = copyFrom.customer;
  3906. f.title.value = copyFrom.title;
  3907. f.kind.value = copyFrom.kind;
  3908. f.product.value = String(copyFrom.product).replace(/<br>/g, " / ");
  3909. f.amount.value = copyFrom.amount;
  3910. f.freight.value = copyFrom.freight || 0;
  3911. f.deposit.value = copyFrom.deposit || 0;
  3912. f.owner.value = copyFrom.owner || "万金强";
  3913. }
  3914. document.getElementById("ctDialog").showModal();
  3915. }
  3916. function saveContract() {
  3917. const f = document.getElementById("ctForm");
  3918. if (!f.reportValidity()) return;
  3919. ctSeq += 1;
  3920. const no = "SX/X-JT" + today.replaceAll("-", "") + String(ctSeq).padStart(3, "0");
  3921. const now = today + " " + new Date().toTimeString().slice(0, 8);
  3922. contracts.unshift({
  3923. id: Date.now(), no, title: f.title.value.trim(), customer: f.customer.value.trim(),
  3924. kind: f.kind.value, att: "", product: f.product.value.trim(), amount: +f.amount.value,
  3925. freight: +f.freight.value || 0, deposit: +f.deposit.value || 0, status: "草稿",
  3926. signDate: f.signDate.value, owner: f.owner.value.trim() || "万金强", created: now
  3927. });
  3928. document.getElementById("ctDialog").close();
  3929. toast("已保存草稿 " + no);
  3930. renderContracts();
  3931. }
  3932. function copyContract() {
  3933. const rows = selectedContracts();
  3934. if (rows.length !== 1) { toast("请勾选一份合同后再复制"); return; }
  3935. openCtDlg(rows[0]);
  3936. }
  3937. function submitContracts() {
  3938. const rows = selectedContracts().filter((x) => x.status === "草稿");
  3939. if (!rows.length) { toast("请勾选草稿状态的合同"); return; }
  3940. rows.forEach((x) => { x.status = "待审批"; x.alert = true; });
  3941. toast(`已提交 ${rows.length} 份合同审批`);
  3942. renderContracts();
  3943. }
  3944. function toggleCtExport(e) {
  3945. e.stopPropagation();
  3946. document.getElementById("ctExportDrop").classList.toggle("open");
  3947. }
  3948. function exportContracts(scope) {
  3949. document.getElementById("ctExportDrop").classList.remove("open");
  3950. const n = scope === "all" ? filteredContracts().length : Math.min(ctPageSize, filteredContracts().length);
  3951. toast(`演示:已导出 ${n} 条合同`);
  3952. }
  3953. function openCtScan(id) {
  3954. scanCtIds = id ? [id] : selectedCtIds.size ? [...selectedCtIds] : [];
  3955. if (!scanCtIds.length) { toast("请先勾选合同,或直接点行内上传"); return; }
  3956. const f = document.getElementById("ctScanForm");
  3957. f.reset();
  3958. const names = contracts.filter((x) => scanCtIds.includes(x.id)).map((x) => x.no).join("、");
  3959. document.getElementById("ctScanHint").textContent = "将为 " + names + " 上传双章 / 单章扫描件。";
  3960. document.getElementById("ctScanDialog").showModal();
  3961. }
  3962. function saveCtScan() {
  3963. const f = document.getElementById("ctScanForm");
  3964. if (!f.reportValidity()) return;
  3965. const att = f.att.value;
  3966. scanCtIds.forEach((id) => {
  3967. const x = contracts.find((c) => c.id === id);
  3968. if (x) x.att = att;
  3969. });
  3970. document.getElementById("ctScanDialog").close();
  3971. toast("已上传" + att + "扫描件");
  3972. renderContracts();
  3973. }
  3974. const vehicles = [
  3975. { plate: "皖H57837", trailer: "皖HE155", type: "柴油罐车", media: "柴油", driver: "周建军", escort: "李强", km: 188900, status: "运行", tankDue: "2026-10-12", insDue: "2026-12-01", nextMaint: 190000 },
  3976. { plate: "皖HA3690", trailer: "皖HA368", type: "甲醇罐车", media: "甲醇", driver: "陈伟", escort: "王磊", km: 145200, status: "维保中", tankDue: "2027-03-08", insDue: "2026-10-20", nextMaint: 146000 },
  3977. { plate: "皖LB1283", trailer: "皖LB129", type: "液氨罐车", media: "液氨", driver: "胡明", escort: "赵鹏", km: 100480, status: "运行", tankDue: "2026-11-02", insDue: "2027-01-15", nextMaint: 104000 },
  3978. { plate: "皖K92881", trailer: "皖K929", type: "柴油罐车", media: "柴油", driver: "吴涛", escort: "孙斌", km: 213100, status: "运行", tankDue: "2027-06-18", insDue: "2026-09-30", nextMaint: 215000 },
  3979. { plate: "皖H61208", trailer: "皖HE208", type: "硫酸罐车", media: "硫酸", driver: "郑浩", escort: "刘洋", km: 66180, status: "停运", tankDue: "2026-10-05", insDue: "2026-11-11", nextMaint: 68000 },
  3980. { plate: "皖K73155", trailer: "皖KE155", type: "盐酸罐车", media: "盐酸", driver: "李强", escort: "周建军", km: 121050, status: "运行", tankDue: "2027-02-14", insDue: "2026-12-18", nextMaint: 124000 },
  3981. { plate: "皖L88201", trailer: "皖LE201", type: "液碱罐车", media: "液碱", driver: "王磊", escort: "陈伟", km: 159400, status: "运行", tankDue: "2027-04-22", insDue: "2027-02-08", nextMaint: 162000 },
  3982. { plate: "皖H90366", trailer: "皖HE366", type: "柴油罐车", media: "柴油", driver: "孙斌", escort: "吴涛", km: 91880, status: "运行", tankDue: "2026-12-30", insDue: "2026-11-20", nextMaint: 94000 }
  3983. ];
  3984. function hazPlate(v) {
  3985. if (!v) return "—";
  3986. const plate = typeof v === "string" ? v : v.plate;
  3987. const row = typeof v === "string" ? vehicles.find((x) => x.plate === v) : v;
  3988. const trailer = row && row.trailer ? `${row.trailer}挂` : "";
  3989. return `危${plate}${trailer}`;
  3990. }
  3991. let fuels = [
  3992. { plate: "皖H57837", date: "2026-09-21", station: "中石化猇亭站", oil: "0# 柴油", liters: 300, amount: 2220, km: 185210, note: "", attachments: [{ name: "猇亭站小票_0921.jpg" }] },
  3993. { plate: "皖H57837", date: "2026-09-28", station: "中石化猇亭站", oil: "0# 柴油", liters: 320, amount: 2368, km: 186420, note: "", attachments: [{ name: "猇亭站小票_0928.jpg" }] },
  3994. { plate: "皖HA3690", date: "2026-09-10", station: "公司内部油库", oil: "0# 柴油", liters: 350, amount: 2520, km: 141500, note: "", attachments: [{ name: "内库出库单_0910.jpg" }] },
  3995. { plate: "皖HA3690", date: "2026-09-22", station: "中石化夷陵站", oil: "0# 柴油", liters: 340, amount: 2516, km: 142200, note: "", attachments: [{ name: "夷陵站小票_0922.jpg" }] },
  3996. { plate: "皖LB1283", date: "2026-09-20", station: "中石油伍家岗", oil: "0# 柴油", liters: 260, amount: 1924, km: 96800, note: "", attachments: [{ name: "伍家岗小票_0920.jpg" }] },
  3997. { plate: "皖LB1283", date: "2026-09-27", station: "中石油伍家岗", oil: "0# 柴油", liters: 280, amount: 2072, km: 97850, note: "", attachments: [{ name: "伍家岗发票_0927.pdf" }] },
  3998. { plate: "皖K92881", date: "2026-09-18", station: "中石化夷陵站", oil: "0# 柴油", liters: 380, amount: 2736, km: 208900, note: "", attachments: [{ name: "夷陵站小票_0918.jpg" }] },
  3999. { plate: "皖K92881", date: "2026-09-26", station: "公司内部油库", oil: "0# 柴油", liters: 410, amount: 2952, km: 210110, note: "夜班补油", attachments: [{ name: "内库出库单_0926.jpg" }] },
  4000. { plate: "皖H61208", date: "2026-09-16", station: "中石化猇亭站", oil: "0# 柴油", liters: 220, amount: 1628, km: 64200, note: "", attachments: [{ name: "猇亭站小票_0916.jpg" }] },
  4001. { plate: "皖K73155", date: "2026-09-19", station: "中石油伍家岗", oil: "0# 柴油", liters: 240, amount: 1776, km: 116800, note: "", attachments: [{ name: "伍家岗小票_0919.jpg" }] },
  4002. { plate: "皖L88201", date: "2026-09-24", station: "中石化夷陵站", oil: "0# 柴油", liters: 280, amount: 2072, km: 154100, note: "", attachments: [{ name: "夷陵站小票_0924.jpg" }] },
  4003. { plate: "皖H90366", date: "2026-09-25", station: "公司内部油库", oil: "0# 柴油", liters: 250, amount: 1850, km: 88200, note: "", attachments: [{ name: "内库出库单_0925.jpg" }] },
  4004. { plate: "皖H57837", date: "2026-10-01", station: "中石化猇亭站", oil: "0# 柴油", liters: 310, amount: 2294, km: 186420, note: "", attachments: [{ name: "猇亭站小票_1001.jpg" }] },
  4005. { plate: "皖H57837", date: "2026-10-08", station: "中石化猇亭站", oil: "0# 柴油", liters: 360, amount: 2664, km: 188900, note: "", attachments: [{ name: "猇亭站小票_1008.jpg" }] },
  4006. { plate: "皖HA3690", date: "2026-10-01", station: "中石化夷陵站", oil: "0# 柴油", liters: 280, amount: 2072, km: 142880, note: "", attachments: [{ name: "夷陵站小票_1001.jpg" }] },
  4007. { plate: "皖HA3690", date: "2026-10-07", station: "中石化夷陵站", oil: "0# 柴油", liters: 320, amount: 2368, km: 145200, note: "", attachments: [{ name: "夷陵站小票_1007.jpg" }] },
  4008. { plate: "皖LB1283", date: "2026-10-01", station: "中石油伍家岗", oil: "0# 柴油", liters: 250, amount: 1850, km: 97850, note: "", attachments: [{ name: "伍家岗小票_1001.jpg" }] },
  4009. { plate: "皖LB1283", date: "2026-10-08", station: "中石油伍家岗", oil: "0# 柴油", liters: 290, amount: 2146, km: 100480, note: "", attachments: [{ name: "伍家岗小票_1008.jpg" }] },
  4010. { plate: "皖K92881", date: "2026-10-01", station: "公司内部油库", oil: "0# 柴油", liters: 360, amount: 2664, km: 210330, note: "", attachments: [{ name: "内库出库单_1001.jpg" }] },
  4011. { plate: "皖K92881", date: "2026-10-09", station: "公司内部油库", oil: "0# 柴油", liters: 420, amount: 3108, km: 213100, note: "", attachments: [{ name: "内库出库单_1009.jpg" }] },
  4012. { plate: "皖H61208", date: "2026-10-01", station: "中石化猇亭站", oil: "0# 柴油", liters: 180, amount: 1332, km: 65420, note: "停运前补油", attachments: [{ name: "猇亭站小票_1001b.jpg" }] },
  4013. { plate: "皖H61208", date: "2026-10-03", station: "中石化猇亭站", oil: "0# 柴油", liters: 90, amount: 666, km: 66180, note: "", attachments: [{ name: "猇亭站小票_1003.jpg" }] },
  4014. { plate: "皖K73155", date: "2026-10-01", station: "中石油伍家岗", oil: "0# 柴油", liters: 220, amount: 1628, km: 118600, note: "", attachments: [{ name: "伍家岗小票_1001b.jpg" }] },
  4015. { plate: "皖K73155", date: "2026-10-08", station: "中石油伍家岗", oil: "0# 柴油", liters: 260, amount: 1924, km: 121050, note: "", attachments: [{ name: "伍家岗小票_1008b.jpg" }] },
  4016. { plate: "皖L88201", date: "2026-10-01", station: "中石化夷陵站", oil: "0# 柴油", liters: 300, amount: 2220, km: 156200, note: "", attachments: [{ name: "夷陵站小票_1001b.jpg" }] },
  4017. { plate: "皖L88201", date: "2026-10-08", station: "中石化夷陵站", oil: "0# 柴油", liters: 330, amount: 2442, km: 159400, note: "", attachments: [{ name: "夷陵站小票_1008.jpg" }] },
  4018. { plate: "皖H90366", date: "2026-10-01", station: "公司内部油库", oil: "0# 柴油", liters: 260, amount: 1924, km: 89400, note: "", attachments: [{ name: "内库出库单_1001b.jpg" }] },
  4019. { plate: "皖H90366", date: "2026-10-08", station: "公司内部油库", oil: "0# 柴油", liters: 280, amount: 2072, km: 91880, note: "", attachments: [{ name: "内库出库单_1008.jpg" }] }
  4020. ];
  4021. let etcs = [
  4022. { applicant: "周建军", plate: "皖H57837", type: "ETC", date: "2026-09-12", km: 185210, amount: 600, used: 480, note: "新车上牌后开通", status: "已提交", attachments: [{ name: "ETC开通凭证_0912.jpg" }] },
  4023. { applicant: "吴九华", plate: "皖HA3690", type: "ETC", date: "2026-09-25", km: 142880, amount: 800, used: 650, note: "", status: "已提交", attachments: [{ name: "ETC开通凭证.jpg" }] },
  4024. { applicant: "胡明", plate: "皖LB1283", type: "ETC", date: "2026-09-18", km: 96800, amount: 500, used: 420, note: "", status: "已提交", attachments: [{ name: "ETC充值_0918.jpg" }] },
  4025. { applicant: "吴涛", plate: "皖K92881", type: "ETC", date: "2026-09-08", km: 208900, amount: 400, used: 300, note: "", status: "已提交", attachments: [{ name: "ETC充值_0908.jpg" }] },
  4026. { applicant: "吴涛", plate: "皖K92881", type: "ETC", date: "2026-09-26", km: 210110, amount: 500, used: 380, note: "", status: "已提交", attachments: [{ name: "ETC充值_0926.jpg" }] },
  4027. { applicant: "郑浩", plate: "皖H61208", type: "ETC", date: "2026-09-16", km: 64200, amount: 300, used: 180, note: "", status: "已提交", attachments: [{ name: "ETC充值_0916.jpg" }] },
  4028. { applicant: "李强", plate: "皖K73155", type: "ETC", date: "2026-09-19", km: 116800, amount: 450, used: 360, note: "", status: "已提交", attachments: [{ name: "ETC充值_0919.jpg" }] },
  4029. { applicant: "王磊", plate: "皖L88201", type: "ETC", date: "2026-09-24", km: 154100, amount: 700, used: 520, note: "", status: "已提交", attachments: [{ name: "ETC充值_0924.jpg" }] },
  4030. { applicant: "孙斌", plate: "皖H90366", type: "ETC", date: "2026-09-25", km: 88200, amount: 400, used: 290, note: "", status: "已提交", attachments: [{ name: "ETC充值_0925.jpg" }] },
  4031. { applicant: "周建军", plate: "皖H57837", type: "ETC", date: "2026-10-01", km: 186420, amount: 800, used: 620, note: "", status: "已提交", attachments: [{ name: "ETC充值_1001.jpg" }] },
  4032. { applicant: "周建军", plate: "皖H57837", type: "ETC", date: "2026-10-08", km: 188900, amount: 500, used: 410, note: "", status: "已提交", attachments: [{ name: "ETC充值_1008.jpg" }] },
  4033. { applicant: "陈伟", plate: "皖HA3690", type: "ETC", date: "2026-10-07", km: 145200, amount: 1000, used: 780, note: "", status: "已提交", attachments: [{ name: "ETC充值_1007.jpg" }] },
  4034. { applicant: "胡明", plate: "皖LB1283", type: "ETC", date: "2026-10-01", km: 97850, amount: 600, used: 480, note: "", status: "已提交", attachments: [{ name: "ETC充值_1001b.jpg" }] },
  4035. { applicant: "胡明", plate: "皖LB1283", type: "ETC", date: "2026-10-08", km: 100480, amount: 400, used: 310, note: "", status: "已提交", attachments: [{ name: "ETC充值_1008b.jpg" }] },
  4036. { applicant: "吴涛", plate: "皖K92881", type: "ETC", date: "2026-10-01", km: 210330, amount: 700, used: 540, note: "", status: "已提交", attachments: [{ name: "ETC充值_1001c.jpg" }] },
  4037. { applicant: "吴涛", plate: "皖K92881", type: "ETC", date: "2026-10-09", km: 213100, amount: 650, used: 500, note: "", status: "已提交", attachments: [{ name: "ETC充值_1009.jpg" }] },
  4038. { applicant: "郑浩", plate: "皖H61208", type: "ETC", date: "2026-10-03", km: 66180, amount: 200, used: 90, note: "停运前充值", status: "已提交", attachments: [{ name: "ETC充值_1003.jpg" }] },
  4039. { applicant: "李强", plate: "皖K73155", type: "ETC", date: "2026-10-01", km: 118600, amount: 500, used: 390, note: "", status: "已提交", attachments: [{ name: "ETC充值_1001d.jpg" }] },
  4040. { applicant: "李强", plate: "皖K73155", type: "ETC", date: "2026-10-08", km: 121050, amount: 480, used: 360, note: "", status: "已提交", attachments: [{ name: "ETC充值_1008c.jpg" }] },
  4041. { applicant: "王磊", plate: "皖L88201", type: "ETC", date: "2026-10-01", km: 156200, amount: 750, used: 580, note: "", status: "已提交", attachments: [{ name: "ETC充值_1001e.jpg" }] },
  4042. { applicant: "王磊", plate: "皖L88201", type: "ETC", date: "2026-10-08", km: 159400, amount: 600, used: 450, note: "", status: "已提交", attachments: [{ name: "ETC充值_1008d.jpg" }] },
  4043. { applicant: "孙斌", plate: "皖H90366", type: "ETC", date: "2026-10-08", km: 91880, amount: 550, used: 410, note: "", status: "已提交", attachments: [{ name: "ETC充值_1008e.jpg" }] }
  4044. ];
  4045. let oilcards = [
  4046. { applicant: "周建军", plate: "皖H57837", type: "油卡", date: "2026-09-12", km: 185210, amount: 2000, used: 1680, note: "月初充值", status: "已提交", attachments: [{ name: "油卡充值_0912.jpg" }] },
  4047. { applicant: "吴九华", plate: "皖HA3690", type: "油卡", date: "2026-09-25", km: 142880, amount: 2500, used: 2100, note: "", status: "已提交", attachments: [{ name: "油卡充值_0925.jpg" }] },
  4048. { applicant: "胡明", plate: "皖LB1283", type: "油卡", date: "2026-09-18", km: 96800, amount: 1800, used: 1420, note: "", status: "已提交", attachments: [{ name: "油卡充值_0918.jpg" }] },
  4049. { applicant: "吴涛", plate: "皖K92881", type: "油卡", date: "2026-09-08", km: 208900, amount: 2200, used: 1900, note: "", status: "已提交", attachments: [{ name: "油卡充值_0908.jpg" }] },
  4050. { applicant: "吴涛", plate: "皖K92881", type: "油卡", date: "2026-09-26", km: 210110, amount: 1500, used: 980, note: "", status: "已提交", attachments: [{ name: "油卡充值_0926.jpg" }] },
  4051. { applicant: "郑浩", plate: "皖H61208", type: "油卡", date: "2026-09-16", km: 64200, amount: 800, used: 520, note: "", status: "已提交", attachments: [{ name: "油卡充值_0916.jpg" }] },
  4052. { applicant: "李强", plate: "皖K73155", type: "油卡", date: "2026-09-19", km: 116800, amount: 1600, used: 1280, note: "", status: "已提交", attachments: [{ name: "油卡充值_0919.jpg" }] },
  4053. { applicant: "王磊", plate: "皖L88201", type: "油卡", date: "2026-09-24", km: 154100, amount: 2400, used: 1860, note: "", status: "已提交", attachments: [{ name: "油卡充值_0924.jpg" }] },
  4054. { applicant: "孙斌", plate: "皖H90366", type: "油卡", date: "2026-09-25", km: 88200, amount: 1200, used: 860, note: "", status: "已提交", attachments: [{ name: "油卡充值_0925.jpg" }] },
  4055. { applicant: "周建军", plate: "皖H57837", type: "油卡", date: "2026-10-01", km: 186420, amount: 2200, used: 1740, note: "", status: "已提交", attachments: [{ name: "油卡充值_1001.jpg" }] },
  4056. { applicant: "周建军", plate: "皖H57837", type: "油卡", date: "2026-10-08", km: 188900, amount: 1500, used: 1100, note: "", status: "已提交", attachments: [{ name: "油卡充值_1008.jpg" }] },
  4057. { applicant: "陈伟", plate: "皖HA3690", type: "油卡", date: "2026-10-07", km: 145200, amount: 2800, used: 2300, note: "", status: "已提交", attachments: [{ name: "油卡充值_1007.jpg" }] },
  4058. { applicant: "胡明", plate: "皖LB1283", type: "油卡", date: "2026-10-01", km: 97850, amount: 1600, used: 1280, note: "", status: "已提交", attachments: [{ name: "油卡充值_1001b.jpg" }] },
  4059. { applicant: "胡明", plate: "皖LB1283", type: "油卡", date: "2026-10-08", km: 100480, amount: 1200, used: 860, note: "", status: "已提交", attachments: [{ name: "油卡充值_1008b.jpg" }] },
  4060. { applicant: "吴涛", plate: "皖K92881", type: "油卡", date: "2026-10-01", km: 210330, amount: 2000, used: 1640, note: "", status: "已提交", attachments: [{ name: "油卡充值_1001c.jpg" }] },
  4061. { applicant: "吴涛", plate: "皖K92881", type: "油卡", date: "2026-10-09", km: 213100, amount: 1800, used: 1420, note: "", status: "已提交", attachments: [{ name: "油卡充值_1009.jpg" }] },
  4062. { applicant: "郑浩", plate: "皖H61208", type: "油卡", date: "2026-10-03", km: 66180, amount: 500, used: 220, note: "停运前充值", status: "已提交", attachments: [{ name: "油卡充值_1003.jpg" }] },
  4063. { applicant: "李强", plate: "皖K73155", type: "油卡", date: "2026-10-01", km: 118600, amount: 1700, used: 1310, note: "", status: "已提交", attachments: [{ name: "油卡充值_1001d.jpg" }] },
  4064. { applicant: "李强", plate: "皖K73155", type: "油卡", date: "2026-10-08", km: 121050, amount: 1400, used: 980, note: "", status: "已提交", attachments: [{ name: "油卡充值_1008c.jpg" }] },
  4065. { applicant: "王磊", plate: "皖L88201", type: "油卡", date: "2026-10-01", km: 156200, amount: 2100, used: 1680, note: "", status: "已提交", attachments: [{ name: "油卡充值_1001e.jpg" }] },
  4066. { applicant: "王磊", plate: "皖L88201", type: "油卡", date: "2026-10-08", km: 159400, amount: 1600, used: 1210, note: "", status: "已提交", attachments: [{ name: "油卡充值_1008d.jpg" }] },
  4067. { applicant: "孙斌", plate: "皖H90366", type: "油卡", date: "2026-10-08", km: 91880, amount: 1300, used: 940, note: "", status: "已提交", attachments: [{ name: "油卡充值_1008e.jpg" }] }
  4068. ];
  4069. let maints = [
  4070. { plate: "皖H57837", type: "定期保养", inDate: "2026-03-12", outDate: "2026-03-12", shop: "公司维修班", amount: 580, nextKm: 178000, item: "机油机滤" },
  4071. { plate: "皖H57837", type: "定期保养", inDate: "2026-09-08", outDate: "2026-09-08", shop: "公司维修班", amount: 620, nextKm: 190000, item: "机油机滤、检查罐阀" },
  4072. { plate: "皖H57837", type: "故障维修", inDate: "2026-10-06", outDate: "2026-10-07", shop: "宜昌安达汽修", amount: 980, nextKm: 192000, item: "更换半轴油封" },
  4073. { plate: "皖HA3690", type: "定期保养", inDate: "2026-04-18", outDate: "2026-04-18", shop: "公司维修班", amount: 720, nextKm: 138000, item: "机油机滤、空滤" },
  4074. { plate: "皖HA3690", type: "故障维修", inDate: "2026-09-28", outDate: "", shop: "宜昌安达汽修", amount: 1860, nextKm: 146000, item: "更换刹车片、排查气路" },
  4075. { plate: "皖LB1283", type: "定期保养", inDate: "2026-05-06", outDate: "2026-05-06", shop: "公司维修班", amount: 560, nextKm: 98000, item: "机油机滤" },
  4076. { plate: "皖LB1283", type: "故障维修", inDate: "2026-10-03", outDate: "2026-10-04", shop: "宜昌安达汽修", amount: 1280, nextKm: 104000, item: "更换轮胎、动平衡" },
  4077. { plate: "皖K92881", type: "定期保养", inDate: "2026-06-22", outDate: "2026-06-22", shop: "公司维修班", amount: 890, nextKm: 208000, item: "机油机滤、检查罐阀" },
  4078. { plate: "皖K92881", type: "故障维修", inDate: "2026-10-08", outDate: "2026-10-09", shop: "宜昌安达汽修", amount: 2100, nextKm: 215000, item: "更换刹车片、调整间隙" },
  4079. { plate: "皖H61208", type: "定期保养", inDate: "2026-02-16", outDate: "2026-02-16", shop: "公司维修班", amount: 540, nextKm: 62000, item: "机油机滤" },
  4080. { plate: "皖H61208", type: "罐体检验", inDate: "2026-09-20", outDate: "", shop: "省特检院宜昌分院", amount: 4800, nextKm: 68000, item: "罐体定期检验" },
  4081. { plate: "皖K73155", type: "定期保养", inDate: "2026-04-09", outDate: "2026-04-09", shop: "公司维修班", amount: 640, nextKm: 114000, item: "机油机滤、检查泵阀" },
  4082. { plate: "皖K73155", type: "故障维修", inDate: "2026-10-02", outDate: "2026-10-03", shop: "宜昌安达汽修", amount: 1560, nextKm: 124000, item: "更换水泵、处理渗漏" },
  4083. { plate: "皖L88201", type: "定期保养", inDate: "2026-05-21", outDate: "2026-05-21", shop: "公司维修班", amount: 700, nextKm: 150000, item: "机油机滤" },
  4084. { plate: "皖L88201", type: "故障维修", inDate: "2026-08-14", outDate: "2026-08-15", shop: "宜昌安达汽修", amount: 1100, nextKm: 156000, item: "更换皮带、张紧轮" },
  4085. { plate: "皖H90366", type: "定期保养", inDate: "2026-02-27", outDate: "2026-02-27", shop: "公司维修班", amount: 580, nextKm: 86000, item: "机油机滤" },
  4086. { plate: "皖H90366", type: "故障维修", inDate: "2026-10-05", outDate: "2026-10-05", shop: "公司维修班", amount: 430, nextKm: 94000, item: "更换雨刮、检查灯光" }
  4087. ];
  4088. let expSeq = 12;
  4089. let preSeq = 4;
  4090. let expTab = "全部";
  4091. let expFinKind = "";
  4092. let expRejectId = null;
  4093. let expFiles = [];
  4094. let expLines = [];
  4095. let selectedInvIds = [];
  4096. const EXP_TYPES = ["差旅费", "交通费", "办公费", "招待费", "通讯费", "采购费", "其他"];
  4097. const EXP_STAFF = [
  4098. { name: "吴九华", dept: "行政人事" },
  4099. { name: "周建军", dept: "运输部" },
  4100. { name: "陈伟", dept: "运输部" },
  4101. { name: "胡明", dept: "生产部" },
  4102. { name: "吴涛", dept: "销售部" },
  4103. { name: "郑浩", dept: "仓储部" },
  4104. { name: "刘洋", dept: "仓储部" },
  4105. { name: "孙斌", dept: "运输部" }
  4106. ];
  4107. let invWallet = [
  4108. { id: 1, name: "高铁票_0920.pdf", amount: 580, date: "2026-09-20" },
  4109. { id: 2, name: "住宿发票.pdf", amount: 420, date: "2026-09-20" },
  4110. { id: 3, name: "过路过桥发票.pdf", amount: 186, date: "2026-09-27" },
  4111. { id: 4, name: "办公耗材专票.pdf", amount: 890, date: "2026-09-26" },
  4112. { id: 5, name: "通讯费发票.pdf", amount: 128, date: "2026-09-25" }
  4113. ];
  4114. const EXP_BUDGET = {
  4115. "运输部": { total: 20000, used: 15800 },
  4116. "生产部": { total: 12000, used: 4100 },
  4117. "行政人事": { total: 8000, used: 2200 },
  4118. "销售部": { total: 15000, used: 14200 },
  4119. "仓储部": { total: 6000, used: 1800 }
  4120. };
  4121. let expLoans = [
  4122. { no: "JK-20260901-01", user: "周建军", dept: "运输部", amount: 3000, remain: 3000, date: "2026-09-01", status: "未清" },
  4123. { no: "JK-20260812-02", user: "吴涛", dept: "销售部", amount: 2000, remain: 0, date: "2026-08-12", status: "已清" }
  4124. ];
  4125. let preExps = [
  4126. { no: "SQ-20260918-001", user: "周建军", dept: "运输部", type: "出差", amount: 2000, date: "2026-09-18", status: "已批准", remark: "宜昌客户拜访差旅预估" },
  4127. { no: "SQ-20260928-002", user: "吴涛", dept: "销售部", type: "招待", amount: 8000, date: "2026-09-28", status: "待财务初审", remark: "重点客户来访招待" },
  4128. { no: "SQ-20260920-003", user: "郑浩", dept: "仓储部", type: "采购", amount: 1200, date: "2026-09-20", status: "已驳回", remark: "劳保采购,事由不清" }
  4129. ];
  4130. let expenses = [
  4131. {
  4132. id: 1, no: "BX-20260928-007", user: "周建军", dept: "运输部", type: "差旅费", amount: 1280,
  4133. date: "2026-09-20", submit: "2026-09-28", status: "待财务初审",
  4134. remark: "宜昌客户拜访往返,关联备用金申请",
  4135. invoices: ["高铁票.pdf", "住宿发票.pdf", "行程单.jpg"],
  4136. payWay: "冲销借款", loanNo: "JK-20260901-01", bank: "",
  4137. preNo: "SQ-20260918-001", overBudget: false,
  4138. logs: [
  4139. { t: "2026-09-18 09:10", who: "周建军", act: "备用金申请已批准", note: "出差预估 2,000 元" },
  4140. { t: "2026-09-28 16:20", who: "周建军", act: "提交报销", note: "贴票 3 张,申请冲销借款" }
  4141. ]
  4142. },
  4143. {
  4144. id: 2, no: "BX-20260927-006", user: "陈伟", dept: "运输部", type: "交通费", amount: 360,
  4145. date: "2026-09-27", submit: "2026-09-27", status: "待财务初审",
  4146. remark: "过路过桥及停车",
  4147. invoices: ["过路过桥发票.pdf", "停车费收据.jpg"],
  4148. payWay: "个人银行卡", loanNo: "", bank: "6222****6180",
  4149. preNo: "", overBudget: false,
  4150. logs: [
  4151. { t: "2026-09-27 11:02", who: "陈伟", act: "提交报销", note: "贴票 2 张" },
  4152. { t: "2026-09-27 15:40", who: "运输部负责人", act: "部门审核通过", note: "业务真实,金额合理" }
  4153. ]
  4154. },
  4155. {
  4156. id: 3, no: "BX-20260926-005", user: "吴九华", dept: "行政人事", type: "办公费", amount: 890,
  4157. date: "2026-09-26", submit: "2026-09-26", status: "待终审",
  4158. remark: "打印耗材与档案袋",
  4159. invoices: ["增值税普通发票.pdf"],
  4160. payWay: "对公转账", loanNo: "", bank: "山峡代工办公耗材户",
  4161. preNo: "", overBudget: false, finalRole: "部门经理",
  4162. logs: [
  4163. { t: "2026-09-26 10:11", who: "吴九华", act: "提交报销", note: "" },
  4164. { t: "2026-09-26 14:02", who: "行政人事负责人", act: "部门审核通过", note: "部门工作需要" },
  4165. { t: "2026-09-26 17:20", who: "财务会计", act: "财务初审通过", note: "发票真伪核验通过,预算有额度" }
  4166. ]
  4167. },
  4168. {
  4169. id: 4, no: "BX-20260925-004", user: "胡明", dept: "生产部", type: "通讯费", amount: 128,
  4170. date: "2026-09-25", submit: "2026-09-25", status: "待付款",
  4171. remark: "9 月通讯补贴",
  4172. invoices: ["通讯费发票.pdf"],
  4173. payWay: "个人银行卡", loanNo: "", bank: "6228****0902",
  4174. preNo: "", overBudget: false, finalRole: "部门经理",
  4175. logs: [
  4176. { t: "2026-09-25 09:00", who: "胡明", act: "提交报销", note: "" },
  4177. { t: "2026-09-25 10:20", who: "生产部负责人", act: "部门审核通过", note: "属岗位补贴" },
  4178. { t: "2026-09-25 11:10", who: "财务会计", act: "财务初审通过", note: "票据合规" },
  4179. { t: "2026-09-25 16:00", who: "部门经理", act: "终审同意", note: "小额,部门经理终审" }
  4180. ]
  4181. },
  4182. {
  4183. id: 5, no: "BX-20260924-003", user: "郑浩", dept: "仓储部", type: "交通费", amount: 210,
  4184. date: "2026-09-24", submit: "", status: "草稿",
  4185. remark: "送样至检测站",
  4186. invoices: [],
  4187. payWay: "个人银行卡", loanNo: "", bank: "",
  4188. preNo: "", overBudget: false, logs: [{ t: "2026-09-24 18:01", who: "郑浩", act: "保存草稿", note: "待补发票" }]
  4189. },
  4190. {
  4191. id: 6, no: "BX-20260922-002", user: "吴涛", dept: "销售部", type: "招待费", amount: 2460,
  4192. date: "2026-09-22", submit: "2026-09-22", status: "已驳回",
  4193. remark: "客户来访接待",
  4194. invoices: ["餐饮小票.jpg"],
  4195. payWay: "个人银行卡", loanNo: "", bank: "6222****0770",
  4196. preNo: "", overBudget: false,
  4197. logs: [
  4198. { t: "2026-09-22 20:10", who: "吴涛", act: "提交报销", note: "" },
  4199. { t: "2026-09-23 09:30", who: "销售部负责人", act: "部门审核通过", note: "来访属实" },
  4200. { t: "2026-09-23 14:18", who: "财务会计", act: "退回补资料", note: "发票抬头不符、缺少酒水清单,请补增值税发票后重提" }
  4201. ]
  4202. },
  4203. {
  4204. id: 7, no: "BX-20260918-001", user: "刘洋", dept: "仓储部", type: "办公费", amount: 540,
  4205. date: "2026-09-18", submit: "2026-09-18", status: "已归档",
  4206. remark: "劳保用品补购",
  4207. invoices: ["专票.pdf", "入库单.jpg"],
  4208. payWay: "对公转账", loanNo: "", bank: "劳保供应商对公户",
  4209. preNo: "", overBudget: false, voucher: "记-202609-118",
  4210. logs: [
  4211. { t: "2026-09-18 11:00", who: "刘洋", act: "提交报销", note: "" },
  4212. { t: "2026-09-18 15:00", who: "仓储部负责人", act: "部门审核通过", note: "属库区劳保" },
  4213. { t: "2026-09-19 09:10", who: "财务会计", act: "财务初审通过", note: "可抵扣专票" },
  4214. { t: "2026-09-19 11:00", who: "部门经理", act: "终审同意", note: "" },
  4215. { t: "2026-09-19 16:40", who: "出纳", act: "已付款", note: "对公支付 540 元" },
  4216. { t: "2026-09-20 10:05", who: "会计", act: "制单归档", note: "凭证 记-202609-118,发票装订" }
  4217. ]
  4218. },
  4219. {
  4220. id: 8, no: "BX-20260930-008", user: "吴涛", dept: "销售部", type: "招待费", amount: 12800,
  4221. date: "2026-09-30", submit: "2026-09-30", status: "待终审",
  4222. remark: "重点客户商务招待,超部门预算",
  4223. invoices: ["餐饮专票.pdf", "签到表.pdf"],
  4224. payWay: "对公转账", loanNo: "", bank: "酒店对公户",
  4225. preNo: "SQ-20260928-002", overBudget: true, finalRole: "总经理",
  4226. logs: [
  4227. { t: "2026-09-30 08:40", who: "吴涛", act: "提交报销", note: "备用金申请尚未批完,同步提报" },
  4228. { t: "2026-09-30 10:12", who: "销售部负责人", act: "部门审核通过", note: "业务真实,金额偏高已说明" },
  4229. { t: "2026-09-30 14:55", who: "财务会计", act: "财务初审通过", note: "票据合规;销售部预算剩余不足,转总经理专项终审" }
  4230. ]
  4231. },
  4232. {
  4233. id: 9, no: "BX-20260929-009", user: "孙斌", dept: "运输部", type: "差旅费", amount: 4860,
  4234. date: "2026-09-12", submit: "2026-09-29", status: "待财务初审",
  4235. remark: "安庆办事处驻点一周",
  4236. invoices: ["住宿发票.pdf", "餐饮发票.pdf", "高铁票.pdf"],
  4237. payWay: "个人银行卡", loanNo: "", bank: "6222****4410",
  4238. preNo: "", overBudget: false, finalRole: "财务主管",
  4239. logs: [
  4240. { t: "2026-09-29 09:18", who: "孙斌", act: "提交报销", note: "发生日距今已超 15 天" },
  4241. { t: "2026-09-29 11:30", who: "运输部负责人", act: "部门审核通过", note: "驻点任务属实" }
  4242. ]
  4243. },
  4244. {
  4245. id: 10, no: "BX-20260801-010", user: "赵鹏", dept: "生产部", type: "交通费", amount: 180,
  4246. date: "2026-07-10", submit: "2026-09-20", status: "已驳回",
  4247. remark: "取样送检打车",
  4248. invoices: ["打车票.jpg"],
  4249. payWay: "个人银行卡", loanNo: "", bank: "6217****3301",
  4250. preNo: "", overBudget: false,
  4251. logs: [
  4252. { t: "2026-09-20 08:00", who: "赵鹏", act: "提交报销", note: "" },
  4253. { t: "2026-09-20 09:40", who: "财务会计", act: "逾期退回", note: "业务结束已超过 45 天,按制度不予受理" }
  4254. ]
  4255. },
  4256. {
  4257. id: 11, no: "BX-20260921-011", user: "李强", dept: "运输部", type: "差旅费", amount: 960,
  4258. date: "2026-09-21", submit: "2026-09-21", status: "待归档",
  4259. remark: "随车押运补贴及餐费",
  4260. invoices: ["餐票.pdf"],
  4261. payWay: "冲销借款", loanNo: "JK-20260901-01", bank: "",
  4262. preNo: "", overBudget: false, paid: 960,
  4263. logs: [
  4264. { t: "2026-09-21 17:00", who: "李强", act: "提交报销", note: "" },
  4265. { t: "2026-09-22 09:00", who: "运输部负责人", act: "部门审核通过", note: "" },
  4266. { t: "2026-09-22 11:20", who: "财务会计", act: "财务初审通过", note: "" },
  4267. { t: "2026-09-22 15:00", who: "部门经理", act: "终审同意", note: "" },
  4268. { t: "2026-09-23 10:10", who: "出纳", act: "已冲借款", note: "冲销 JK-20260901-01 中 960 元" }
  4269. ]
  4270. }
  4271. ];
  4272. let currentMod = "plan";
  4273. const PLACE_PAGES = {
  4274. fav: { parent: "收藏", title: "收藏", desc: "常用功能与页面可在此固定,便于快速进入。" },
  4275. todo: { parent: "待办列表", title: "待办列表", desc: "汇总审批、预警与待处理事项。" },
  4276. "crm-quote": { parent: "CRM管理", title: "报价单管理", desc: "客户询价、报价编制与跟进。" },
  4277. "crm-contract": { parent: "CRM管理", title: "合同管理", desc: "合同起草、审批、归档与履约跟踪。" },
  4278. "crm-price": { parent: "CRM管理", title: "调价函管理", desc: "调价申请、函件下达与生效记录。" },
  4279. "crm-customer": { parent: "CRM管理", title: "客户管理", desc: "客户档案、信用与跟进记录。", coming: "使用山峡系统原系统页面" },
  4280. "fin-ar": { parent: "财务管理", title: "应收应付", desc: "客户应收、供应商应付对账与核销。" },
  4281. "fin-view": { parent: "财务管理", title: "视图中心", desc: "财务看板与关键指标视图。" },
  4282. "fin-petty": { parent: "财务管理", title: "备用金管理", desc: "备用金领用、核销与结余。" },
  4283. "fin-config": { parent: "财务管理", title: "基础配置", desc: "科目、审批流与财务基础参数。" },
  4284. "hr-org": { parent: "人事管理", title: "组织架构", desc: "部门编制与汇报关系。" },
  4285. weigh: { parent: "运输管理", title: "地磅管理", desc: "过磅记录、皮重净重与出厂对账。" },
  4286. freight: { parent: "运输管理", title: "运费结算", desc: "按计划、车辆与线路核算运费,对账后结算。" },
  4287. system: { parent: "系统管理", title: "系统管理", desc: "用户、角色、权限与基础参数。" },
  4288. "sys-dict": { parent: "系统管理", title: "字典管理", desc: "维护系统枚举、编码与基础数据字典。" }
  4289. };
  4290. let prodPage = 1;
  4291. let prodPageSize = 20;
  4292. let prodSeq = 7463;
  4293. let selectedProdIds = new Set();
  4294. let prodCatFilter = "";
  4295. let prodCats = ["其他", "酸类", "碱类", "溶剂", "助剂", "包装"];
  4296. const PROD_SEED = [
  4297. { name: "硼砂", cat: "其他", brand: "益泉", alias: "", spec: "98%", price: "", hz: "", py: "BS" },
  4298. { name: "38%工业硫酸", cat: "", brand: "", alias: "", spec: "38%", price: "", hz: "毒", py: "GYSF" },
  4299. { name: "吨桶", cat: "其他", brand: "", alias: "", spec: "/", price: "", hz: "", py: "DT" },
  4300. { name: "双氧水", cat: "", brand: "", alias: "", spec: "AR", price: "", hz: "爆", py: "SYS" },
  4301. { name: "活性炭", cat: "", brand: "", alias: "", spec: "", price: "", hz: "", py: "HXT" },
  4302. { name: "液氨", cat: "", brand: "", alias: "", spec: "", price: "", hz: "", py: "YA" },
  4303. { name: "乙二醇", cat: "", brand: "", alias: "", spec: "", price: "", hz: "", py: "YEC" },
  4304. { name: "次氯酸钠", cat: "", brand: "", alias: "", spec: "A型I类", price: "", hz: "", py: "CLSN" },
  4305. { name: "亚硫酸氢钠", cat: "", brand: "", alias: "", spec: "AR", price: "", hz: "", py: "YLSQN" },
  4306. { name: "工业硫酸", cat: "", brand: "", alias: "", spec: "≥93%", price: "", hz: "毒", py: "GYSF" },
  4307. { name: "氨水", cat: "", brand: "", alias: "", spec: "25%-28%", price: "", hz: "", py: "AS" },
  4308. { name: "工业硫酸", cat: "", brand: "", alias: "", spec: "53%-56%", price: "", hz: "毒", py: "GYSF" },
  4309. { name: "片碱", cat: "碱类", brand: "", alias: "氢氧化钠", spec: "99%", price: "3700", hz: "", py: "PJ" },
  4310. { name: "32%液碱", cat: "碱类", brand: "", alias: "", spec: "32%", price: "840", hz: "", py: "YJ" },
  4311. { name: "50%液碱", cat: "碱类", brand: "", alias: "", spec: "50%", price: "1270", hz: "", py: "YJ" },
  4312. { name: "工业盐酸", cat: "酸类", brand: "", alias: "", spec: "31%", price: "200", hz: "毒", py: "YS" },
  4313. { name: "一水柠檬酸", cat: "助剂", brand: "", alias: "", spec: "99.50%", price: "11000", hz: "", py: "NMS" },
  4314. { name: "聚合氯化铝", cat: "助剂", brand: "", alias: "PAC", spec: "28%", price: "2000", hz: "", py: "JHLSL" },
  4315. { name: "氯化钠", cat: "助剂", brand: "", alias: "工业盐", spec: "国标", price: "1400", hz: "", py: "LHN" },
  4316. { name: "98%工业硫酸", cat: "酸类", brand: "", alias: "", spec: "98%", price: "900", hz: "毒", py: "GYSF" }
  4317. ];
  4318. const products = [];
  4319. (function buildProducts() {
  4320. for (let i = 0; i < 143; i++) {
  4321. const s = PROD_SEED[i % PROD_SEED.length];
  4322. const no = "SXWL" + String(7463 - i).padStart(6, "0");
  4323. const extraOff = i > 40 && i % 11 === 0;
  4324. products.push({
  4325. id: i + 1,
  4326. no,
  4327. name: s.name,
  4328. cat: s.cat,
  4329. brand: s.brand,
  4330. alias: s.alias,
  4331. spec: s.spec,
  4332. price: s.price,
  4333. hz: s.hz,
  4334. py: s.py,
  4335. status: extraOff ? "下架" : "上架"
  4336. });
  4337. }
  4338. })();
  4339. function dash(v) { return v === undefined || v === null || String(v).trim() === "" ? "-" : v; }
  4340. function filteredProducts() {
  4341. const q = (document.getElementById("prodQ")?.value || "").trim().toUpperCase();
  4342. let rows = products.slice();
  4343. if (prodCatFilter) rows = rows.filter((x) => (x.cat || "未分类") === prodCatFilter);
  4344. if (q) {
  4345. rows = rows.filter((x) => [x.no, x.name, x.brand, x.alias, x.spec, x.py, x.cat].join(" ").toUpperCase().includes(q));
  4346. }
  4347. return rows;
  4348. }
  4349. function selectedProds() { return products.filter((x) => selectedProdIds.has(x.id)); }
  4350. function toggleProd(id, on) {
  4351. if (on) selectedProdIds.add(id); else selectedProdIds.delete(id);
  4352. renderProducts();
  4353. }
  4354. function toggleProdAll(on) {
  4355. const rows = filteredProducts();
  4356. const start = (prodPage - 1) * prodPageSize;
  4357. rows.slice(start, start + prodPageSize).forEach((x) => on ? selectedProdIds.add(x.id) : selectedProdIds.delete(x.id));
  4358. renderProducts();
  4359. }
  4360. function fillProdCatSelect(val) {
  4361. const sel = document.getElementById("prodCatSelect");
  4362. if (!sel) return;
  4363. sel.innerHTML = `<option value="">未分类</option>` + prodCats.map((c) => `<option>${c}</option>`).join("");
  4364. if (val != null) sel.value = val;
  4365. }
  4366. function renderProducts() {
  4367. const rows = filteredProducts();
  4368. const pages = Math.max(1, Math.ceil(rows.length / prodPageSize));
  4369. if (prodPage > pages) prodPage = pages;
  4370. const start = (prodPage - 1) * prodPageSize;
  4371. const pageRows = rows.slice(start, start + prodPageSize);
  4372. const allOn = pageRows.length && pageRows.every((x) => selectedProdIds.has(x.id));
  4373. document.getElementById("prodSelHint").textContent = `已选 ${selectedProdIds.size} 项`;
  4374. const filt = `<span class="th-filt"></span>`;
  4375. document.getElementById("prodTable").innerHTML = pageRows.length ? `<table class="erp-table prod-table">
  4376. <thead><tr>
  4377. <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="toggleProdAll(this.checked)" /></th>
  4378. <th class="col-idx">序号</th>
  4379. <th>产品编号${filt}</th>
  4380. <th>产品名称${filt}</th>
  4381. <th>产品分类${filt}</th>
  4382. <th>品牌${filt}</th>
  4383. <th>别名${filt}</th>
  4384. <th>规格型号${filt}</th>
  4385. <th>标准价(元)${filt}</th>
  4386. <th>状态</th>
  4387. </tr></thead>
  4388. <tbody>${pageRows.map((x, i) => `<tr>
  4389. <td class="chk"><input type="checkbox" ${selectedProdIds.has(x.id) ? "checked" : ""} onchange="toggleProd(${x.id}, this.checked)" /></td>
  4390. <td>${start + i + 1}</td>
  4391. <td>${x.no}</td>
  4392. <td><button class="quote-link" type="button" onclick="openProdDlg(${x.id})">${x.name}</button>${x.hz ? `<span class="prod-hz">${x.hz}</span>` : ""}</td>
  4393. <td>${x.cat || ""}</td>
  4394. <td>${dash(x.brand)}</td>
  4395. <td>${dash(x.alias)}</td>
  4396. <td>${dash(x.spec)}</td>
  4397. <td class="${x.price ? "" : "prod-price"}">${dash(x.price)}</td>
  4398. <td><span class="prod-st${x.status === "下架" ? " off" : ""}">${x.status}</span></td>
  4399. </tr>`).join("")}</tbody></table>` : `<div class="empty">暂无产品</div>`;
  4400. const nums = [];
  4401. const show = Math.min(pages, 6);
  4402. for (let n = 1; n <= show; n++) nums.push(n);
  4403. document.getElementById("prodPager").innerHTML = `
  4404. <span class="muted">共 ${rows.length} 条</span>
  4405. <select class="page-size" onchange="prodPageSize=+this.value;prodPage=1;renderProducts()">
  4406. <option value="20" ${prodPageSize === 20 ? "selected" : ""}>20条/页</option>
  4407. <option value="50" ${prodPageSize === 50 ? "selected" : ""}>50条/页</option>
  4408. </select>
  4409. <button type="button" onclick="prodPage=Math.max(1, prodPage-1);renderProducts()">‹</button>
  4410. ${nums.map((n) => `<button type="button" class="${n === prodPage ? "on" : ""}" onclick="prodPage=${n};renderProducts()">${n}</button>`).join("")}
  4411. ${pages > 6 ? `<span class="muted">…</span><button type="button" class="${prodPage === pages ? "on" : ""}" onclick="prodPage=${pages};renderProducts()">${pages}</button>` : ""}
  4412. <button type="button" onclick="prodPage=Math.min(${pages}, prodPage+1);renderProducts()">›</button>
  4413. <span class="muted">前往</span>
  4414. <input class="quote-goto" type="number" min="1" max="${pages}" value="${prodPage}" onchange="prodPage=Math.min(${pages}, Math.max(1, +this.value||1));renderProducts()" />
  4415. <span class="muted">页</span>`;
  4416. }
  4417. function openProdDlg(id) {
  4418. const f = document.getElementById("prodForm");
  4419. f.reset();
  4420. const x = id ? products.find((p) => p.id === id) : null;
  4421. fillProdCatSelect(x ? (x.cat || "") : "");
  4422. document.getElementById("prodDlgTitle").textContent = x ? "编辑产品" : "新增产品";
  4423. f.id.value = x ? x.id : "";
  4424. if (x) {
  4425. f.name.value = x.name;
  4426. f.no.value = x.no;
  4427. f.brand.value = x.brand || "";
  4428. f.alias.value = x.alias || "";
  4429. f.spec.value = x.spec || "";
  4430. f.price.value = x.price || "";
  4431. f.py.value = x.py || "";
  4432. f.hz.value = x.hz || "";
  4433. f.status.value = x.status;
  4434. }
  4435. document.getElementById("prodDialog").showModal();
  4436. }
  4437. function saveProd() {
  4438. const f = document.getElementById("prodForm");
  4439. if (!f.reportValidity()) return;
  4440. const payload = {
  4441. name: f.name.value.trim(),
  4442. no: f.no.value.trim(),
  4443. cat: f.cat.value,
  4444. brand: f.brand.value.trim(),
  4445. alias: f.alias.value.trim(),
  4446. spec: f.spec.value.trim(),
  4447. price: f.price.value,
  4448. py: (f.py.value.trim() || f.name.value.trim()).toUpperCase(),
  4449. hz: f.hz.value,
  4450. status: f.status.value
  4451. };
  4452. const id = f.id.value ? +f.id.value : 0;
  4453. if (id) {
  4454. const x = products.find((p) => p.id === id);
  4455. Object.assign(x, payload);
  4456. toast("已保存 " + x.no);
  4457. } else {
  4458. prodSeq += 1;
  4459. if (!payload.no) payload.no = "SXWL" + String(prodSeq).padStart(6, "0");
  4460. products.unshift({ id: Date.now(), ...payload });
  4461. toast("已新增 " + payload.no);
  4462. prodPage = 1;
  4463. }
  4464. document.getElementById("prodDialog").close();
  4465. renderProducts();
  4466. }
  4467. function editSelectedProd() {
  4468. const rows = selectedProds();
  4469. if (rows.length !== 1) { toast("请勾选一条产品后再编辑"); return; }
  4470. openProdDlg(rows[0].id);
  4471. }
  4472. function deleteProds() {
  4473. const rows = selectedProds();
  4474. if (!rows.length) { toast("请先勾选产品"); return; }
  4475. const ids = new Set(rows.map((x) => x.id));
  4476. for (let i = products.length - 1; i >= 0; i--) {
  4477. if (ids.has(products[i].id)) products.splice(i, 1);
  4478. }
  4479. selectedProdIds.clear();
  4480. toast(`已删除 ${rows.length} 条产品`);
  4481. renderProducts();
  4482. }
  4483. function shelfProds(st) {
  4484. const rows = selectedProds();
  4485. if (!rows.length) { toast("请先勾选产品"); return; }
  4486. rows.forEach((x) => { x.status = st; });
  4487. toast(`已${st} ${rows.length} 条产品`);
  4488. renderProducts();
  4489. }
  4490. function toggleProdExport(e) {
  4491. e.stopPropagation();
  4492. document.getElementById("prodExportDrop").classList.toggle("open");
  4493. }
  4494. function exportProducts(scope) {
  4495. document.getElementById("prodExportDrop").classList.remove("open");
  4496. const n = scope === "page" ? Math.min(prodPageSize, filteredProducts().length) : filteredProducts().length;
  4497. toast(`演示:已导出 ${n} 条产品`);
  4498. }
  4499. function openProdCatDlg() {
  4500. renderProdCats();
  4501. document.getElementById("prodCatDialog").showModal();
  4502. }
  4503. function renderProdCats() {
  4504. const counts = {};
  4505. products.forEach((x) => {
  4506. const k = x.cat || "未分类";
  4507. counts[k] = (counts[k] || 0) + 1;
  4508. });
  4509. const keys = ["全部", ...prodCats, "未分类"];
  4510. document.getElementById("prodCatList").innerHTML = keys.map((c) => {
  4511. const n = c === "全部" ? products.length : (counts[c] || 0);
  4512. const on = (c === "全部" && !prodCatFilter) || prodCatFilter === c;
  4513. return `<button type="button" class="${on ? "on" : ""}" onclick="filterProdCat('${c}')"><span>${c}</span><span>${n}</span></button>`;
  4514. }).join("");
  4515. }
  4516. function filterProdCat(c) {
  4517. prodCatFilter = c === "全部" ? "" : c;
  4518. prodPage = 1;
  4519. renderProdCats();
  4520. renderProducts();
  4521. }
  4522. function addProdCat() {
  4523. const name = (document.getElementById("prodCatNew").value || "").trim();
  4524. if (!name) { toast("请输入分类名称"); return; }
  4525. if (prodCats.includes(name)) { toast("分类已存在"); return; }
  4526. prodCats.push(name);
  4527. document.getElementById("prodCatNew").value = "";
  4528. renderProdCats();
  4529. toast("已新增分类 " + name);
  4530. }
  4531. let currentCertId = null;
  4532. let certSeq = 4;
  4533. let transSeq = 5;
  4534. let planSeq = 21;
  4535. let planTab = "全部";
  4536. let planDateTab = "全部";
  4537. let archiveTab = "待审核";
  4538. let selectedArchiveIds = new Set();
  4539. let planCardFilter = "";
  4540. let selectedPlanIds = new Set();
  4541. let linkingPlanTaskIds = [];
  4542. let taskTab = "全部";
  4543. let selectedTaskIds = new Set();
  4544. let reportingTaskId = null;
  4545. const SITE_STAFF = ["刘洋(硫酸库)", "张伟(盐酸库)", "陈刚(分装库)", "王磊(液碱库)", "李强(现场班长)", "赵敏(精制岗)"];
  4546. let taskSeq = 3;
  4547. const TASKS = [
  4548. {
  4549. id: 1, no: "SXFZ20261001-001", kind: "稀释", status: "待执行",
  4550. date: "2026-10-01", orderDate: "2026-10-01", deliverDate: "2026-10-04",
  4551. orderBy: "周建军", handler: "刘洋(硫酸库)", maker: "郑丽娜", auditor: "", supervisor: "",
  4552. bucket: "桶循环", remark: "现场按规程佩戴防护",
  4553. planTime: "2026-10-04T08:00", planId: 7, planNo: "CK20260928001",
  4554. product: "硫酸", spec: "98%", pack: "槽罐车", qty: 30, unit: "吨",
  4555. assignee: "刘洋(硫酸库)",
  4556. lines: [{ planId: 7, planNo: "CK20260928001", product: "硫酸", model: "98%", pack: "槽罐车", unit: "吨", qty: 30, done: false }]
  4557. },
  4558. {
  4559. id: 2, no: "SXFZ20261001-002", kind: "精制", status: "执行中",
  4560. date: "2026-10-01", orderDate: "2026-10-01", deliverDate: "2026-10-03",
  4561. orderBy: "胡明", handler: "张伟(盐酸库)", maker: "郑丽娜", auditor: "", supervisor: "",
  4562. bucket: "全新桶", remark: "试剂线单独作业,不得混兑",
  4563. planTime: "2026-10-03T09:00", planId: 9, planNo: "RZ20260921012",
  4564. product: "工业盐酸", spec: "31%", pack: "槽罐车", qty: 28, unit: "吨",
  4565. assignee: "张伟(盐酸库)",
  4566. receivedAt: "2026-10-03T08:30",
  4567. lines: [{ planId: 9, planNo: "RZ20260921012", product: "工业盐酸", model: "31%", pack: "槽罐车", unit: "吨", qty: 28, done: false }]
  4568. },
  4569. {
  4570. id: 3, no: "SXFZ20260930-001", kind: "分装", status: "已完成",
  4571. date: "2026-09-30", orderDate: "2026-09-30", deliverDate: "2026-10-07",
  4572. orderBy: "调度员", handler: "陈刚(分装库)", maker: "郑丽娜", auditor: "", supervisor: "",
  4573. bucket: "桶循环", remark: "江峰",
  4574. planTime: "2026-10-02T14:00", planId: 8, planNo: "ZX20260925008",
  4575. product: "工业硫酸", spec: "98%", pack: "吨桶", qty: 22, unit: "吨",
  4576. assignee: "陈刚(分装库)",
  4577. receivedAt: "2026-10-02T13:40",
  4578. doneTime: "2026-10-02T17:20", doneQty: 22,
  4579. report: "22 吨已分装,桶循环。",
  4580. sheetPhotos: [{ name: "分装单_SXFZ20260930-001.jpg", type: "image/svg+xml", url: "data:image/svg+xml;charset=utf-8," + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="640" height="400"><rect width="100%" height="100%" fill="#f4faf6"/><rect x="24" y="24" width="592" height="352" fill="none" stroke="#167c4a" stroke-width="2"/><text x="320" y="90" text-anchor="middle" font-size="22" fill="#167c4a" font-family="sans-serif">山峡分装任务单</text><text x="320" y="140" text-anchor="middle" font-size="16" fill="#1c2430" font-family="sans-serif">SXFZ20260930-001 · 已完成</text><text x="320" y="190" text-anchor="middle" font-size="14" fill="#6a7380" font-family="sans-serif">工业硫酸 98% 吨桶 22吨</text><text x="320" y="240" text-anchor="middle" font-size="14" fill="#6a7380" font-family="sans-serif">经手人 陈刚 · 2026-10-02</text></svg>') }],
  4581. lines: [{ planId: 8, planNo: "ZX20260925008", product: "工业硫酸", model: "98%", pack: "吨桶", unit: "吨", qty: 22, done: true }]
  4582. }
  4583. ];
  4584. let linkingPlanId = null;
  4585. let waitBuyPlanId = null;
  4586. let waitTransPlanId = null;
  4587. let voidCertTarget = null;
  4588. let linkingCertId = null;
  4589. let linkingCertFocusIds = [];
  4590. let extraTransN = 0;
  4591. let archivingPlanId = null;
  4592. let currentTransId = null;
  4593. let attachBuyId = null;
  4594. const certFiles = { buy: null, trans: null, buyUrl: "", transUrl: "" };
  4595. const ocrBuySample = {
  4596. buyNo: "G36260499487476", buyDate: "2026-09-24",
  4597. buyer: "上饶市敏达精细化工有限公司", buyerLegal: "熊伟", buyerTel: "0793-8224115", buyerAddr: "上饶市胜利路96-3号",
  4598. seller: "安庆市山峡化工有限公司", sellerLegal: "吴春生", sellerTel: "0556-5348918", sellerAddr: "安徽省安庆市高新区霞虹路5号",
  4599. product: "硫酸", usage: "销售", buyQty: 450, unit: "吨", buyTimes: "多次有效",
  4600. buyFrom: "2026-09-24", buyDue: "2026-10-23",
  4601. buyPolice: "上饶市公安局信州分局禁毒大队", buyHandler: "潘佳", buyPoliceTel: "13755372147"
  4602. };
  4603. const ocrTransSample = {
  4604. transNo: "Y34260211715884", transDate: "2026-09-03",
  4605. owner: "铜陵锦帆物资有限责任公司", ownerLegal: "何红燕", ownerTel: "0562-2119658",
  4606. ownerAddr: "安徽省铜陵市滨江大道和铜港路交叉口滨江大道停车场",
  4607. pack: "槽罐", permitNo: "G34260528735347",
  4608. carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
  4609. mode: "汽车", plates: "皖HA3690、皖H57837、皖LB1283",
  4610. route: "自安徽省铜陵市铜官区经池州市至安徽省安庆市高新",
  4611. consignee: "安庆市山峡化工有限公司", transQty: 500,
  4612. transTimes: "多次有效", transFrom: "2026-09-03", transDue: "2026-12-02",
  4613. applicantUnit: "铜陵锦帆物资有限责任公司",
  4614. transPolice: "铜陵市公安局铜官分局环食药(禁毒)大队", transHandler: "桂强"
  4615. };
  4616. const ocrTransSample2 = {
  4617. transNo: "Y34260222800112", transDate: "2026-09-25",
  4618. owner: "铜陵锦帆物资有限责任公司", ownerLegal: "何红燕", ownerTel: "0562-2119658",
  4619. ownerAddr: "安徽省铜陵市滨江大道和铜港路交叉口滨江大道停车场",
  4620. pack: "槽罐", permitNo: "G36260499487476",
  4621. carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
  4622. mode: "汽车", plates: "皖K92881",
  4623. route: "自安徽省铜陵市经安庆至宜昌猇亭",
  4624. consignee: "安庆市山峡化工有限公司", transQty: 200,
  4625. transTimes: "多次有效", transFrom: "2026-09-25", transDue: "2026-12-24",
  4626. applicantUnit: "铜陵锦帆物资有限责任公司",
  4627. transPolice: "铜陵市公安局铜官分局环食药(禁毒)大队", transHandler: "桂强"
  4628. };
  4629. const ocrTransSamples = [ocrTransSample, ocrTransSample2];
  4630. let certs = [
  4631. {
  4632. id: 1, created: today, shipped: 40, ctrl: "易制毒",
  4633. ...ocrBuySample, product: "硫酸", unit: "吨",
  4634. buyFile: "购买备案证明样例.png", buyUrl: "购买备案证明样例.png",
  4635. transList: [
  4636. {
  4637. id: 1, created: today, shipped: 40,
  4638. transNo: "Y34260211715884", transDate: "2026-09-03",
  4639. owner: "铜陵锦帆物资有限责任公司", ownerLegal: "何红燕", ownerTel: "0562-2119658",
  4640. ownerAddr: "安徽省铜陵市滨江大道和铜港路交叉口滨江大道停车场",
  4641. pack: "槽罐", permitNo: "G36260499487476",
  4642. carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
  4643. mode: "汽车", plates: "皖HA3690、皖H57837、皖LB1283",
  4644. route: "自安徽省铜陵市铜官区经池州市至安徽省安庆市高新",
  4645. consignee: "安庆市山峡化工有限公司", transQty: 250,
  4646. transTimes: "多次有效", transFrom: "2026-09-03", transDue: "2026-12-02",
  4647. applicantUnit: "铜陵锦帆物资有限责任公司",
  4648. transPolice: "铜陵市公安局铜官分局环食药(禁毒)大队", transHandler: "桂强",
  4649. transFile: "运输备案证明样例.png", transUrl: "运输备案证明样例.png",
  4650. dispatches: [
  4651. { plate: "皖HA3690", driver: "周建军", qty: 90, shipped: 0 },
  4652. { plate: "皖H57837", driver: "陈伟", qty: 80, shipped: 0 },
  4653. { plate: "皖LB1283", driver: "胡明", qty: 80, shipped: 40 }
  4654. ]
  4655. },
  4656. {
  4657. id: 2, created: today, shipped: 0,
  4658. transNo: "Y34260222800112", transDate: "2026-09-25",
  4659. owner: "铜陵锦帆物资有限责任公司", ownerLegal: "何红燕", ownerTel: "0562-2119658",
  4660. ownerAddr: "安徽省铜陵市滨江大道和铜港路交叉口滨江大道停车场",
  4661. pack: "槽罐", permitNo: "G36260499487476",
  4662. carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
  4663. mode: "汽车", plates: "皖K92881",
  4664. route: "自安徽省铜陵市经安庆至宜昌猇亭",
  4665. consignee: "安庆市山峡化工有限公司", transQty: 200,
  4666. transTimes: "多次有效", transFrom: "2026-09-25", transDue: "2026-12-24",
  4667. applicantUnit: "铜陵锦帆物资有限责任公司",
  4668. transPolice: "铜陵市公安局铜官分局环食药(禁毒)大队", transHandler: "桂强",
  4669. transFile: "运输证_硫酸_2.jpg", transUrl: "",
  4670. dispatches: [
  4671. { plate: "皖K92881", driver: "吴涛", qty: 200, shipped: 0 }
  4672. ],
  4673. voided: true, voidedAt: today, voidQty: 200
  4674. },
  4675. {
  4676. id: 5, created: today, shipped: 0,
  4677. transNo: "Y36260433900188", transDate: "2026-09-26",
  4678. owner: "上饶市敏达精细化工有限公司", ownerLegal: "熊伟", ownerTel: "0793-8224115",
  4679. ownerAddr: "上饶市胜利路96-3号",
  4680. pack: "槽罐", permitNo: "G36260499487476",
  4681. carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
  4682. mode: "汽车", plates: "皖HA3690、皖H57837、皖K92881",
  4683. route: "自安庆市高新区至上饶市信州区",
  4684. consignee: "上饶市敏达精细化工有限公司", transQty: 180,
  4685. transTimes: "多次有效", transFrom: "2026-09-26", transDue: "2026-12-25",
  4686. applicantUnit: "上饶市敏达精细化工有限公司",
  4687. transPolice: "上饶市公安局信州分局禁毒大队", transHandler: "潘佳",
  4688. transFile: "运输证_敏达硫酸.jpg", transUrl: "",
  4689. dispatches: [
  4690. { plate: "皖HA3690", driver: "周建军", qty: 60, shipped: 0 },
  4691. { plate: "皖H57837", driver: "陈伟", qty: 60, shipped: 0 },
  4692. { plate: "皖K92881", driver: "吴涛", qty: 60, shipped: 0 }
  4693. ]
  4694. }
  4695. ]
  4696. },
  4697. {
  4698. id: 2, created: today, shipped: 16, ctrl: "易制爆",
  4699. buyNo: "G42100311880021", buyDate: "2026-09-20",
  4700. buyer: "宜昌爆破器材公司", buyerLegal: "张磊", buyerTel: "0717-8801122", buyerAddr: "宜昌市夷陵区发展大道18号",
  4701. seller: "安庆市山峡化工有限公司", sellerLegal: "吴春生", sellerTel: "0556-5348918", sellerAddr: "安徽省安庆市高新区霞虹路5号",
  4702. product: "硝酸铵", usage: "生产使用", buyQty: 80, unit: "吨", buyTimes: "多次有效",
  4703. buyFrom: "2026-09-20", buyDue: "2026-11-20",
  4704. buyPolice: "宜昌市公安局禁毒支队", buyHandler: "李敏", buyPoliceTel: "0717-1234567",
  4705. buyFile: "购买证_硝酸铵.jpg", buyUrl: "",
  4706. transList: [
  4707. {
  4708. id: 3, created: today, shipped: 16,
  4709. transNo: "Y42100322991008", transDate: "2026-09-20",
  4710. owner: "宜昌爆破器材公司", ownerLegal: "张磊", ownerTel: "0717-8801122",
  4711. ownerAddr: "宜昌市夷陵区发展大道18号",
  4712. pack: "袋装", permitNo: "G42100311880021",
  4713. carrier: "宜昌安达危运有限公司", carrierAddr: "宜昌市猇亭区/0717-6677889",
  4714. mode: "汽车", plates: "鄂E·危D077",
  4715. route: "自安庆市高新区至宜昌市夷陵区",
  4716. consignee: "宜昌爆破器材公司", transQty: 80,
  4717. transTimes: "多次有效", transFrom: "2026-09-20", transDue: "2026-11-20",
  4718. applicantUnit: "宜昌爆破器材公司",
  4719. transPolice: "宜昌市公安局禁毒支队", transHandler: "王芳",
  4720. transFile: "运输证_硝酸铵.jpg", transUrl: "",
  4721. dispatches: [
  4722. { plate: "鄂E·危D077", driver: "吴涛", qty: 80, shipped: 16 }
  4723. ]
  4724. }
  4725. ]
  4726. },
  4727. {
  4728. id: 3, created: today, shipped: 0, ctrl: "易制毒",
  4729. buyNo: "G34080266110031", buyDate: "2026-09-18",
  4730. buyer: "安庆市山峡化工有限公司", buyerLegal: "吴春生", buyerTel: "0556-5348918", buyerAddr: "安徽省安庆市高新区霞虹路5号",
  4731. seller: "铜陵锦帆物资有限责任公司", sellerLegal: "何红燕", sellerTel: "0562-2119658", sellerAddr: "安徽省铜陵市滨江大道和铜港路交叉口",
  4732. product: "硫酸", usage: "生产使用", buyQty: 500, unit: "吨", buyTimes: "多次有效",
  4733. buyFrom: "2026-09-18", buyDue: "2026-12-17",
  4734. buyPolice: "安庆市公安局高新分局禁毒大队", buyHandler: "刘洋", buyPoliceTel: "0556-5201100",
  4735. buyFile: "购买证_山峡采购硫酸.jpg", buyUrl: "",
  4736. transList: [
  4737. {
  4738. id: 4, created: today, shipped: 0,
  4739. transNo: "Y34080277220045", transDate: "2026-09-18",
  4740. owner: "铜陵锦帆物资有限责任公司", ownerLegal: "何红燕", ownerTel: "0562-2119658",
  4741. ownerAddr: "安徽省铜陵市滨江大道和铜港路交叉口滨江大道停车场",
  4742. pack: "槽罐", permitNo: "G34080266110031",
  4743. carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
  4744. mode: "汽车", plates: "皖LB1283、皖K92881",
  4745. route: "自安徽省铜陵市经安庆至上饶",
  4746. consignee: "安庆市山峡化工有限公司", transQty: 300,
  4747. transTimes: "多次有效", transFrom: "2026-09-18", transDue: "2026-12-17",
  4748. applicantUnit: "铜陵锦帆物资有限责任公司",
  4749. transPolice: "铜陵市公安局铜官分局环食药(禁毒)大队", transHandler: "桂强",
  4750. transFile: "运输证_山峡采购硫酸.jpg", transUrl: "",
  4751. dispatches: [
  4752. { plate: "皖LB1283", driver: "胡明", qty: 150, shipped: 0 },
  4753. { plate: "皖K92881", driver: "吴涛", qty: 150, shipped: 0 }
  4754. ]
  4755. },
  4756. {
  4757. id: 7, created: today, shipped: 0,
  4758. transNo: "Y34080277220118", transDate: "2026-09-28",
  4759. owner: "铜陵锦帆物资有限责任公司", ownerLegal: "何红燕", ownerTel: "0562-2119658",
  4760. ownerAddr: "安徽省铜陵市滨江大道和铜港路交叉口滨江大道停车场",
  4761. pack: "槽罐", permitNo: "G34080266110031",
  4762. carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
  4763. mode: "汽车", plates: "皖HA3690、皖H57837",
  4764. route: "自安徽省铜陵市至安庆市高新区",
  4765. consignee: "安庆市山峡化工有限公司", transQty: 160,
  4766. transTimes: "多次有效", transFrom: "2026-09-28", transDue: "2026-12-27",
  4767. applicantUnit: "铜陵锦帆物资有限责任公司",
  4768. transPolice: "铜陵市公安局铜官分局环食药(禁毒)大队", transHandler: "桂强",
  4769. transFile: "运输证_山峡采购硫酸_2.jpg", transUrl: "",
  4770. dispatches: [
  4771. { plate: "皖HA3690", driver: "周建军", qty: 80, shipped: 0 },
  4772. { plate: "皖H57837", driver: "陈伟", qty: 80, shipped: 0 }
  4773. ]
  4774. }
  4775. ]
  4776. },
  4777. {
  4778. id: 4, created: today, shipped: 0, ctrl: "易制毒",
  4779. buyNo: "G36260499510088", buyDate: "2026-09-28",
  4780. buyer: "上饶市敏达精细化工有限公司", buyerLegal: "熊伟", buyerTel: "0793-8224115", buyerAddr: "上饶市胜利路96-3号",
  4781. seller: "安庆市山峡化工有限公司", sellerLegal: "吴春生", sellerTel: "0556-5348918", sellerAddr: "安徽省安庆市高新区霞虹路5号",
  4782. product: "硫酸", usage: "生产使用", buyQty: 220, unit: "吨", buyTimes: "多次有效",
  4783. buyFrom: "2026-09-28", buyDue: "2026-12-27",
  4784. buyPolice: "上饶市公安局信州分局禁毒大队", buyHandler: "潘佳", buyPoliceTel: "13755372147",
  4785. buyFile: "购买证_敏达硫酸_2.jpg", buyUrl: "",
  4786. transList: [
  4787. {
  4788. id: 6, created: today, shipped: 0,
  4789. transNo: "Y36260455110076", transDate: "2026-09-28",
  4790. owner: "上饶市敏达精细化工有限公司", ownerLegal: "熊伟", ownerTel: "0793-8224115",
  4791. ownerAddr: "上饶市胜利路96-3号",
  4792. pack: "槽罐", permitNo: "G36260499510088",
  4793. carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
  4794. mode: "汽车", plates: "皖HA3690、皖H57837、皖LB1283、皖K92881",
  4795. route: "自安庆市高新区至上饶市信州区",
  4796. consignee: "上饶市敏达精细化工有限公司", transQty: 220,
  4797. transTimes: "多次有效", transFrom: "2026-09-28", transDue: "2026-12-27",
  4798. applicantUnit: "上饶市敏达精细化工有限公司",
  4799. transPolice: "上饶市公安局信州分局禁毒大队", transHandler: "潘佳",
  4800. transFile: "运输证_敏达硫酸_2.jpg", transUrl: "",
  4801. dispatches: [
  4802. { plate: "皖HA3690", driver: "周建军", qty: 55, shipped: 0 },
  4803. { plate: "皖H57837", driver: "陈伟", qty: 55, shipped: 0 },
  4804. { plate: "皖LB1283", driver: "胡明", qty: 55, shipped: 0 },
  4805. { plate: "皖K92881", driver: "吴涛", qty: 55, shipped: 0 }
  4806. ]
  4807. }
  4808. ]
  4809. }
  4810. ];
  4811. let plans = [
  4812. { id: 16, no: "PO202610110001", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "出库单", docKind: "开单类", salesman: "万金强", dept: "销售一部", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "40%工业硫酸", spec: "40%", pack: "槽罐车", unit: "吨", planQty: 32, realQty: 0, tractor: "皖K92881", driver: "吴涛", certId: null, transId: null, warehouseId: "h2so4-custom", source: "手工录入", sourceNo: "PO202610110001" },
  4813. { id: 1, no: "PO202508050191", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "出库单", docKind: "选单类", salesman: "杨磊", dept: "运输调度组", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 32, realQty: 0, tractor: "皖HA3690", driver: "周建军", certId: null, transId: null, warehouseId: null, source: "ERP", sourceNo: "JT202508050191" },
  4814. { id: 2, no: "PO202508050190", status: "已安排", date: "2026-10-03", deliverDate: "2026-10-03", type: "出库单", docKind: "选单类", salesman: "杨磊 / 杨帆", dept: "北京 / 上海", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "安徽东泰化学试剂有限公司", customer: "安徽东泰化学试剂有限公司", ctrl: "易制毒", product: "槽车运输", spec: "CP", pack: "槽罐车", unit: "吨", planQty: 32, realQty: 0, tractor: "皖HA3690", driver: "周建军", certId: 1, transId: 1, warehouseId: null, source: "ERP", sourceNo: "JT202508050190" },
  4815. { id: 3, no: "ZX202609280354", status: "已完成", date: "2026-10-02", deliverDate: "2026-10-03", type: "直销单", docKind: "开单类", salesman: "BLJT8482 / BLJT9", dept: "南京 / 上海", shipMode: "车辆运输", loadFrom: "安庆市山峡化工有限责任公司", settler: "南京市秦淮区化工危险品有限公司", customer: "南京市秦淮区化工危险品有限公司", ctrl: "", product: "30%以上烧碱", spec: "30%", pack: "槽罐车", unit: "吨", planQty: 33, realQty: 33, tractor: "皖LB1283", driver: "胡明", certId: null, transId: null, warehouseId: null, archived: false, source: "直销", sourceNo: "ZX202609280354" },
  4816. { id: 4, no: "PO202508050117", status: "已完成", date: "2026-10-02", deliverDate: "2026-10-02", type: "出库单", docKind: "选单类", salesman: "杨A3748", dept: "销售一部", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "安庆市山峡化工有限公司", customer: "安庆市山峡化工有限公司", ctrl: "", product: "30%以上烧碱", spec: "32%", pack: "槽罐车", unit: "吨", planQty: 6, realQty: 6, tractor: "皖H57837", driver: "陈伟", certId: null, transId: null, warehouseId: "naoh", archived: false, source: "ERP", sourceNo: "JT202508050117" },
  4817. { id: 5, no: "PO202508050116", status: "已完成", date: "2026-10-02", deliverDate: "2026-10-02", type: "出库单", docKind: "选单类", salesman: "杨A3748", dept: "销售一部", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "南京市秦淮区化工危险品有限公司", customer: "南京市秦淮区化工危险品有限公司", ctrl: "", product: "30%-烧碱", spec: "32%", pack: "槽罐车", unit: "吨", planQty: 8, realQty: 8, tractor: "皖K92881", driver: "吴涛", certId: null, transId: null, warehouseId: "naoh", archived: false, source: "ERP", sourceNo: "JT202508050116" },
  4818. { id: 6, no: "PO202508050109", status: "已完成", date: "2026-10-03", deliverDate: "2026-10-03", type: "出库单", docKind: "选单类", salesman: "杨A3742", dept: "销售一部", shipMode: "槽车运输", loadFrom: "武汉市鑫源化工有限公司", settler: "安徽鑫源新材料有限公司", customer: "安徽鑫源新材料有限公司", ctrl: "", product: "32%-烧碱", spec: "32%", pack: "槽罐车", unit: "吨", planQty: 15, realQty: 15, tractor: "鄂E·危D077", driver: "吴涛", certId: null, transId: null, warehouseId: "naoh", archived: false, source: "ERP", sourceNo: "JT202508050109" },
  4819. { id: 7, no: "CK20260928001", status: "已安排", date: "2026-09-30", deliverDate: "2026-10-03", type: "出库单", docKind: "选单类", salesman: "周建军", dept: "运输调度组", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 30, realQty: 0, tractor: "皖HA3690", driver: "周建军", certId: 1, transId: 1, warehouseId: null, source: "出库同步", sourceNo: "PO20260928001", taskNo: "SXFZ20261001-001" },
  4820. { id: 8, no: "ZX20260925008", status: "待安排", date: "2026-10-02", deliverDate: "2026-10-02", type: "直销单", docKind: "开单类", salesman: "吴涛", dept: "运输调度组", shipMode: "车辆运输", loadFrom: "山峡猇亭库区", settler: "宜昌爆破器材公司", customer: "宜昌爆破器材公司", ctrl: "易制爆", product: "硝酸铵", spec: "工业级", pack: "袋装", unit: "吨", planQty: 12, realQty: null, tractor: "", driver: "", certId: null, transId: null, warehouseId: null, waitNotify: true, source: "直销同步", sourceNo: "PO20260925008" },
  4821. { id: 9, no: "RZ20260921012", status: "已完成", date: "2026-10-01", deliverDate: "2026-10-02", type: "入直单", docKind: "选单类", salesman: "胡明", dept: "仓储组", shipMode: "槽车运输", loadFrom: "荆州长江溶剂厂", settler: "安庆市山峡化工有限公司", customer: "荆州长江溶剂厂", ctrl: "", product: "工业盐酸", spec: "31%", pack: "槽罐车", unit: "吨", planQty: 28, realQty: 28, tractor: "鄂E·危C902", driver: "胡明", certId: null, transId: null, warehouseId: null, archived: false, source: "入直同步", sourceNo: "PO20260921012", taskNo: "SXFZ20261001-002" },
  4822. { id: 13, no: "RZ20261001006", status: "已完成", date: "2026-10-01", deliverDate: "2026-10-02", type: "入直单", docKind: "选单类", salesman: "胡明", dept: "仓储组", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "安庆市山峡化工有限公司", customer: "安庆市山峡化工有限公司", ctrl: "易制毒", product: "30%硫酸", spec: "30%", pack: "槽罐车", unit: "吨", planQty: 40, realQty: 40, tractor: "皖LB1283", driver: "胡明", certId: 3, shipCertId: 3, transId: 4, warehouseId: null, archived: false, certDeducted: true, deductedQty: 40, source: "入直同步", sourceNo: "PO20261001006" },
  4823. { id: 10, no: "PO202508050083", status: "已作废", date: "2026-10-02", deliverDate: "2026-10-03", type: "出库单", docKind: "选单类", salesman: "杨磊", dept: "销售一部", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "安徽鑫源新材料有限公司", customer: "安徽鑫源新材料有限公司", ctrl: "", product: "30%-工业硫酸", spec: "30%", pack: "槽罐车", unit: "吨", planQty: 50, realQty: null, tractor: "", driver: "", certId: null, transId: null, warehouseId: null, source: "ERP", sourceNo: "JT202508050083" },
  4824. { id: 11, no: "PO202508050084", status: "已安排", date: "2026-10-02", deliverDate: "2026-10-03", type: "出库单", docKind: "选单类", salesman: "BLJT / masakazunak", dept: "销售一部", shipMode: "槽车运输", loadFrom: "南京市化工工程有限公司", settler: "安徽鑫源化工有限公司", customer: "安徽鑫源化工有限公司", ctrl: "", product: "32%-烧碱", spec: "32%", pack: "槽罐车", unit: "吨", planQty: 32.6, realQty: 0, tractor: "皖K92881", driver: "吴涛", certId: null, transId: null, warehouseId: null, source: "ERP", sourceNo: "JT202508050084" },
  4825. { id: 12, no: "PO202508050082", status: "已完成", date: "2026-10-02", deliverDate: "2026-10-03", type: "出库单", docKind: "选单类", salesman: "BLJT / masakazunak", dept: "销售一部", shipMode: "槽车运输", loadFrom: "南京市化工工程有限公司", settler: "安徽鑫源化工有限公司", customer: "安徽鑫源化工有限公司", ctrl: "", product: "32%-工业硫酸", spec: "32%", pack: "槽罐车", unit: "吨", planQty: 33, realQty: 33, tractor: "皖HA3690", driver: "周建军", certId: null, transId: null, warehouseId: "h2so4-50", archived: true, archiveDate: "2026-10-02", source: "ERP", sourceNo: "JT202508050082" },
  4826. { id: 14, no: "PO202610040001", status: "草稿", date: "2026-10-04", deliverDate: "2026-10-05", type: "出库单", docKind: "选单类", salesman: "杨磊", dept: "运输调度组", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "安徽东泰化学试剂有限公司", customer: "安徽东泰化学试剂有限公司", ctrl: "", product: "32%液碱", spec: "32%", pack: "槽罐车", unit: "吨", planQty: 28, realQty: null, tractor: "", driver: "", certId: null, transId: null, warehouseId: null, source: "手工录入", sourceNo: "PO202610040001" },
  4827. { id: 15, no: "FZ20260930001", status: "已安排", date: "2026-09-30", deliverDate: "2026-10-07", type: "出库单", docKind: "开单类", salesman: "调度员", dept: "仓储组", shipMode: "车辆运输", loadFrom: "安庆市山峡化工有限公司", settler: "江峰", customer: "江峰", ctrl: "", product: "工业硫酸", spec: "98%", pack: "吨桶", unit: "吨", planQty: 22, realQty: 0, tractor: "", driver: "", certId: null, transId: null, warehouseId: null, source: "手工录入", sourceNo: "FZ20260930001" },
  4828. { id: 17, no: "PO202609200016", status: "已完成", date: "2026-09-22", deliverDate: "2026-09-22", type: "出库单", docKind: "选单类", salesman: "吴涛", dept: "运输调度组", shipMode: "车辆运输", loadFrom: "安庆市山峡化工有限公司", settler: "宜昌爆破器材公司", customer: "宜昌爆破器材公司", ctrl: "易制爆", product: "硝酸铵", spec: "工业级", pack: "袋装", unit: "吨", planQty: 16, realQty: 16, tractor: "鄂E·危D077", driver: "吴涛", certId: 2, transId: 3, warehouseId: null, archived: true, archiveDate: "2026-09-22", certDeducted: true, deductedQty: 16, source: "出库同步", sourceNo: "PO202609200016" },
  4829. { id: 18, no: "PO202610080018", status: "已安排", date: "2026-10-08", deliverDate: "2026-10-09", type: "出库单", docKind: "选单类", salesman: "吴涛", dept: "运输调度组", shipMode: "车辆运输", loadFrom: "安庆市山峡化工有限公司", settler: "宜昌爆破器材公司", customer: "宜昌爆破器材公司", ctrl: "易制爆", product: "硝酸铵", spec: "工业级", pack: "袋装", unit: "吨", planQty: 32, realQty: 0, tractor: "鄂E·危D077", driver: "吴涛", certId: 2, transId: 3, warehouseId: null, source: "出库同步", sourceNo: "PO202610080018" },
  4830. { id: 19, no: "PO202610010019", status: "待安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "出库单", docKind: "开单类", salesman: "万金强", dept: "销售一部", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "中新联科", customer: "中新联科", ctrl: "易制毒", product: "98%工业硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 28, realQty: 0, tractor: "", driver: "", certId: null, transId: null, warehouseId: "h2so4-98", source: "ERP", sourceNo: "PO202610010019" },
  4831. { id: 20, no: "RZ202610010020", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "入直单", docKind: "选单类", salesman: "胡明", dept: "仓储组", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "安庆市山峡化工有限公司", customer: "安庆市山峡化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 36, realQty: 0, tractor: "皖LB1283", driver: "胡明", certId: null, transId: null, warehouseId: "h2so4-98", source: "入直同步", sourceNo: "RZ202610010020" },
  4832. { id: 21, no: "ZX202610010021", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "直销单", docKind: "开单类", salesman: "吴涛", dept: "运输调度组", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 32, realQty: null, tractor: "皖K92881", driver: "吴涛", certId: null, transId: null, warehouseId: null, source: "直销同步", sourceNo: "ZX202610010021" },
  4833. { id: 22, no: "PO202610010022", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "出库单", docKind: "选单类", salesman: "杨磊", dept: "运输调度组", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 28, realQty: 0, tractor: "皖H57837", driver: "陈伟", certId: null, transId: null, warehouseId: "h2so4-98", source: "ERP", sourceNo: "JT202610010022" },
  4834. { id: 23, no: "PO202610010023", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "出库单", docKind: "选单类", salesman: "周建军", dept: "运输调度组", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 30, realQty: 0, tractor: "皖HA3690", driver: "周建军", certId: 1, shipCertId: 1, transId: null, warehouseId: "h2so4-98", source: "出库同步", sourceNo: "PO202610010023" },
  4835. { id: 24, no: "RZ202610010024", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "入直单", docKind: "选单类", salesman: "胡明", dept: "仓储组", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "安庆市山峡化工有限公司", customer: "安庆市山峡化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 30, realQty: 0, tractor: "皖K92881", driver: "吴涛", certId: null, transId: null, warehouseId: "h2so4-98", source: "入直同步", sourceNo: "RZ202610010024" },
  4836. { id: 25, no: "ZX202610010025", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "直销单", docKind: "开单类", salesman: "吴涛", dept: "运输调度组", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 32, realQty: null, tractor: "皖LB1283", driver: "胡明", certId: null, transId: null, warehouseId: null, source: "直销同步", sourceNo: "ZX202610010025" },
  4837. { id: 26, no: "RZ202610010026", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "入直单", docKind: "选单类", salesman: "胡明", dept: "仓储组", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "安庆市山峡化工有限公司", customer: "安庆市山峡化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 32, realQty: 0, tractor: "皖LB1283", driver: "胡明", certId: 3, shipCertId: 3, transId: null, warehouseId: "h2so4-98", source: "入直同步", sourceNo: "RZ202610010026" },
  4838. { id: 27, no: "ZX202610010027", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "直销单", docKind: "开单类", salesman: "万金强", dept: "销售一部", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 30, realQty: null, tractor: "皖HA3690", driver: "周建军", certId: null, transId: null, warehouseId: null, source: "直销同步", sourceNo: "ZX202610010027" },
  4839. { id: 28, no: "ZX202610010028", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "直销单", docKind: "开单类", salesman: "杨磊", dept: "运输调度组", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 28, realQty: null, tractor: "皖H57837", driver: "陈伟", certId: 3, procureCertId: 3, shipCertId: 1, transId: null, warehouseId: null, source: "直销同步", sourceNo: "ZX202610010028" },
  4840. { id: 29, no: "ZX202610010029", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "直销单", docKind: "开单类", salesman: "吴涛", dept: "运输调度组", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 32, realQty: null, tractor: "皖K92881", driver: "吴涛", certId: 1, procureCertId: 3, shipCertId: 1, transId: 5, warehouseId: null, source: "直销同步", sourceNo: "ZX202610010029" },
  4841. { id: 30, no: "ZX202610010030", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "直销单", docKind: "开单类", salesman: "万金强", dept: "销售一部", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 26, realQty: null, tractor: "皖HA3690", driver: "周建军", certId: null, transId: null, warehouseId: null, source: "直销同步", sourceNo: "ZX202610010030" },
  4842. { id: 31, no: "RZ202610010031", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "入直单", docKind: "选单类", salesman: "胡明", dept: "仓储组", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "安庆市山峡化工有限公司", customer: "安庆市山峡化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 28, realQty: 0, tractor: "皖K92881", driver: "吴涛", certId: null, transId: null, warehouseId: "h2so4-98", source: "入直同步", sourceNo: "RZ202610010031" },
  4843. { id: 32, no: "PO202610010032", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "出库单", docKind: "选单类", salesman: "杨磊", dept: "运输调度组", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 30, realQty: 0, tractor: "皖H57837", driver: "陈伟", certId: null, transId: null, warehouseId: "h2so4-98", source: "ERP", sourceNo: "JT202610010032" },
  4844. { id: 33, no: "ZX202610010033", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "直销单", docKind: "开单类", salesman: "万金强", dept: "销售一部", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 29, realQty: null, tractor: "皖LB1283", driver: "胡明", certId: null, transId: null, warehouseId: null, source: "直销同步", sourceNo: "ZX202610010033" },
  4845. { id: 34, no: "RZ202610010034", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "入直单", docKind: "选单类", salesman: "胡明", dept: "仓储组", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "安庆市山峡化工有限公司", customer: "安庆市山峡化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 32, realQty: 0, tractor: "皖HA3690", driver: "周建军", certId: null, transId: null, warehouseId: "h2so4-98", source: "入直同步", sourceNo: "RZ202610010034" },
  4846. { id: 35, no: "PO202610010035", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "出库单", docKind: "选单类", salesman: "杨磊", dept: "运输调度组", shipMode: "槽车运输", loadFrom: "安庆市山峡化工有限公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 32, realQty: 0, tractor: "皖LB1283", driver: "胡明", certId: null, transId: null, warehouseId: "h2so4-98", source: "ERP", sourceNo: "JT202610010035" },
  4847. { id: 36, no: "ZX202610010036", status: "已安排", date: "2026-10-01", deliverDate: "2026-10-01", type: "直销单", docKind: "开单类", salesman: "万金强", dept: "销售一部", shipMode: "槽车运输", loadFrom: "铜陵锦帆物资有限责任公司", settler: "上饶市敏达精细化工有限公司", customer: "上饶市敏达精细化工有限公司", ctrl: "易制毒", product: "硫酸", spec: "98%", pack: "槽罐车", unit: "吨", planQty: 30, realQty: null, tractor: "皖H57837", driver: "陈伟", certId: null, transId: null, warehouseId: null, source: "直销同步", sourceNo: "ZX202610010036" }
  4848. ];
  4849. const planExtras = {
  4850. 1: { deliverTime: "08:00", escort: "", contractNo: "SX/X-JT202508050191" },
  4851. 2: { deliverTime: "08:30", escort: "李强", contractNo: "SX/X-JT202508050190" },
  4852. 3: { deliverTime: "09:00", escort: "王磊", contractNo: "ZX-QH202609280354" },
  4853. 4: { deliverTime: "10:00", escort: "赵鹏", contractNo: "SX/X-JT202508050117" },
  4854. 5: { deliverTime: "13:00", escort: "孙磊", contractNo: "SX/X-JT202508050116" },
  4855. 6: { deliverTime: "14:00", escort: "孙磊", contractNo: "SX/X-XY202508050109" },
  4856. 7: { deliverTime: "07:30", escort: "李强", contractNo: "SX/X-JT20261003001" },
  4857. 8: { deliverTime: "16:00", escort: "", contractNo: "SX/X-JT20261004001" },
  4858. 9: { deliverTime: "09:30", escort: "王磊", contractNo: "SG-GX2026-01-0031" },
  4859. 13: { deliverTime: "11:00", escort: "王磊", contractNo: "CG10120260930002" },
  4860. 10: { deliverTime: "15:00", escort: "", contractNo: "SX/X-XY202508050083" },
  4861. 11: { deliverTime: "08:20", escort: "孙磊", contractNo: "SX/X-XY202508050084" },
  4862. 12: { deliverTime: "06:40", escort: "李强", contractNo: "SX/X-XY202508050082" },
  4863. 14: { deliverTime: "08:00", escort: "", contractNo: "" },
  4864. 15: { deliverTime: "10:00", escort: "", contractNo: "" },
  4865. 16: { deliverTime: "08:00", escort: "", contractNo: "" },
  4866. 17: { deliverTime: "09:00", escort: "孙磊", contractNo: "SX/X-YC202609200016" },
  4867. 18: { deliverTime: "10:00", escort: "孙磊", contractNo: "SX/X-YC202610080018" },
  4868. 19: { deliverTime: "11:00", escort: "", contractNo: "" },
  4869. 20: { deliverTime: "09:30", escort: "王磊", contractNo: "CG10120261001020" },
  4870. 21: { deliverTime: "13:00", escort: "李强", contractNo: "ZX-MD20261001021" },
  4871. 22: { deliverTime: "14:00", escort: "赵鹏", contractNo: "SX/X-JT202610010022" },
  4872. 23: { deliverTime: "08:40", escort: "李强", contractNo: "SX/X-JT202610010023" },
  4873. 24: { deliverTime: "10:20", escort: "王磊", contractNo: "CG10120261001024" },
  4874. 25: { deliverTime: "15:00", escort: "李强", contractNo: "ZX-MD20261001025" },
  4875. 26: { deliverTime: "11:30", escort: "王磊", contractNo: "CG10120261001026" },
  4876. 27: { deliverTime: "09:00", escort: "李强", contractNo: "ZX-MD20261001027" },
  4877. 28: { deliverTime: "10:40", escort: "赵鹏", contractNo: "ZX-MD20261001028" },
  4878. 29: { deliverTime: "14:20", escort: "李强", contractNo: "ZX-MD20261001029" },
  4879. 30: { deliverTime: "16:00", escort: "王磊", contractNo: "ZX-MD20261001030" },
  4880. 31: { deliverTime: "08:50", escort: "王磊", contractNo: "CG10120261001031" },
  4881. 32: { deliverTime: "11:10", escort: "赵鹏", contractNo: "SX/X-JT202610010032" },
  4882. 33: { deliverTime: "15:40", escort: "李强", contractNo: "ZX-MD20261001033" },
  4883. 34: { deliverTime: "07:40", escort: "王磊", contractNo: "CG10120261001034" },
  4884. 35: { deliverTime: "09:20", escort: "李强", contractNo: "SX/X-JT202610010035" },
  4885. 36: { deliverTime: "12:10", escort: "赵鹏", contractNo: "ZX-MD20261001036" }
  4886. };
  4887. plans.forEach((p) => Object.assign(p, planExtras[p.id] || {}));
  4888. const carrierFleet = [
  4889. { company: "安庆宏泰危险货物运输有限公司", plate: "皖HA3690", driver: "周建军", cap: 35 },
  4890. { company: "安庆宏泰危险货物运输有限公司", plate: "皖H57837", driver: "陈伟", cap: 35 },
  4891. { company: "安庆宏泰危险货物运输有限公司", plate: "皖LB1283", driver: "胡明", cap: 32 },
  4892. { company: "安庆宏泰危险货物运输有限公司", plate: "皖K92881", driver: "吴涛", cap: 32 },
  4893. { company: "宜昌安达危运有限公司", plate: "鄂E·危A123", driver: "周建军", cap: 32 },
  4894. { company: "宜昌安达危运有限公司", plate: "鄂E·危B618", driver: "陈伟", cap: 32 },
  4895. { company: "宜昌安达危运有限公司", plate: "鄂E·危D077", driver: "吴涛", cap: 32 },
  4896. { company: "宜昌安达危运有限公司", plate: "鄂E·危C902", driver: "胡明", cap: 30 },
  4897. { company: "山峡自有车队", plate: "鄂E·危E441", driver: "郑浩", cap: 28 }
  4898. ];
  4899. const WARN_RATE = 0.1;
  4900. function defaultWarnQty(total) {
  4901. const n = Number(total);
  4902. if (!Number.isFinite(n) || n <= 0) return 0;
  4903. return Math.round(n * WARN_RATE * 100) / 100;
  4904. }
  4905. certs.forEach((c) => {
  4906. if (c.warnQty == null || c.warnQty === "") c.warnQty = defaultWarnQty(c.buyQty);
  4907. (c.transList || []).forEach((t) => {
  4908. if (t.warnQty == null || t.warnQty === "") t.warnQty = defaultWarnQty(t.transQty);
  4909. });
  4910. });
  4911. const labels = { dash: "总览", fleet: "车辆台账", fuel: "加油登记管理", fuelwarn: "油耗预警", etc: "ETC充值卡管理", oilcard: "加油充值卡管理", maint: "车辆维保管理", stats: "统计" };
  4912. const fleetMenuView = { dash: "fuel", fleet: "fuel", fuel: "fuel", fuelwarn: "fuel", etc: "etc", oilcard: "oilcard", maint: "maint", stats: "fuel" };
  4913. let currentFleetView = "etc";
  4914. const FUEL_NORM = 33 / 100;
  4915. const money = (n) => Number(n).toLocaleString("zh-CN", { maximumFractionDigits: 0 });
  4916. const stClass = (s) => s === "运行" ? "st-run" : s === "维保中" ? "st-fix" : "st-stop";
  4917. const applicants = ["吴九华", ...vehicles.map((v) => v.driver)].filter((n, i, a) => a.indexOf(n) === i);
  4918. function fillPlates() {
  4919. const fuelPlate = document.querySelector("#fuelForm [name=plate]");
  4920. if (fuelPlate) fuelPlate.innerHTML = vehicles.map((v) => `<option value="${v.plate}">${hazPlate(v)}</option>`).join("");
  4921. const plateOpts = `<option value="">请选择车牌</option>` + vehicles.map((v) => `<option value="${v.plate}">${hazPlate(v)}</option>`).join("");
  4922. const maintPlate = document.querySelector("#maintForm [name=plate]");
  4923. if (maintPlate) maintPlate.innerHTML = plateOpts;
  4924. const etcPlate = document.querySelector("#etcForm [name=plate]");
  4925. etcPlate.innerHTML = `<option value="">请选择自有车牌</option>` + vehicles.map((v) => `<option value="${v.plate}">${hazPlate(v)}</option>`).join("");
  4926. const appOpts = applicants.map((n) => `<option value="${n}">${n}(${n})</option>`).join("");
  4927. const etcApp = document.querySelector("#etcForm [name=applicant]");
  4928. const oilApp = document.querySelector("#oilCardForm [name=applicant]");
  4929. const maintApp = document.querySelector("#maintForm [name=applicant]");
  4930. if (etcApp) etcApp.innerHTML = appOpts;
  4931. if (oilApp) oilApp.innerHTML = appOpts;
  4932. if (maintApp) maintApp.innerHTML = appOpts;
  4933. const oilPlate = document.querySelector("#oilCardForm [name=plate]");
  4934. if (oilPlate) oilPlate.innerHTML = `<option value="">请选择自有车牌</option>` + vehicles.map((v) => `<option value="${v.plate}">${hazPlate(v)}</option>`).join("");
  4935. document.querySelector("#fuelForm [name=date]").value = today;
  4936. document.querySelector("#etcForm [name=date]").value = today;
  4937. const oilDate = document.querySelector("#oilCardForm [name=date]");
  4938. if (oilDate) oilDate.value = today;
  4939. const applyDate = document.querySelector("#maintForm [name=applyDate]");
  4940. const inDate = document.querySelector("#maintForm [name=inDate]");
  4941. if (applyDate) applyDate.value = today;
  4942. if (inDate) inDate.value = today;
  4943. }
  4944. function daysTo(d) {
  4945. return Math.ceil((new Date(d) - new Date(today)) / 86400000);
  4946. }
  4947. function showView(name) {
  4948. currentFleetView = name;
  4949. ["dash", "fleet", "fuel", "fuelwarn", "etc", "oilcard", "maint", "stats"].forEach((v) => {
  4950. document.getElementById("view-" + v).classList.toggle("hidden", v !== name);
  4951. });
  4952. document.querySelectorAll("#subnav button").forEach((b) => b.classList.toggle("on", b.dataset.view === name));
  4953. setCrumb("车辆管理", labels[name]);
  4954. markNav(`[data-fleet-view="${fleetMenuView[name] || "etc"}"]`);
  4955. if (name === "dash") renderDash();
  4956. if (name === "fleet") renderFleet();
  4957. if (name === "fuel") renderFuel();
  4958. if (name === "fuelwarn") renderFuelWarn();
  4959. if (name === "etc") renderEtc();
  4960. if (name === "oilcard") renderOilCard();
  4961. if (name === "maint") renderMaint();
  4962. if (name === "stats") renderStats();
  4963. }
  4964. function toast(msg) {
  4965. const el = document.getElementById("toast");
  4966. el.textContent = msg;
  4967. el.classList.add("show");
  4968. setTimeout(() => el.classList.remove("show"), 1800);
  4969. }
  4970. function fuelSegments() {
  4971. const byPlate = {};
  4972. fuels.forEach((f) => {
  4973. (byPlate[f.plate] ||= []).push(f);
  4974. });
  4975. const rows = [];
  4976. vehicles.forEach((v) => {
  4977. const list = (byPlate[v.plate] || []).slice().sort((a, b) => a.km - b.km || a.date.localeCompare(b.date));
  4978. if (list.length < 2) {
  4979. rows.push({
  4980. plate: v.plate,
  4981. driver: v.driver,
  4982. fromDate: list[0]?.date || "—",
  4983. toDate: list[0]?.date || "—",
  4984. fromKm: list[0]?.km || v.km,
  4985. toKm: list[0]?.km || v.km,
  4986. km: 0,
  4987. liters: list[0]?.liters || 0,
  4988. rate: null,
  4989. per100: null,
  4990. status: list.length ? "不足" : "无记录"
  4991. });
  4992. return;
  4993. }
  4994. for (let i = 1; i < list.length; i++) {
  4995. const prev = list[i - 1];
  4996. const cur = list[i];
  4997. const km = cur.km - prev.km;
  4998. const liters = Number(cur.liters);
  4999. const rate = km > 0 ? liters / km : null;
  5000. let status = "正常";
  5001. if (km <= 0) status = "异常";
  5002. else if (rate > FUEL_NORM) status = "预警";
  5003. rows.push({
  5004. plate: v.plate,
  5005. driver: v.driver,
  5006. fromDate: prev.date,
  5007. toDate: cur.date,
  5008. fromKm: prev.km,
  5009. toKm: cur.km,
  5010. km,
  5011. liters,
  5012. rate,
  5013. per100: rate == null ? null : rate * 100,
  5014. status
  5015. });
  5016. }
  5017. });
  5018. const order = { 预警: 0, 异常: 1, 不足: 2, 无记录: 3, 正常: 4 };
  5019. return rows.sort((a, b) => order[a.status] - order[b.status] || a.plate.localeCompare(b.plate));
  5020. }
  5021. function fuelWarnCount() {
  5022. return fuelSegments().filter((r) => r.status === "预警").length;
  5023. }
  5024. function renderDash() {
  5025. const due = vehicles.filter((v) => daysTo(v.tankDue) <= 20 || daysTo(v.insDue) <= 20);
  5026. const fixing = vehicles.filter((v) => v.status === "维保中").length;
  5027. const warnN = fuelWarnCount();
  5028. document.getElementById("dashAlerts").innerHTML = `
  5029. <div class="alert red" style="cursor:pointer" onclick="showView('fuelwarn')">油耗超标预警 ${warnN} 条(标准 0.33 L/km)</div>
  5030. <div class="alert red">罐检 / 保险 20 日内到期 ${due.length} 辆</div>
  5031. <div class="alert">维保中 ${fixing} 辆,注意出厂登记</div>
  5032. <div class="alert ok">本月已登加油 ${fuels.length} 笔</div>`;
  5033. const fuelSum = fuels.reduce((s, x) => s + x.amount, 0);
  5034. const maintOpen = maints.filter((m) => !m.outDate).length;
  5035. document.getElementById("dashStats").innerHTML = [
  5036. ["在册车辆", vehicles.length, `运行 ${vehicles.filter(v=>v.status==="运行").length} · 维保 ${fixing}`],
  5037. ["本月加油(元)", money(fuelSum), `${fuels.reduce((s,x)=>s+x.liters,0)} 升`],
  5038. ["未出厂维保", maintOpen, `记录 ${maints.length} 条`],
  5039. ["本月 ETC 申请", etcs.length, `已提交 ${etcs.filter(x=>x.status==="已提交").length} · 草稿 ${etcs.filter(x=>x.status==="草稿").length}`]
  5040. ].map(([k,v,s]) => `<div class="stat"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");
  5041. document.getElementById("dashFleet").innerHTML = `<table><thead><tr><th>车牌</th><th>介质</th><th>驾驶员</th><th>状态</th><th>罐检到期</th></tr></thead><tbody>${
  5042. vehicles.map((v) => `<tr><td>${v.plate}</td><td>${v.media}</td><td>${v.driver}</td><td><span class="st ${stClass(v.status)}">${v.status}</span></td><td>${daysTo(v.tankDue) <= 20 ? `<span class="st st-due">${v.tankDue}</span>` : v.tankDue}</td></tr>`).join("")
  5043. }</tbody></table>`;
  5044. const events = [
  5045. ...fuels.map((x) => ({ date: x.date, title: "加油 " + x.plate, d: x.liters + " 升 · " + x.station })),
  5046. ...etcs.map((x) => ({ date: x.date, title: "ETC申请 " + x.plate, d: x.applicant + " · 期初 " + x.km + " km" })),
  5047. ...maints.map((x) => ({ date: x.inDate, title: x.type + " " + x.plate, d: x.item }))
  5048. ].sort((a, b) => b.date.localeCompare(a.date)).slice(0, 6);
  5049. document.getElementById("dashTl").innerHTML = events.map((e) => `<div class="tl"><div class="t">${e.date.slice(5)}</div><div><b>${e.title}</b><div class="d">${e.d}</div></div></div>`).join("");
  5050. const byPlate = {};
  5051. fuels.forEach((f) => { byPlate[f.plate] = (byPlate[f.plate] || 0) + f.liters; });
  5052. const max = Math.max(...Object.values(byPlate), 1);
  5053. document.getElementById("dashBars").innerHTML = Object.entries(byPlate).map(([p, n]) =>
  5054. `<div class="bar-row"><span>${p.replace("鄂E·","")}</span><div class="bar-track"><div class="bar-fill" style="width:${(n/max)*100}%"></div></div><span class="n">${n}</span></div>`
  5055. ).join("");
  5056. const sb = document.getElementById("sideBadge");
  5057. if (sb) sb.textContent = due.length + maintOpen + warnN;
  5058. }
  5059. function renderFleet() {
  5060. const q = (document.getElementById("fleetQ").value || "").trim();
  5061. const st = document.getElementById("fleetSt").value;
  5062. const rows = vehicles.filter((v) => (!q || v.plate.includes(q) || v.driver.includes(q)) && (!st || v.status === st));
  5063. document.getElementById("fleetTable").innerHTML = `<table><thead><tr><th>车牌</th><th>车型</th><th>介质</th><th>驾驶员</th><th>押运员</th><th>里程</th><th>状态</th><th>罐检</th><th>保险</th><th>下次保养</th></tr></thead><tbody>${
  5064. rows.map((v) => `<tr><td>${v.plate}</td><td>${v.type}</td><td>${v.media}</td><td>${v.driver}</td><td>${v.escort}</td><td>${v.km.toLocaleString()}</td><td><span class="st ${stClass(v.status)}">${v.status}</span></td><td>${v.tankDue}</td><td>${v.insDue}</td><td>${v.nextMaint.toLocaleString()} km</td></tr>`).join("")
  5065. }</tbody></table>`;
  5066. }
  5067. function attachListOf(source) {
  5068. if (source === "etc") return etcs;
  5069. if (source === "oilcard") return oilcards;
  5070. if (source === "maint") return maints;
  5071. return fuels;
  5072. }
  5073. function attachChips(source, rowIndex) {
  5074. const items = (attachListOf(source)[rowIndex] || {}).attachments || [];
  5075. if (!items.length) return `<span class="st st-due">缺附件</span>`;
  5076. return `<div class="chips">${items.map((a, i) =>
  5077. a.url
  5078. ? `<button class="chip" type="button" onclick="openAttach('${source}',${rowIndex},${i})">${a.name}</button>`
  5079. : `<span class="chip ghost" title="历史存档">${a.name}</span>`
  5080. ).join("")}</div>`;
  5081. }
  5082. function ymShift(ym, n) {
  5083. const [y, m] = String(ym).split("-").map(Number);
  5084. const d = new Date(y, m - 1 + n, 1);
  5085. return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
  5086. }
  5087. function fuelsInMonth(plate, ym) {
  5088. return fuels.filter((f) => f.plate === plate && String(f.date).slice(0, 7) === ym);
  5089. }
  5090. function lastFuelKm(rows) {
  5091. const list = rows.filter((f) => f.km != null && f.km !== "").slice().sort((a, b) => String(a.date).localeCompare(String(b.date)));
  5092. return list.length ? Number(list[list.length - 1].km) : null;
  5093. }
  5094. function monthRunKm(plate, ym) {
  5095. const cur = lastFuelKm(fuelsInMonth(plate, ym));
  5096. const prev = lastFuelKm(fuelsInMonth(plate, ymShift(ym, -1)));
  5097. if (cur != null && prev != null) return Math.max(0, cur - prev);
  5098. const rows = fuelsInMonth(plate, ym).filter((f) => f.km != null).slice().sort((a, b) => String(a.date).localeCompare(String(b.date)));
  5099. if (rows.length >= 2) return Math.max(0, Number(rows[rows.length - 1].km) - Number(rows[0].km));
  5100. return 0;
  5101. }
  5102. function litersMomHtml(cur, prev) {
  5103. if (!prev) return `<em class="mom flat">环比 —</em>`;
  5104. const pct = ((cur - prev) / prev) * 100;
  5105. if (Math.abs(pct) < 0.05) return `<em class="mom flat">环比持平</em>`;
  5106. const up = pct > 0;
  5107. return `<em class="mom ${up ? "up" : "down"}">环比${up ? "↑" : "↓"}${Math.abs(pct).toFixed(1)}%</em>`;
  5108. }
  5109. function renderFuel() {
  5110. const ym = String(today).slice(0, 7);
  5111. const prevYm = ymShift(ym, -1);
  5112. const box = document.getElementById("fuelPlateCards");
  5113. if (box) {
  5114. box.innerHTML = vehicles.map((v) => {
  5115. const rows = fuelsInMonth(v.plate, ym);
  5116. const prevRows = fuelsInMonth(v.plate, prevYm);
  5117. const liters = rows.reduce((s, f) => s + Number(f.liters || 0), 0);
  5118. const prevLiters = prevRows.reduce((s, f) => s + Number(f.liters || 0), 0);
  5119. const runKm = monthRunKm(v.plate, ym);
  5120. const prevKm = monthRunKm(v.plate, prevYm);
  5121. return `<article class="fuel-plate-card" onclick="openFuelDlg('${v.plate}')">
  5122. <div class="plate">${hazPlate(v)}</div>
  5123. <div class="meta">${v.driver || "—"} · ${v.type || ""} · <span class="st ${stClass(v.status)}">${v.status}</span></div>
  5124. <div class="nums">
  5125. <div><b>${rows.length}</b><span>当月笔数</span>${litersMomHtml(rows.length, prevRows.length)}</div>
  5126. <div><b>${money(runKm)}</b><span>当月公里</span>${litersMomHtml(runKm, prevKm)}</div>
  5127. <div><b>${fmtTon(liters)}</b><span>升</span>${litersMomHtml(liters, prevLiters)}</div>
  5128. </div>
  5129. </article>`;
  5130. }).join("");
  5131. }
  5132. document.getElementById("fuelTable").innerHTML = `<table><thead><tr><th>日期</th><th>车辆</th><th>加油站</th><th>油品</th><th>升数</th><th>金额</th><th>里程</th><th>附件</th><th>备注</th></tr></thead><tbody>${
  5133. fuels.map((x, i) => `<tr><td>${x.date}</td><td>${hazPlate(x.plate)}</td><td>${x.station}</td><td>${x.oil}</td><td>${x.liters}</td><td>${x.amount}</td><td>${x.km}</td><td>${attachChips("fuel", i)}</td><td>${x.note || "—"}</td></tr>`).join("")
  5134. }</tbody></table>`;
  5135. }
  5136. function openFuelDlg(plate) {
  5137. const form = document.getElementById("fuelForm");
  5138. form.reset();
  5139. document.getElementById("fuelFileChips").innerHTML = "";
  5140. fillPlates();
  5141. if (plate) form.plate.value = plate;
  5142. const v = vehicles.find((x) => x.plate === form.plate.value);
  5143. if (v && form.km) form.km.value = v.km;
  5144. document.getElementById("fuelDialog").showModal();
  5145. }
  5146. function closeFuelDlg() {
  5147. document.getElementById("fuelDialog").close();
  5148. }
  5149. function fmtRate(n, digits) {
  5150. return n == null ? "—" : n.toFixed(digits);
  5151. }
  5152. function renderFuelWarn() {
  5153. const q = (document.getElementById("fuelwarnQ").value || "").trim();
  5154. const st = document.getElementById("fuelwarnSt").value;
  5155. const all = fuelSegments();
  5156. const warnN = all.filter((r) => r.status === "预警").length;
  5157. const okN = all.filter((r) => r.status === "正常").length;
  5158. const lackN = all.filter((r) => r.status === "不足" || r.status === "无记录" || r.status === "异常").length;
  5159. const rates = all.filter((r) => r.rate != null).map((r) => r.rate);
  5160. const maxRate = rates.length ? Math.max(...rates) : 0;
  5161. document.getElementById("fuelwarnKpis").innerHTML = [
  5162. ["标准油耗", "0.33 L/km", "33 ÷ 100"],
  5163. ["超标预警", warnN, "相邻加油区间"],
  5164. ["正常区间", okN, "未超过标准"],
  5165. ["数据不足 / 异常", lackN, "少于两次加油或里程未增"]
  5166. ].map(([k, v, s]) => `<div class="stat"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");
  5167. const latestByPlate = {};
  5168. all.filter((r) => r.rate != null).forEach((r) => {
  5169. const prev = latestByPlate[r.plate];
  5170. if (!prev || r.toDate > prev.toDate) latestByPlate[r.plate] = r;
  5171. });
  5172. const barMax = Math.max(maxRate, FUEL_NORM, 0.01);
  5173. document.getElementById("fuelwarnBars").innerHTML = Object.values(latestByPlate).map((r) => {
  5174. const over = r.rate > FUEL_NORM;
  5175. return `<div class="bar-row"><span>${hazPlate(r.plate)}</span><div class="bar-track"><div class="bar-fill${over ? " warn" : ""}" style="width:${(r.rate / barMax) * 100}%"></div><i class="bar-mark" style="left:${(FUEL_NORM / barMax) * 100}%" title="标准 0.33"></i></div><span class="n${over ? " rate-over" : ""}">${fmtRate(r.rate, 3)}</span></div>`;
  5176. }).join("") || `<div style="color:var(--muted)">暂无可对照的加油区间</div>`;
  5177. const rows = all.filter((r) => (!q || r.plate.includes(q) || hazPlate(r.plate).includes(q)) && (!st || (st === "不足" ? ["不足", "无记录", "异常"].includes(r.status) : r.status === st)));
  5178. const stHtml = (s) => s === "预警" || s === "异常" ? `<span class="st st-due">${s}</span>` : s === "正常" ? `<span class="st st-run">${s}</span>` : `<span class="st st-stop">${s}</span>`;
  5179. document.getElementById("fuelwarnTable").innerHTML = `<table><thead><tr><th>车辆</th><th>驾驶员</th><th>区间</th><th>起止里程</th><th>行驶(km)</th><th>加油(L)</th><th>L/km</th><th>L/100km</th><th>对照</th><th>状态</th></tr></thead><tbody>${
  5180. rows.map((r) => {
  5181. const over = r.status === "预警";
  5182. const delta = r.rate == null ? "—" : (r.rate - FUEL_NORM >= 0 ? "+" : "") + (r.rate - FUEL_NORM).toFixed(3);
  5183. return `<tr><td>${hazPlate(r.plate)}</td><td>${r.driver}</td><td>${r.fromDate} → ${r.toDate}</td><td>${Number(r.fromKm).toLocaleString()} → ${Number(r.toKm).toLocaleString()}</td><td>${r.km || "—"}</td><td>${r.liters || "—"}</td><td class="${over ? "rate-over" : ""}">${fmtRate(r.rate, 3)}</td><td class="${over ? "rate-over" : ""}">${fmtRate(r.per100, 1)}</td><td>${delta}</td><td>${stHtml(r.status)}</td></tr>`;
  5184. }).join("") || `<tr><td colspan="10" style="color:var(--muted)">没有符合条件的记录</td></tr>`
  5185. }</tbody></table>`;
  5186. }
  5187. function previewFuelFiles(input) {
  5188. const box = document.getElementById("fuelFileChips");
  5189. const files = [...input.files];
  5190. box.innerHTML = files.length
  5191. ? files.map((f) => `<span class="chip ghost">${f.name}</span>`).join("")
  5192. : "";
  5193. }
  5194. function readFiles(fileList) {
  5195. return Promise.all([...fileList].map((file) => new Promise((resolve, reject) => {
  5196. const reader = new FileReader();
  5197. reader.onload = () => resolve({ name: file.name, type: file.type, url: reader.result });
  5198. reader.onerror = reject;
  5199. reader.readAsDataURL(file);
  5200. })));
  5201. }
  5202. function openAttach(source, rowIndex, fileIndex) {
  5203. const file = source === "task"
  5204. ? ((TASKS.find((x) => x.id === rowIndex) || {}).sheetPhotos || [])[fileIndex]
  5205. : (attachListOf(source)[rowIndex].attachments || [])[fileIndex];
  5206. if (!file) return;
  5207. document.getElementById("attachTitle").textContent = file.name;
  5208. const body = document.getElementById("attachBody");
  5209. if (file.type && file.type.startsWith("image/") && file.url) {
  5210. body.innerHTML = `<img alt="${file.name}" src="${file.url}" />`;
  5211. } else if (file.url) {
  5212. body.innerHTML = `<p style="margin-bottom:8px;color:var(--muted)">文件可新窗口打开查看。</p><a class="btn btn-primary" href="${file.url}" target="_blank" rel="noopener">打开 ${file.name}</a>`;
  5213. } else {
  5214. body.innerHTML = `<p>该附件已随历史记录存档,当前演示环境未保留原文件内容。</p>`;
  5215. }
  5216. document.getElementById("attachDialog").showModal();
  5217. }
  5218. function closeAttach() {
  5219. document.getElementById("attachDialog").close();
  5220. }
  5221. function etcsInMonth(plate, ym) {
  5222. return etcs.filter((x) =>
  5223. x.plate === plate &&
  5224. String(x.date).slice(0, 7) === ym &&
  5225. x.status !== "草稿" &&
  5226. (x.type === "ETC" || !x.type)
  5227. );
  5228. }
  5229. function renderEtc() {
  5230. const ym = String(today).slice(0, 7);
  5231. const prevYm = ymShift(ym, -1);
  5232. const box = document.getElementById("etcPlateCards");
  5233. if (box) {
  5234. box.innerHTML = vehicles.map((v) => {
  5235. const rows = etcsInMonth(v.plate, ym);
  5236. const prevRows = etcsInMonth(v.plate, prevYm);
  5237. const pay = rows.reduce((s, x) => s + Number(x.amount || 0), 0);
  5238. const prevPay = prevRows.reduce((s, x) => s + Number(x.amount || 0), 0);
  5239. const used = rows.reduce((s, x) => s + Number(x.used || 0), 0);
  5240. const prevUsed = prevRows.reduce((s, x) => s + Number(x.used || 0), 0);
  5241. const runKm = monthRunKm(v.plate, ym);
  5242. const prevKm = monthRunKm(v.plate, prevYm);
  5243. return `<article class="fuel-plate-card" onclick="openEtcDlg('${v.plate}')">
  5244. <div class="plate">${hazPlate(v)}</div>
  5245. <div class="meta">${v.driver || "—"} · ${v.type || ""} · <span class="st ${stClass(v.status)}">${v.status}</span></div>
  5246. <div class="nums four">
  5247. <div><b>${rows.length}</b><span>充值次数</span>${litersMomHtml(rows.length, prevRows.length)}</div>
  5248. <div><b>${money(runKm)}</b><span>当月公里</span>${litersMomHtml(runKm, prevKm)}</div>
  5249. <div><b>${money(pay)}</b><span>充值金额</span>${litersMomHtml(pay, prevPay)}</div>
  5250. <div><b>${money(used)}</b><span>使用金额</span>${litersMomHtml(used, prevUsed)}</div>
  5251. </div>
  5252. </article>`;
  5253. }).join("");
  5254. }
  5255. document.getElementById("etcTable").innerHTML = `<table><thead><tr><th>申请日期</th><th>申请人</th><th>车牌</th><th>充值类型</th><th>充值金额</th><th>使用金额</th><th>期初公里</th><th>状态</th><th>附件</th><th>备注</th></tr></thead><tbody>${
  5256. etcs.map((x, i) => `<tr><td>${x.date}</td><td>${x.applicant}</td><td>${hazPlate(x.plate)}</td><td>${x.type}</td><td>${money(x.amount || 0)}</td><td>${money(x.used || 0)}</td><td>${Number(x.km).toLocaleString()}</td><td><span class="st ${x.status==="已提交"?"st-run":"st-fix"}">${x.status}</span></td><td>${attachChips("etc", i)}</td><td>${x.note || "—"}</td></tr>`).join("")
  5257. }</tbody></table>`;
  5258. }
  5259. function previewMaintFiles(input) {
  5260. const box = document.getElementById("maintFileChips");
  5261. const files = [...input.files];
  5262. box.innerHTML = files.length
  5263. ? files.map((f) => `<span class="chip ghost">${f.name}</span>`).join("")
  5264. : "";
  5265. }
  5266. function openMaintDlg(plate) {
  5267. const form = document.getElementById("maintForm");
  5268. form.reset();
  5269. document.getElementById("maintFileChips").innerHTML = "";
  5270. fillPlates();
  5271. if (plate) form.plate.value = plate;
  5272. document.getElementById("maintDialog").showModal();
  5273. }
  5274. function closeMaintDlg() {
  5275. document.getElementById("maintDialog").close();
  5276. }
  5277. function focusMaintPlate(plate) {
  5278. openMaintDlg(plate);
  5279. }
  5280. function renderMaint() {
  5281. const box = document.getElementById("maintPlateCards");
  5282. const yr = String(today).slice(0, 4);
  5283. const ym = String(today).slice(0, 7);
  5284. if (box) {
  5285. box.innerHTML = vehicles.map((v) => {
  5286. const yearRows = maints.filter((m) => m.plate === v.plate && String(m.inDate).slice(0, 4) === yr);
  5287. const monthRows = yearRows.filter((m) => String(m.inDate).slice(0, 7) === ym);
  5288. const yearAmt = yearRows.reduce((s, m) => s + Number(m.amount || 0), 0);
  5289. const monthAmt = monthRows.reduce((s, m) => s + Number(m.amount || 0), 0);
  5290. return `<article class="fuel-plate-card" onclick="focusMaintPlate('${v.plate}')">
  5291. <div class="plate">${hazPlate(v)}</div>
  5292. <div class="meta">${v.driver || "—"} · ${v.type || ""} · <span class="st ${stClass(v.status)}">${v.status}</span></div>
  5293. <div class="nums">
  5294. <div><b>${yearRows.length}</b><span>本年度维修次数</span></div>
  5295. <div><b>${money(monthAmt)}</b><span>本月合计</span></div>
  5296. <div><b>${money(monthAmt)} / ${money(yearAmt)}</b><span>本月 / 本年度合计</span></div>
  5297. </div>
  5298. </article>`;
  5299. }).join("");
  5300. }
  5301. document.getElementById("maintTable").innerHTML = `<table><thead><tr><th>申请日期</th><th>申请人</th><th>车牌</th><th>类型</th><th>开始日期</th><th>预计取车</th><th>维保内容</th><th>维修店</th><th>附件</th><th>备注</th></tr></thead><tbody>${
  5302. maints.map((x, i) => `<tr><td>${x.applyDate || x.inDate}</td><td>${x.applicant || "—"}</td><td>${hazPlate(x.plate)}</td><td>${x.type}</td><td>${x.inDate}</td><td>${x.outDate || "—"}</td><td>${x.item || "—"}</td><td>${x.shop || "—"}</td><td>${attachChips("maint", i)}</td><td>${x.note || "—"}</td></tr>`).join("")
  5303. }</tbody></table>`;
  5304. }
  5305. function renderStats() {
  5306. const fuelAmt = fuels.reduce((s, x) => s + Number(x.amount), 0);
  5307. const maintAmt = maints.reduce((s, x) => s + Number(x.amount), 0);
  5308. const liters = fuels.reduce((s, x) => s + Number(x.liters), 0);
  5309. document.getElementById("statKpis").innerHTML = [
  5310. ["加油升数", liters, "本月已登记"],
  5311. ["加油费用", money(fuelAmt) + " 元", ""],
  5312. ["维保费用", money(maintAmt) + " 元", "含未出厂"],
  5313. ["ETC 申请", etcs.length, `已提交 ${etcs.filter(x=>x.status==="已提交").length} 笔`]
  5314. ].map(([k,v,s]) => `<div class="stat"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s || "&nbsp;"}</div></div>`).join("");
  5315. const byPlate = {};
  5316. fuels.forEach((f) => { byPlate[f.plate] = (byPlate[f.plate] || 0) + f.liters; });
  5317. const max = Math.max(...Object.values(byPlate), 1);
  5318. document.getElementById("statBars").innerHTML = Object.entries(byPlate).map(([p, n]) =>
  5319. `<div class="bar-row"><span>${p.replace("鄂E·","")}</span><div class="bar-track"><div class="bar-fill" style="width:${(n/max)*100}%"></div></div><span class="n">${n} L</span></div>`
  5320. ).join("");
  5321. const total = fuelAmt + maintAmt || 1;
  5322. const segs = [
  5323. { name: "加油", n: fuelAmt, c: "#148a3c" },
  5324. { name: "维保", n: maintAmt, c: "#c2410c" }
  5325. ];
  5326. let acc = 0;
  5327. const circ = 2 * Math.PI * 10;
  5328. document.getElementById("pie").innerHTML = segs.map((s) => {
  5329. const len = (s.n / total) * circ;
  5330. const dash = `${len} ${circ}`;
  5331. const rot = -90 + acc * 360;
  5332. acc += s.n / total;
  5333. return `<circle r="10" cx="16" cy="16" fill="none" stroke="${s.c}" stroke-width="4" stroke-dasharray="${dash}" transform="rotate(${rot} 16 16)"/>`;
  5334. }).join("");
  5335. document.getElementById("pieLegend").innerHTML = segs.map((s) =>
  5336. `<span><i class="sw" style="background:${s.c}"></i>${s.name} ${money(s.n)} 元</span>`
  5337. ).join("");
  5338. document.getElementById("certTable").innerHTML = `<table><thead><tr><th>车牌</th><th>罐检到期</th><th>剩余</th><th>保险到期</th><th>剩余</th></tr></thead><tbody>${
  5339. vehicles.map((v) => `<tr><td>${v.plate}</td><td>${v.tankDue}</td><td>${daysTo(v.tankDue)} 天</td><td>${v.insDue}</td><td>${daysTo(v.insDue)} 天</td></tr>`).join("")
  5340. }</tbody></table>`;
  5341. }
  5342. function addFuel(e) {
  5343. e.preventDefault();
  5344. const input = e.target.querySelector("[name=attach]");
  5345. if (!input.files || !input.files.length) {
  5346. toast("请上传加油小票或发票");
  5347. return false;
  5348. }
  5349. const fd = new FormData(e.target);
  5350. const f = Object.fromEntries(fd);
  5351. delete f.attach;
  5352. readFiles(input.files).then((attachments) => {
  5353. fuels.unshift({ ...f, liters: +f.liters, amount: +f.amount, km: +f.km, attachments });
  5354. const v = vehicles.find((x) => x.plate === f.plate);
  5355. if (v) v.km = +f.km;
  5356. toast("加油已登记,附件已保存");
  5357. e.target.reset();
  5358. document.getElementById("fuelFileChips").innerHTML = "";
  5359. fillPlates();
  5360. closeFuelDlg();
  5361. renderFuel();
  5362. renderDash();
  5363. if (!document.getElementById("view-fuelwarn").classList.contains("hidden")) renderFuelWarn();
  5364. });
  5365. return false;
  5366. }
  5367. function previewEtcFiles(input) {
  5368. const box = document.getElementById("etcFileChips");
  5369. const files = [...input.files];
  5370. box.innerHTML = files.length
  5371. ? files.map((f) => `<span class="chip ghost">${f.name}</span>`).join("")
  5372. : "";
  5373. }
  5374. function stepKm(delta) {
  5375. const el = document.querySelector("#etcForm [name=km]");
  5376. el.value = Math.max(0, (+el.value || 0) + delta);
  5377. }
  5378. function syncEtcKm() {
  5379. const plate = document.querySelector("#etcForm [name=plate]").value;
  5380. const v = vehicles.find((x) => x.plate === plate);
  5381. if (v) document.querySelector("#etcForm [name=km]").value = v.km;
  5382. }
  5383. function openEtcDlg(plate) {
  5384. const form = document.getElementById("etcForm");
  5385. form.reset();
  5386. document.getElementById("etcFileChips").innerHTML = "";
  5387. fillPlates();
  5388. if (plate) form.plate.value = plate;
  5389. syncEtcKm();
  5390. document.getElementById("etcDialog").showModal();
  5391. }
  5392. function closeEtcDlg() {
  5393. document.getElementById("etcDialog").close();
  5394. }
  5395. function saveEtc(submit) {
  5396. const form = document.getElementById("etcForm");
  5397. if (!form.reportValidity()) return;
  5398. const input = form.querySelector("[name=attach]");
  5399. if (!input.files || !input.files.length) {
  5400. toast("请上传附件");
  5401. return;
  5402. }
  5403. const f = Object.fromEntries(new FormData(form));
  5404. delete f.attach;
  5405. readFiles(input.files).then((attachments) => {
  5406. etcs.unshift({ ...f, km: +f.km, amount: +f.amount || 0, used: +f.used || 0, attachments, status: submit ? "已提交" : "草稿" });
  5407. toast(submit ? "ETC 申请已提交" : "ETC 申请已保存");
  5408. closeEtcDlg();
  5409. renderEtc();
  5410. renderDash();
  5411. });
  5412. }
  5413. function addMaint(e) {
  5414. e.preventDefault();
  5415. const form = document.getElementById("maintForm");
  5416. if (!form.reportValidity()) return false;
  5417. const f = Object.fromEntries(new FormData(form));
  5418. delete f.attach;
  5419. const input = form.querySelector("[name=attach]");
  5420. const save = (attachments) => {
  5421. maints.unshift({
  5422. ...f,
  5423. amount: +f.amount || 0,
  5424. nextKm: f.nextKm ? +f.nextKm : "",
  5425. attachments: attachments || []
  5426. });
  5427. const v = vehicles.find((x) => x.plate === f.plate);
  5428. if (v && !f.outDate) v.status = "维保中";
  5429. toast("维保申请已提交");
  5430. closeMaintDlg();
  5431. renderMaint();
  5432. renderDash();
  5433. };
  5434. if (input && input.files && input.files.length) readFiles(input.files).then(save);
  5435. else save([]);
  5436. return false;
  5437. }
  5438. function transOf(c) { return c.transList || []; }
  5439. function findTrans(c, transId) { return transOf(c).find((t) => t.id === transId) || null; }
  5440. function isPlanDone(p) {
  5441. return !!(p && (p.status === "已完成" || p.archived));
  5442. }
  5443. function deductQty(p) {
  5444. if (!p || p.status === "已作废" || !isPlanDone(p)) return 0;
  5445. return Number(p.realQty != null ? p.realQty : p.planQty || 0);
  5446. }
  5447. function holdQty(p) {
  5448. if (!p || p.status === "已作废" || isPlanDone(p)) return 0;
  5449. return Number(p.planQty || 0);
  5450. }
  5451. function occupyQty(p) {
  5452. return deductQty(p) + holdQty(p);
  5453. }
  5454. function warnQtyOf(v, total) {
  5455. if (v !== "" && v != null) {
  5456. const n = Number(v);
  5457. if (Number.isFinite(n) && n >= 0) return n;
  5458. }
  5459. return defaultWarnQty(total);
  5460. }
  5461. function buyWarnQty(c) { return warnQtyOf(c && c.warnQty, c && c.buyQty); }
  5462. function transWarnQty(t) { return warnQtyOf(t && t.warnQty, t && t.transQty); }
  5463. function warnQtyField(certId, transId, unit) {
  5464. const c = certs.find((x) => x.id === certId);
  5465. const v = transId == null ? buyWarnQty(c) : transWarnQty(c ? findTrans(c, transId) : null);
  5466. const tid = transId == null ? "null" : transId;
  5467. return `<label class="warn-qty-edit" onclick="event.stopPropagation()"><input type="number" min="0" step="0.01" value="${v}" onchange="setCertWarnQty(${certId}, ${tid}, this.value)" /><span>${unit || "吨"}</span></label>`;
  5468. }
  5469. function ensureWarnQtyInputs(root) {
  5470. if (!root) return;
  5471. root.querySelectorAll("input[name=warnQty]").forEach((el) => {
  5472. const box = el.closest(".form") || el.closest(".extra-trans") || el.closest("#buyFormCol") || root;
  5473. const qtyEl = box.querySelector("[name=buyQty]") || box.querySelector("[name=transQty]");
  5474. if (el.value === "" || el.value == null) el.value = defaultWarnQty(qtyEl && qtyEl.value);
  5475. });
  5476. }
  5477. function setCertWarnQty(certId, transId, val) {
  5478. const c = certs.find((x) => x.id === certId);
  5479. if (!c) return;
  5480. if (val === "" || val == null) return;
  5481. const n = Number(val);
  5482. if (!Number.isFinite(n) || n < 0) {
  5483. toast("请输入有效预警量");
  5484. if (currentCertId === certId) renderCertDetail();
  5485. return;
  5486. }
  5487. if (transId == null || transId === "") {
  5488. c.warnQty = n;
  5489. toast(`购买证预警量已设为 ${fmtTon(n)}${c.unit || "吨"}`);
  5490. } else {
  5491. const t = findTrans(c, +transId);
  5492. if (!t) return;
  5493. t.warnQty = n;
  5494. toast(`运输证 ${t.transNo || ""} 预警量已设为 ${fmtTon(n)}${c.unit || "吨"}`);
  5495. }
  5496. if (currentCertId === certId) renderCertDetail();
  5497. renderCertList();
  5498. updateBadges();
  5499. }
  5500. function lowRemain(g, warn) {
  5501. const w = warnQtyOf(warn);
  5502. return g > 0 && g <= w + 1e-9;
  5503. }
  5504. function chemKey(s) {
  5505. const t = String(s || "");
  5506. if (/硝酸铵|硝铵/.test(t)) return "硝酸铵";
  5507. if (/盐酸/.test(t)) return "盐酸";
  5508. if (/硫酸|试剂酸/.test(t)) return "硫酸";
  5509. if (/槽车运输/.test(t)) return "硫酸";
  5510. return t.replace(/\d+(\.\d+)?%/g, "").replace(/工业|试剂/g, "").trim();
  5511. }
  5512. function planMatchesCert(p, c) {
  5513. if (!p || !c) return false;
  5514. if (p.ctrl && c.ctrl && p.ctrl !== c.ctrl) return false;
  5515. const a = chemKey(p.product);
  5516. const b = chemKey(c.product);
  5517. if (!a || !b) return true;
  5518. return a === b || a.includes(b) || b.includes(a);
  5519. }
  5520. function normOrg(s) {
  5521. return String(s || "").replace(/\s+/g, "").replace(/有限责任公司|股份有限公司|有限公司|集团|公司/g, "").toLowerCase();
  5522. }
  5523. function orgMatch(a, b) {
  5524. const x = normOrg(a), y = normOrg(b);
  5525. if (!x || !y) return false;
  5526. return x === y || x.includes(y) || y.includes(x);
  5527. }
  5528. function isShanxiaOrg(s) {
  5529. const t = String(s || "");
  5530. return /山峡化工|山峡/.test(t) || orgMatch(t, "安庆市山峡化工有限公司");
  5531. }
  5532. function planCertScene(p) {
  5533. if (isInboundPlan(p)) return "in";
  5534. if (isDirectSale(p)) return "direct";
  5535. return "out";
  5536. }
  5537. function planSceneLabel(scene) {
  5538. return scene === "in" ? "入库单" : scene === "direct" ? "直销单" : "出库单";
  5539. }
  5540. function planLoadParty(p) {
  5541. return (p && p.loadFrom) || "";
  5542. }
  5543. function planShipParty(p) {
  5544. if (!p) return "";
  5545. if (isInboundPlan(p)) return p.settler || p.customer || "安庆市山峡化工有限公司";
  5546. return p.customer || p.settler || "";
  5547. }
  5548. function planCustName(p) {
  5549. return planShipParty(p);
  5550. }
  5551. function certBuyerMatch(p, c) {
  5552. return certIsShipBuy(p, c);
  5553. }
  5554. function certIsShipBuy(p, c) {
  5555. if (!p || !c) return false;
  5556. if (!planMatchesCert(p, c)) return false;
  5557. return orgMatch(planShipParty(p), c.buyer);
  5558. }
  5559. function certIsProcure(p, c) {
  5560. if (!p || !c) return false;
  5561. if (!planMatchesCert(p, c)) return false;
  5562. if (orgMatch(planLoadParty(p), c.buyer)) return true;
  5563. return isShanxiaOrg(c.buyer) && orgMatch(planLoadParty(p), c.seller);
  5564. }
  5565. function planNeedProcureCert(p) {
  5566. return isDirectSale(p);
  5567. }
  5568. function planCertLinked(p) {
  5569. if (!p) return false;
  5570. if (!p.certId) return false;
  5571. if (planNeedProcureCert(p) && (!p.shipCertId || !p.procureCertId)) return false;
  5572. return true;
  5573. }
  5574. function isWaitListPlan(p) {
  5575. if (!p || p.status !== "已安排") return false;
  5576. if (p.ctrl !== "易制毒") return false;
  5577. return !!String(p.tractor || "").trim();
  5578. }
  5579. function isWaitLinkPlan(p) {
  5580. if (!isWaitListPlan(p)) return false;
  5581. return !p.certId || !p.transId || (planNeedProcureCert(p) && (!p.shipCertId || !p.procureCertId));
  5582. }
  5583. function waitPlanDay(p) {
  5584. return String((p && (p.deliverDate || p.date)) || "");
  5585. }
  5586. function waitWeekStart(dayStr) {
  5587. const d = new Date(String(dayStr) + "T00:00:00");
  5588. if (Number.isNaN(+d)) return 0;
  5589. const wd = d.getDay();
  5590. d.setDate(d.getDate() - (wd === 0 ? 6 : wd - 1));
  5591. d.setHours(0, 0, 0, 0);
  5592. return +d;
  5593. }
  5594. function isWaitTodayPlan(p) {
  5595. return waitPlanDay(p) === today;
  5596. }
  5597. function isWaitWeekPlan(p) {
  5598. const day = waitPlanDay(p);
  5599. if (!day) return false;
  5600. const t = Date.parse(day + "T00:00:00");
  5601. if (!Number.isFinite(t)) return false;
  5602. const w0 = waitWeekStart(today);
  5603. return t >= w0 && t < w0 + 7 * 86400000;
  5604. }
  5605. let waitLinkSceneTab = "";
  5606. function waitListSort(a, b) {
  5607. const sa = planCertScene(a), sb = planCertScene(b);
  5608. const order = { in: 0, out: 1, direct: 2 };
  5609. if (order[sa] !== order[sb]) return order[sa] - order[sb];
  5610. const aToday = (a.date === today || a.deliverDate === today) ? 0 : 1;
  5611. const bToday = (b.date === today || b.deliverDate === today) ? 0 : 1;
  5612. if (aToday !== bToday) return aToday - bToday;
  5613. return String(a.deliverDate || a.date).localeCompare(String(b.deliverDate || b.date));
  5614. }
  5615. function waitListPlans() {
  5616. return plans.filter((p) => isWaitListPlan(p) && (!waitLinkSceneTab || planCertScene(p) === waitLinkSceneTab)).sort(waitListSort);
  5617. }
  5618. function waitLinkPlans() {
  5619. return plans.filter((p) => isWaitLinkPlan(p) && (!waitLinkSceneTab || planCertScene(p) === waitLinkSceneTab)).sort(waitListSort);
  5620. }
  5621. function setWaitLinkScene(tab) {
  5622. waitLinkSceneTab = tab || "";
  5623. renderWaitPlans();
  5624. }
  5625. function platesOfCert(c) {
  5626. const set = new Set();
  5627. transOf(c).forEach((t) => platesOfTrans(t).forEach((pl) => set.add(pl)));
  5628. return [...set];
  5629. }
  5630. function transForPlate(c, plate) {
  5631. if (!c || !plate || certVoided(c)) return null;
  5632. const hits = transOf(c).filter((t) => transSelectable(c, t) && (platesOfTrans(t).includes(plate) || String(t.plates || "").includes(plate)));
  5633. return hits[0] || null;
  5634. }
  5635. function suggestVehicleFromCert(c, p) {
  5636. if (!c) return "";
  5637. const plates = platesOfCert(c);
  5638. if (p && p.tractor && plates.includes(p.tractor)) return p.tractor;
  5639. const ds = allDispatches(c);
  5640. let best = "", bestLeft = -Infinity;
  5641. ds.forEach((d) => {
  5642. const left = Number(d.qty || 0) - vehicleHold(d.plate, d.transId);
  5643. if (left > bestLeft) { bestLeft = left; best = d.plate; }
  5644. });
  5645. return best || plates[0] || (p && p.tractor) || "";
  5646. }
  5647. function rankCertForPlan(p, pred) {
  5648. return certs.map((c) => {
  5649. let score = 0;
  5650. const hit = pred(p, c);
  5651. const match = planMatchesCert(p, c);
  5652. if (hit) score += 10;
  5653. if (match) score += 3;
  5654. if (p.ctrl && c.ctrl && p.ctrl === c.ctrl) score += 2;
  5655. if (remainGap(c) + 1e-9 >= occupyQty(p)) score += 1;
  5656. if (transOf(c).some((x) => transItemRemain(x) + 1e-9 >= occupyQty(p))) score += 1;
  5657. if (transOf(c).some((x) => assignedSum(x) > 1e-9)) score += 4;
  5658. if (transOf(c).some((x) => transAssignLeft(x) + 1e-9 >= occupyQty(p))) score += 5;
  5659. if (p.tractor && transOf(c).some((t) => transSelectable(c, t) && platesOfTrans(t).includes(p.tractor))) score += 4;
  5660. if (certVoided(c)) score -= 50;
  5661. return { c, score, hit };
  5662. }).sort((a, b) => b.score - a.score);
  5663. }
  5664. function suggestShipCert(p) {
  5665. const hit = rankCertForPlan(p, certIsShipBuy).find((x) => x.hit && certSelectable(x.c));
  5666. return (hit && hit.c) || null;
  5667. }
  5668. function suggestProcureCert(p) {
  5669. const hit = rankCertForPlan(p, certIsProcure).find((x) => x.hit && certSelectable(x.c));
  5670. return (hit && hit.c) || null;
  5671. }
  5672. function suggestCertForPlan(p) {
  5673. if (planNeedProcureCert(p)) return suggestProcureCert(p) || suggestShipCert(p);
  5674. return suggestShipCert(p);
  5675. }
  5676. function suggestTransForPlan(c, p) {
  5677. const list = transOf(c).filter((t) => transSelectable(c, t));
  5678. if (!list.length) return null;
  5679. if (p && p.transId) {
  5680. const hit = findTrans(c, p.transId);
  5681. if (hit && transSelectable(c, hit)) return hit;
  5682. }
  5683. if (p && p.tractor) {
  5684. const hit = transForPlate(c, p.tractor);
  5685. if (hit) return hit;
  5686. }
  5687. const qty = occupyQty(p);
  5688. return list.find((t) => transItemRemain(t) + 1e-9 >= qty) || list[0];
  5689. }
  5690. function suggestVehicleForPlan(t, p) {
  5691. if (!t) return "";
  5692. const plates = platesOfTrans(t);
  5693. if (p && p.tractor && (plates.includes(p.tractor) || String(t.plates || "").includes(p.tractor))) return p.tractor;
  5694. const ds = t.dispatches || [];
  5695. let best = "", bestLeft = -Infinity;
  5696. ds.forEach((d) => {
  5697. const left = Number(d.qty || 0) - vehicleHold(d.plate, t.id);
  5698. if (left > bestLeft) { bestLeft = left; best = d.plate; }
  5699. });
  5700. return best || plates[0] || (p && p.tractor) || "";
  5701. }
  5702. function waitPickOf(p) {
  5703. const saved = waitLinkPicks[p.id] || {};
  5704. const scene = planCertScene(p);
  5705. const shipC = (saved.shipCertId && certs.find((x) => x.id === saved.shipCertId)) || suggestShipCert(p);
  5706. let c = (saved.certId && certs.find((x) => x.id === saved.certId)) || null;
  5707. if (!c) c = planNeedProcureCert(p) ? suggestProcureCert(p) : shipC;
  5708. if (!c) return { c: null, t: null, plate: "", shipC, scene, matchCust: false, matchShip: !!shipC && certIsShipBuy(p, shipC), matchProcure: !planNeedProcureCert(p), matchVeh: false };
  5709. const certPlates = platesOfCert(c);
  5710. let plate = saved.plate || "";
  5711. if (plate && !certPlates.includes(plate) && !transForPlate(c, plate)) plate = "";
  5712. if (!plate) plate = suggestVehicleFromCert(c, p);
  5713. let t = saved.transId ? findTrans(c, saved.transId) : null;
  5714. if (plate) {
  5715. const byPlate = transForPlate(c, plate);
  5716. if (byPlate && !saved.transId) t = byPlate;
  5717. if (t && plate && !platesOfTrans(t).includes(plate) && !String(t.plates || "").includes(plate)) t = byPlate || t;
  5718. }
  5719. if (!t) t = suggestTransForPlan(c, p);
  5720. if (t && plate && !platesOfTrans(t).includes(plate) && !String(t.plates || "").includes(plate) && saved.plate == null) {
  5721. plate = suggestVehicleForPlan(t, p);
  5722. }
  5723. const matchShip = !!(shipC && certIsShipBuy(p, shipC));
  5724. const matchProcure = !planNeedProcureCert(p) || !!(c && certIsProcure(p, c));
  5725. return {
  5726. c, t, plate, shipC, scene,
  5727. matchCust: matchShip,
  5728. matchShip, matchProcure,
  5729. matchVeh: !!(plate && t && (platesOfTrans(t).includes(plate) || String(t.plates || "").includes(plate)))
  5730. };
  5731. }
  5732. function afterLinkCalc(c, t, plate, qty, skipId, shipC) {
  5733. const buyAfter = remainGap(c, skipId) - qty;
  5734. const transAfter = t ? transItemGap(t, skipId) - qty : 0;
  5735. const shipAfter = shipC && shipC.id !== (c && c.id) ? remainGap(shipC, skipId) - qty : buyAfter;
  5736. const cap = t && plate ? Number(((t.dispatches || []).find((d) => d.plate === plate) || {}).qty || 0) : 0;
  5737. const vehAfter = plate ? cap - vehicleHold(plate, t && t.id, skipId) - qty : 0;
  5738. const room = vehicleRoom(c, t, plate, skipId, shipC);
  5739. const take = Math.min(qty, room);
  5740. const over = buyAfter < -1e-6 || transAfter < -1e-6 || shipAfter < -1e-6 || (cap > 0 && vehAfter < -1e-6);
  5741. const warn = !over && (
  5742. lowRemain(buyAfter, buyWarnQty(c)) ||
  5743. lowRemain(transAfter, transWarnQty(t)) ||
  5744. (shipC ? lowRemain(shipAfter, buyWarnQty(shipC)) : false) ||
  5745. lowRemain(remainAfterDone(c), buyWarnQty(c))
  5746. );
  5747. return { buyAfter, transAfter, shipAfter, vehAfter, cap, over, warn, room, take };
  5748. }
  5749. const waitLinkPicks = {};
  5750. function fmtTon(n) {
  5751. const x = Number(n || 0);
  5752. return Math.abs(x - Math.round(x)) < 1e-6 ? String(Math.round(x)) : x.toFixed(2);
  5753. }
  5754. function linkedPlansOf(c) {
  5755. return plans.filter((p) => p.status !== "已作废" && (p.certId === c.id || p.shipCertId === c.id || p.procureCertId === c.id));
  5756. }
  5757. function openLinkedPlans(c) {
  5758. return linkedPlansOf(c).filter((p) => !p.archived);
  5759. }
  5760. function reserved(c, skipId) {
  5761. return linkedPlansOf(c).reduce((s, p) => s + (p.id === skipId ? 0 : occupyQty(p)), 0);
  5762. }
  5763. function deductedOf(c, skipId) {
  5764. return linkedPlansOf(c).reduce((s, p) => s + (p.id === skipId ? 0 : deductQty(p)), 0);
  5765. }
  5766. function holdingOf(c, skipId) {
  5767. return linkedPlansOf(c).reduce((s, p) => s + (p.id === skipId ? 0 : holdQty(p)), 0);
  5768. }
  5769. function remainGap(c, skipId) {
  5770. return Number(c.buyQty || 0) - reserved(c, skipId);
  5771. }
  5772. function remainAfterDone(c, skipId) {
  5773. return Number(c.buyQty || 0) - deductedOf(c, skipId);
  5774. }
  5775. function remain(c, skipId) { return Math.max(0, remainGap(c, skipId)); }
  5776. function transItemHold(t, skipId) {
  5777. return plans.filter((p) => p.transId === t.id && p.status !== "已作废" && p.id !== skipId)
  5778. .reduce((s, p) => s + occupyQty(p), 0);
  5779. }
  5780. function transItemDeduct(t, skipId) {
  5781. return plans.filter((p) => p.transId === t.id && p.status !== "已作废" && p.id !== skipId)
  5782. .reduce((s, p) => s + deductQty(p), 0);
  5783. }
  5784. function transItemPlan(t, skipId) {
  5785. return plans.filter((p) => p.transId === t.id && p.status !== "已作废" && p.id !== skipId)
  5786. .reduce((s, p) => s + holdQty(p), 0);
  5787. }
  5788. function transItemGap(t, skipId) {
  5789. return Number(t.transQty || 0) - transItemHold(t, skipId);
  5790. }
  5791. function transItemRemain(t, skipId) { return Math.max(0, transItemGap(t, skipId)); }
  5792. function transItemWarn(t, skipId) {
  5793. if (!t) return false;
  5794. const g = transItemGap(t, skipId);
  5795. const afterDone = Number(t.transQty || 0) - transItemDeduct(t, skipId);
  5796. return g < -1e-6 || lowRemain(g, transWarnQty(t)) || lowRemain(afterDone, transWarnQty(t));
  5797. }
  5798. function transTotal(c) { return transOf(c).reduce((s, t) => s + Number(t.transQty || 0), 0); }
  5799. function transShipped(c) { return transOf(c).reduce((s, t) => s + Number(t.shipped || 0), 0); }
  5800. function transRemain(c, skipId) { return transOf(c).reduce((s, t) => s + transItemRemain(t, skipId), 0); }
  5801. function assignedSum(t) { return (t.dispatches || []).reduce((s, d) => s + Number(d.qty || 0), 0); }
  5802. function assignedSumAll(c) { return transOf(c).reduce((s, t) => s + assignedSum(t), 0); }
  5803. function unassignedTrans(t) { return Math.max(0, Number(t.transQty || 0) - assignedSum(t)); }
  5804. function vehicleHold(plate, transId, skipId) {
  5805. return plans.filter((p) => p.transId === transId && p.tractor === plate && p.status !== "已作废" && p.id !== skipId)
  5806. .reduce((s, p) => s + occupyQty(p), 0);
  5807. }
  5808. function vehiclePlanHold(plate, transId, skipId) {
  5809. return plans.filter((p) => p.transId === transId && p.tractor === plate && p.status !== "已作废" && p.id !== skipId)
  5810. .reduce((s, p) => s + holdQty(p), 0);
  5811. }
  5812. function vehicleDeduct(plate, transId, skipId) {
  5813. return plans.filter((p) => p.transId === transId && p.tractor === plate && p.status !== "已作废" && p.id !== skipId)
  5814. .reduce((s, p) => s + deductQty(p), 0);
  5815. }
  5816. function vehicleBoundQty(c, t, plate, skipId) {
  5817. if (!c || !t) return 0;
  5818. const cap = Number(((t.dispatches || []).find((d) => d.plate === plate) || {}).qty || 0);
  5819. const self = plate ? vehicleHold(plate, t.id, skipId) : 0;
  5820. const buyRoom = remainGap(c, skipId) + self;
  5821. const transRoom = transItemGap(t, skipId) + self;
  5822. const rooms = [buyRoom, transRoom];
  5823. if (cap > 0) rooms.push(cap);
  5824. return Math.max(0, Math.min(...rooms));
  5825. }
  5826. function vehicleRoom(c, t, plate, skipId, shipC) {
  5827. if (!c) return 0;
  5828. const rooms = [remainGap(c, skipId)];
  5829. if (t) rooms.push(transItemGap(t, skipId));
  5830. if (shipC && shipC.id !== c.id) rooms.push(remainGap(shipC, skipId));
  5831. const cap = t && plate ? Number(((t.dispatches || []).find((d) => d.plate === plate) || {}).qty || 0) : 0;
  5832. const used = t && plate ? vehicleHold(plate, t.id, skipId) : 0;
  5833. if (cap > 0) rooms.push(cap - used);
  5834. return Math.max(0, Math.min(...rooms));
  5835. }
  5836. function calcVehPlanQty(c, t, plate, p) {
  5837. const want = Number((p && p.planQty) || 0);
  5838. const shipC = p && p.shipCertId ? certs.find((x) => x.id === p.shipCertId) : null;
  5839. const room = vehicleRoom(c, t, plate, p && p.id, shipC);
  5840. return { want, room, qty: Math.min(want, room) };
  5841. }
  5842. function syncCertShipped(c) {
  5843. if (!c) return;
  5844. c.shipped = deductedOf(c);
  5845. transOf(c).forEach((t) => {
  5846. t.shipped = transItemDeduct(t);
  5847. (t.dispatches || []).forEach((d) => {
  5848. d.shipped = vehicleDeduct(d.plate, t.id);
  5849. });
  5850. });
  5851. }
  5852. function settleCertOnComplete(p) {
  5853. if (!p || !p.certId) return null;
  5854. const n = deductQty(p);
  5855. p.certDeducted = true;
  5856. p.deductedQty = n;
  5857. const cert = certs.find((c) => c.id === p.certId);
  5858. if (!cert) return null;
  5859. syncCertShipped(cert);
  5860. const shipC = p.shipCertId ? certs.find((x) => x.id === p.shipCertId) : null;
  5861. if (shipC && shipC.id !== cert.id) syncCertShipped(shipC);
  5862. const t = findTrans(cert, p.transId);
  5863. const buyLeft = remainAfterDone(cert);
  5864. const transLeft = t ? Number(t.transQty || 0) - transItemDeduct(t) : 0;
  5865. return {
  5866. cert, t, n, buyLeft, transLeft,
  5867. availBuy: remainGap(cert),
  5868. availTrans: t ? transItemGap(t) : 0
  5869. };
  5870. }
  5871. function certQtyWarn(c) {
  5872. return lowRemain(remainGap(c), buyWarnQty(c)) || lowRemain(remainAfterDone(c), buyWarnQty(c));
  5873. }
  5874. function transQtyWarn(t) {
  5875. if (!t) return false;
  5876. const afterDone = Number(t.transQty || 0) - transItemDeduct(t);
  5877. return transItemGap(t) < -1e-6 || lowRemain(transItemGap(t), transWarnQty(t)) || lowRemain(afterDone, transWarnQty(t));
  5878. }
  5879. function vehicleOver(d, t) {
  5880. const cap = Number(d.qty || 0);
  5881. if (cap <= 0) return vehicleHold(d.plate, t.id) > 1e-6;
  5882. return vehicleHold(d.plate, t.id) - cap > 1e-6;
  5883. }
  5884. function platesOfTrans(t) {
  5885. const set = new Set();
  5886. (t.dispatches || []).forEach((d) => { if (d.plate) set.add(d.plate); });
  5887. plans.forEach((p) => {
  5888. if (p.transId === t.id && p.status !== "已作废" && p.tractor) set.add(p.tractor);
  5889. });
  5890. return [...set];
  5891. }
  5892. function unplatedHold(c) {
  5893. return linkedPlansOf(c).filter((p) => !p.tractor).reduce((s, p) => s + occupyQty(p), 0);
  5894. }
  5895. function vehicleAlertOf(c, t, hold, cap, planHold, shipped) {
  5896. const unit = c.unit || "吨";
  5897. const buyG = remainGap(c);
  5898. const transG = transItemGap(t);
  5899. const buyDone = remainAfterDone(c);
  5900. const transDone = Number(t.transQty || 0) - transItemDeduct(t);
  5901. if (transG < -1e-6) return { level: "over", text: `本车计划 ${fmtTon(planHold || hold)}${unit},运输证已超证载 ${fmtTon(-transG)}${unit}` };
  5902. if (buyG < -1e-6) return { level: "over", text: `本车计划 ${fmtTon(planHold || hold)}${unit},购买证已超证载 ${fmtTon(-buyG)}${unit}` };
  5903. if (cap > 0 && hold - cap > 1e-6) return { level: "over", text: `本车计划 ${fmtTon(hold)}${unit} 超过分派 ${fmtTon(cap)}${unit}` };
  5904. if (cap <= 0 && hold > 1e-6) return { level: "warn", text: `本车计划 ${fmtTon(planHold || hold)}${unit},运输证尚未分派该车` };
  5905. if (lowRemain(transG, transWarnQty(t)) || lowRemain(transDone, transWarnQty(t))) return { level: "warn", text: `运输证余量 ${fmtTon(Math.min(transG, transDone))}${unit} ≤ ${fmtTon(transWarnQty(t))} 吨` };
  5906. if (lowRemain(buyG, buyWarnQty(c)) || lowRemain(buyDone, buyWarnQty(c))) return { level: "warn", text: `购买证余量 ${fmtTon(Math.min(buyG, buyDone))}${unit} ≤ ${fmtTon(buyWarnQty(c))} 吨` };
  5907. return { level: "", text: (planHold || shipped) ? `计划 ${fmtTon(planHold || 0)}${unit} · 已完成扣减 ${fmtTon(shipped || 0)}${unit}` : "正常" };
  5908. }
  5909. function vehicleRowsOf(c) {
  5910. const rows = transOf(c).flatMap((t) => platesOfTrans(t).map((plate) => {
  5911. const d = (t.dispatches || []).find((x) => x.plate === plate) || { plate, qty: 0, driver: "", shipped: 0 };
  5912. const hold = vehicleHold(plate, t.id);
  5913. const planHold = vehiclePlanHold(plate, t.id);
  5914. const shipped = vehicleDeduct(plate, t.id);
  5915. const cap = Number(d.qty || 0);
  5916. const bound = vehicleBoundQty(c, t, plate);
  5917. const alert = vehicleAlertOf(c, t, hold, cap, planHold, shipped);
  5918. return { plate, driver: d.driver || "", transId: t.id, transNo: t.transNo, carrier: t.carrier, cap, hold, planHold, shipped, bound, level: alert.level, text: alert.text };
  5919. }));
  5920. const loose = unplatedHold(c);
  5921. if (loose > 1e-6) {
  5922. const t = transOf(c)[0] || { transNo: "—", carrier: "", transQty: 0 };
  5923. rows.push({
  5924. plate: "未派车计划", driver: "—", transId: t.id, transNo: t.transNo || "—", carrier: t.carrier || "",
  5925. cap: 0, hold: loose, planHold: loose, shipped: 0, bound: 0, level: "warn", text: `有 ${fmtTon(loose)}${c.unit} 计划已关联证件但未指定车辆`
  5926. });
  5927. }
  5928. return rows;
  5929. }
  5930. function vehLinkedPlans(plate, transId) {
  5931. const tid = Number(transId);
  5932. if (plate === "未派车计划") {
  5933. return plans.filter((p) => p.status !== "已作废" && Number(p.transId) === tid && !String(p.tractor || "").trim());
  5934. }
  5935. const plateN = String(plate || "").trim();
  5936. return plans.filter((p) => p.status !== "已作废" && Number(p.transId) === tid && String(p.tractor || "").trim() === plateN);
  5937. }
  5938. function openVehPlanDlg(certId, transId, plate) {
  5939. const c = certs.find((x) => x.id === certId);
  5940. const t = c ? findTrans(c, transId) : null;
  5941. const rows = vehLinkedPlans(plate, transId);
  5942. const unit = (c && c.unit) || "吨";
  5943. document.getElementById("vehPlanTitle").textContent =
  5944. `${plate} · ${t ? t.transNo : "运输证"} · 关联计划 ${rows.length} 条`;
  5945. const sumPlan = rows.reduce((s, p) => s + Number(p.planQty || 0), 0);
  5946. const sumDone = rows.reduce((s, p) => s + Number(deductQty(p) || 0), 0);
  5947. document.getElementById("vehPlanBody").innerHTML = rows.length ? `<table>
  5948. <thead><tr><th>计划编号</th><th>状态</th><th>单据类型</th><th>品名</th><th>车牌</th><th>计划量</th><th>已完成量</th><th>送货时间</th></tr></thead>
  5949. <tbody>${rows.map((p) => `<tr>
  5950. <td class="cell-no">${p.no}</td>
  5951. <td>${planStHtml(p)}</td>
  5952. <td>${planTypeName(p)}</td>
  5953. <td>${p.product || "—"}${p.ctrl === "易制毒" ? `<span class="prod-tag red">易制毒</span>` : ""}</td>
  5954. <td>${p.tractor || "未派车"}</td>
  5955. <td class="num">${fmtTon(p.planQty)} ${p.unit || unit}</td>
  5956. <td class="num">${fmtTon(deductQty(p))} ${p.unit || unit}</td>
  5957. <td>${planDeliverAt(p)}</td>
  5958. </tr>`).join("")}</tbody>
  5959. <tfoot><tr>
  5960. <td colspan="5">合计</td>
  5961. <td class="num">${fmtTon(sumPlan)} ${unit}</td>
  5962. <td class="num">${fmtTon(sumDone)} ${unit}</td>
  5963. <td></td>
  5964. </tr></tfoot>
  5965. </table>` : `<div class="empty">该车辆在此运输证下暂无关联计划。</div>`;
  5966. const dlg = document.getElementById("vehPlanDialog");
  5967. if (dlg.open) dlg.close();
  5968. dlg.showModal();
  5969. }
  5970. function closeVehPlanDlg() {
  5971. document.getElementById("vehPlanDialog").close();
  5972. }
  5973. function vehicleWarnCount(c) {
  5974. return vehicleRowsOf(c).filter((r) => r.level).length;
  5975. }
  5976. function certOver(c) {
  5977. if (remainGap(c) < -1e-6) return true;
  5978. return transOf(c).some((t) => transItemGap(t) < -1e-6);
  5979. }
  5980. function transWarn(c) { return transOf(c).some((t) => transItemWarn(t)); }
  5981. function transCount(c) { return transOf(c).length; }
  5982. function transNos(c) { return transOf(c).map((t) => t.transNo).filter(Boolean); }
  5983. function transDueMin(c) {
  5984. const dates = [c.buyDue, ...transOf(c).map((t) => t.transDue)].filter(Boolean);
  5985. return dates.length ? dates.reduce((a, b) => a < b ? a : b) : today;
  5986. }
  5987. function allDispatches(c) {
  5988. return transOf(c).flatMap((t) => (t.dispatches || []).map((d) => ({ ...d, transId: t.id, transNo: t.transNo, carrier: t.carrier })));
  5989. }
  5990. function buyWarn(c) {
  5991. return remainGap(c) < -1e-6 || certQtyWarn(c);
  5992. }
  5993. function sameCompanyCars(company) {
  5994. return carrierFleet.filter((v) => v.company === company);
  5995. }
  5996. function fieldsOf(root) {
  5997. const o = {};
  5998. if (!root) return o;
  5999. root.querySelectorAll("input, select, textarea").forEach((el) => {
  6000. if (el.name && el.type !== "file") o[el.name] = el.value;
  6001. });
  6002. return o;
  6003. }
  6004. function fillFields(root, data) {
  6005. if (!root || !data) return;
  6006. Object.entries(data).forEach(([k, v]) => {
  6007. const el = root.querySelector(`[name="${k}"]`);
  6008. if (el) el.value = v;
  6009. });
  6010. }
  6011. function vehicleWarnText(d, t, unit) {
  6012. const hold = vehicleHold(d.plate, t.id);
  6013. const cap = Number(d.qty || 0);
  6014. if (cap > 0 && hold - cap > 1e-6) return `计划 ${hold}${unit} 超过分派 ${cap}${unit}`;
  6015. if (cap <= 0 && hold > 1e-6) return `计划 ${hold}${unit},尚未分派该车`;
  6016. if (transItemWarn(t)) return `运输证余量 ≤${fmtTon(transWarnQty(t))}吨`;
  6017. return "";
  6018. }
  6019. function certVoided(c) { return !!(c && c.voided); }
  6020. function transVoided(t) { return !!(t && t.voided); }
  6021. function certSelectable(c) { return !!c && !certVoided(c); }
  6022. function transSelectable(c, t) { return certSelectable(c) && !!t && !transVoided(t); }
  6023. function openVoidCertDlg(kind, certId, transId) {
  6024. const c = certs.find((x) => x.id === certId);
  6025. if (!c) return;
  6026. if (kind === "buy") {
  6027. if (certVoided(c)) { toast("该购买证已核销"); return; }
  6028. voidCertTarget = { kind, certId };
  6029. document.getElementById("voidCertTitle").textContent = "核销购买证";
  6030. document.getElementById("voidCertHint").textContent =
  6031. `购买证 ${c.buyNo} 当前余量 ${fmtTon(remain(c))}${c.unit || "吨"}。核销后不可再关联运输计划,已关联在途计划不受影响。`;
  6032. } else {
  6033. const t = findTrans(c, transId);
  6034. if (!t) return;
  6035. if (transVoided(t)) { toast("该运输证已核销"); return; }
  6036. voidCertTarget = { kind, certId, transId };
  6037. document.getElementById("voidCertTitle").textContent = "核销运输证";
  6038. document.getElementById("voidCertHint").textContent =
  6039. `运输证 ${t.transNo} 当前余量 ${fmtTon(transItemRemain(t))}${c.unit || "吨"}。核销后不可再关联运输计划,已关联在途计划不受影响。`;
  6040. }
  6041. document.getElementById("voidCertDialog").showModal();
  6042. }
  6043. function closeVoidCertDlg() {
  6044. document.getElementById("voidCertDialog").close();
  6045. voidCertTarget = null;
  6046. }
  6047. function confirmVoidCert() {
  6048. const tgt = voidCertTarget;
  6049. if (!tgt) return;
  6050. const c = certs.find((x) => x.id === tgt.certId);
  6051. if (!c) return;
  6052. if (tgt.kind === "buy") {
  6053. c.voided = true;
  6054. c.voidedAt = today;
  6055. c.voidQty = remain(c);
  6056. toast(`购买证 ${c.buyNo} 已核销,不可再关联`);
  6057. } else {
  6058. const t = findTrans(c, tgt.transId);
  6059. if (!t) return;
  6060. t.voided = true;
  6061. t.voidedAt = today;
  6062. t.voidQty = transItemRemain(t);
  6063. toast(`运输证 ${t.transNo} 已核销,不可再关联`);
  6064. }
  6065. closeVoidCertDlg();
  6066. refreshCertViews();
  6067. }
  6068. function certStatusOf(c) {
  6069. if (certVoided(c)) return "已核销";
  6070. const r = remainGap(c);
  6071. const tr = transRemain(c);
  6072. const due = daysTo(transDueMin(c));
  6073. if (certOver(c)) return "超证";
  6074. if (r <= 0 && tr <= 0) return "已用尽";
  6075. if (vehicleWarnCount(c)) return "车辆预警";
  6076. if (transWarn(c)) return "运输证预警";
  6077. if (due <= 15) return "即将到期";
  6078. if (buyWarn(c)) return "余量预警";
  6079. return "有余量";
  6080. }
  6081. function certStHtml(s) {
  6082. if (s === "有余量") return `<span class="st st-run">${s}</span>`;
  6083. if (s === "余量预警" || s === "即将到期") return `<span class="st st-fix">${s}</span>`;
  6084. if (s === "运输证预警") return `<span class="st st-due">运输证预警</span>`;
  6085. if (s === "车辆预警") return `<span class="st st-due">车辆计划预警</span>`;
  6086. if (s === "超证") return `<span class="st st-due">超证</span>`;
  6087. return `<span class="st st-due">${s}</span>`;
  6088. }
  6089. function remainHtml(c) {
  6090. const g = remainGap(c);
  6091. const r = remain(c);
  6092. const cls = g < -1e-6 ? "bad" : (r <= 0 ? "bad" : (buyWarn(c) || transWarn(c) ? "warn" : ""));
  6093. return `<span class="remain ${cls}">${g < -1e-6 ? g.toFixed(2) : r} ${c.unit || "吨"}</span><div class="muted">预警 ${fmtTon(buyWarnQty(c))}${c.unit || "吨"}</div>`;
  6094. }
  6095. function transRemainHtml(c) {
  6096. const gaps = transOf(c).map((t) => transItemGap(t));
  6097. const over = gaps.reduce((s, g) => s + Math.min(0, g), 0);
  6098. const r = transRemain(c);
  6099. const cls = over < -1e-6 || r <= 0 ? "bad" : (transWarn(c) ? "warn" : "");
  6100. if (over < -1e-6) return `<span class="remain ${cls}">超 ${(-over).toFixed(2)} ${c.unit || "吨"}</span>`;
  6101. const th = transOf(c).map((t) => transWarnQty(t));
  6102. const uniq = [...new Set(th.map((n) => fmtTon(n)))];
  6103. return `<span class="remain ${cls}">${r} ${c.unit || "吨"}</span><div class="muted">预警 ${uniq.join(" / ")}${c.unit || "吨"}</div>`;
  6104. }
  6105. function certWarnCount() {
  6106. return certs.filter((c) => ["余量预警", "运输证预警", "车辆预警", "即将到期", "已用尽", "超证"].includes(certStatusOf(c))).length;
  6107. }
  6108. function updateBadges() {
  6109. const needLink = plans.filter((p) => p.ctrl && !p.certId && p.status !== "已完成" && p.status !== "已作废").length;
  6110. const waitPrecursor = plans.filter(isWaitPrecursorPlan).length;
  6111. document.getElementById("planBadge").textContent = waitPrecursor || needLink;
  6112. document.getElementById("planBadge").style.display = (waitPrecursor || needLink) ? "" : "none";
  6113. const waitArch = plans.filter((p) => p.status === "已完成" && !p.archived).length;
  6114. const ab = document.getElementById("archiveBadge");
  6115. if (ab) ab.textContent = waitArch;
  6116. document.getElementById("certBadge").textContent = certWarnCount();
  6117. const expN = expenses.filter((x) => ["待财务初审", "待终审", "待付款", "待归档", "已驳回"].includes(x.status)).length
  6118. + preExps.filter((p) => p.status === "待财务初审" || p.status === "待批准").length;
  6119. document.getElementById("expBadge").textContent = expN;
  6120. const tb = document.getElementById("taskBadge");
  6121. if (tb) tb.textContent = TASKS.filter((t) => t.status === "待执行" || t.status === "执行中").length;
  6122. const cb = document.getElementById("ctBadge");
  6123. if (cb) {
  6124. const n = contracts.filter((x) => x.status === "待审批" || x.status === "草稿").length;
  6125. cb.textContent = n;
  6126. cb.style.display = n ? "" : "none";
  6127. }
  6128. }
  6129. const WH_TREE = [
  6130. {
  6131. id: "h2so4", name: "硫酸库", code: "WH-H2SO4", dot: "acid", cap: 3600, unit: "吨",
  6132. manager: "刘洋", area: "高新库区酸罐区",
  6133. ops: ["采购入库", "销货出库", "稀释加工", "精制加工"],
  6134. desc: "98% 工业硫酸可直销出库、加水稀释。试剂硫酸单独成线。工业 / 试剂分罐,不得混兑。",
  6135. children: [
  6136. { id: "h2so4-98", name: "98%工业硫酸库", code: "WH-H2SO4-98", cap: 720, unit: "吨", manager: "刘洋", area: "V0701A V0701B V0701C V0701D V070E V0701F", kind: "raw", line: "industrial", ops: ["采购入库", "销货出库", "稀释加工", "精制加工"], desc: "98%工业硫酸库原料罐:V0701A、V0701B、V0701C、V0701D、V070E、V0701F。可直销出库,可稀释加工成 30%、50% 或其他定制浓度。" },
  6137. { id: "h2so4-sul", name: "硫磺酸库", code: "WH-H2SO4-S", cap: 80, unit: "吨", manager: "刘洋", area: "V0702C", kind: "raw", line: "industrial", ops: ["采购入库", "销货出库", "稀释加工", "精制加工"], desc: "98%硫磺酸存放于 V0702C。可直销出库,也可稀释加工。" },
  6138. { id: "h2so4-rg", name: "试剂硫酸库", code: "WH-H2SO4-RG", cap: 400, unit: "吨", manager: "刘洋", area: "V0702A V0702B V0705B", kind: "raw", line: "reagent", ops: ["采购入库", "销货出库", "稀释加工", "精制加工"], desc: "试剂硫酸库原料罐:V0702A、V0702B、V0705B。可直销出库,也可稀释加工成 30%、50% 试剂酸或其他定制浓度。不与工业酸混用。" },
  6139. { id: "h2so4-30", name: "30%工业硫酸库", code: "WH-H2SO4-30", cap: 800, unit: "吨", manager: "郑浩", area: "V0705A", kind: "product", line: "both", ops: ["销货出库"], desc: "30% 工业硫酸存放于 V0705A。试剂酸分罐存放,可分别出库。" },
  6140. { id: "h2so4-50", name: "50%工业硫酸库", code: "WH-H2SO4-50", cap: 1000, unit: "吨", manager: "郑浩", area: "V0704", kind: "product", line: "both", ops: ["销货出库"], desc: "50% 工业硫酸存放于 V0704。50% 试剂酸存放于 V0301A、V0301B,分罐出库。" },
  6141. { id: "h2so4-custom", name: "定制浓度库", code: "WH-H2SO4-DX", cap: 600, unit: "吨", manager: "郑浩", area: "成品罐区 3#", kind: "product", line: "both", ops: ["销货出库"], desc: "承接订单定制浓度(如 40%、70%、80% 等)。工业酸、试剂酸分罐。" }
  6142. ]
  6143. },
  6144. { id: "naoh", name: "液碱库", code: "WH-NAOH", dot: "alkali", cap: 450, unit: "吨", manager: "陈伟", area: "32%液碱 V0801 V0802 V0803", ops: ["进货入库", "销货出库"], desc: "32% 液碱存放于 V0801、V0802、V0803。", children: [
  6145. { id: "naoh-32", name: "32%液碱库", code: "WH-NAOH-32", cap: 450, unit: "吨", manager: "陈伟", area: "V0801 V0802 V0803", kind: "product", ops: ["进货入库", "销货出库"], desc: "32% 液碱存放于 V0801、V0802、V0803。" }
  6146. ] },
  6147. { id: "hcl", name: "盐酸库", code: "WH-HCL", dot: "hcl", cap: 520, unit: "吨", manager: "刘洋", area: "盐酸罐区", ops: ["采购入库", "销货出库"], desc: "盐酸库分仓:31% 工业盐酸、31% 合成盐酸、36% 分析纯盐酸。", children: [
  6148. { id: "hcl-31-ind", name: "31% 工业盐酸库", code: "WH-HCL-31I", cap: 200, unit: "吨", manager: "刘洋", area: "V0501 V0502", kind: "product", ops: ["采购入库", "销货出库"], desc: "31% 工业盐酸存放于 V0501、V0502。" },
  6149. { id: "hcl-31", name: "31% 合成盐酸库", code: "WH-HCL-31", cap: 200, unit: "吨", manager: "刘洋", area: "V0601 V0602", kind: "product", ops: ["采购入库", "销货出库"], desc: "31% 合成盐酸存放于 V0601、V0602。" },
  6150. { id: "hcl-36", name: "36% 分析纯盐酸库", code: "WH-HCL-36", cap: 120, unit: "吨", manager: "刘洋", area: "V0901 V0902", kind: "product", ops: ["采购入库", "销货出库"], desc: "36% 分析纯盐酸存放于 V0901、V0902。" }
  6151. ] },
  6152. {
  6153. id: "pack", name: "分装库", code: "WH-PACK", dot: "pack", cap: 400, unit: "吨", manager: "胡明", area: "分装车间库",
  6154. ops: ["形态转换入库", "销货出库"], desc: "分装成品按品号管理。",
  6155. children: [
  6156. { id: "pack-s98-25", name: "S-98-25PL", code: "S-98-25PL", cap: 40, unit: "吨", manager: "胡明", area: "分装库", desc: "98%工业硫酸 · 25kg塑桶" },
  6157. { id: "pack-s98-5", name: "S-98-5PL", code: "S-98-5PL", cap: 20, unit: "吨", manager: "胡明", area: "分装库", desc: "98%工业硫酸 · 5kg塑桶" },
  6158. { id: "pack-cp97-25", name: "CP-97-25PL", code: "CP-97-25PL", cap: 30, unit: "吨", manager: "胡明", area: "分装库", desc: "试剂硫酸 97% · 25kg塑桶" },
  6159. { id: "pack-cp97-5", name: "CP-97-5PL", code: "CP-97-5PL", cap: 15, unit: "吨", manager: "胡明", area: "分装库", desc: "试剂硫酸 97% · 5kg塑桶" },
  6160. { id: "pack-h31", name: "H-31-25PL", code: "H-31-25PL", cap: 30, unit: "吨", manager: "胡明", area: "分装库", desc: "31%盐酸 · 25kg塑桶" },
  6161. { id: "pack-ah36", name: "AH-36-25PL", code: "AH-36-25PL", cap: 25, unit: "吨", manager: "胡明", area: "分装库", desc: "36%醋酸 · 25kg塑桶" },
  6162. { id: "pack-c10", name: "C-10-25PL", code: "C-10-25PL", cap: 30, unit: "吨", manager: "胡明", area: "分装库", desc: "10%次氯酸钠 · 25kg塑桶" },
  6163. { id: "pack-c13", name: "C-13-25PL", code: "C-13-25PL", cap: 25, unit: "吨", manager: "胡明", area: "分装库", desc: "13%次氯酸钠 · 25kg塑桶" }
  6164. ]
  6165. },
  6166. {
  6167. id: "solid", name: "固体库", code: "WH-SOLID", dot: "solid", cap: 600, unit: "吨", manager: "吴涛", area: "固体危化库",
  6168. ops: ["进货入库", "销货出库"], desc: "固体库按货源分仓:片碱库(君正)、片碱库(红四方)、纯碱库(红四方)。",
  6169. children: [
  6170. { id: "solid-jz", name: "片碱库(君正)", code: "WH-SOLID-JZ", cap: 200, unit: "吨", manager: "吴涛", area: "固体库 · 君正片碱区", ops: ["进货入库", "销货出库"], desc: "存放君正货源片碱。" },
  6171. { id: "solid-hsf", name: "片碱库(红四方)", code: "WH-SOLID-HSF", cap: 200, unit: "吨", manager: "吴涛", area: "固体库 · 红四方片碱区", ops: ["进货入库", "销货出库"], desc: "存放红四方货源片碱。" },
  6172. { id: "solid-soda", name: "纯碱库(红四方)", code: "WH-SOLID-SODA", cap: 200, unit: "吨", manager: "吴涛", area: "固体库 · 红四方纯碱区", ops: ["进货入库", "销货出库"], desc: "存放红四方货源纯碱。" }
  6173. ]
  6174. }
  6175. ];
  6176. const WH_STOCK = [
  6177. { wh: "h2so4-98", name: "98%工业硫酸", grade: "industrial", conc: 98, qty: 326.0, cost: 410.0, unit: "吨", loc: "V0701A / V0701B / V0701C / V0701D / V070E / V0701F", tankCap: 480, tanks: [
  6178. { id: "V0701A", qty: 58, cap: 80 }, { id: "V0701B", qty: 54, cap: 80 }, { id: "V0701C", qty: 52, cap: 80 },
  6179. { id: "V0701D", qty: 56, cap: 80 }, { id: "V070E", qty: 50, cap: 80 }, { id: "V0701F", qty: 56, cap: 80 }
  6180. ] },
  6181. { wh: "h2so4-sul", name: "98%硫磺酸", grade: "industrial", conc: 98, qty: 42.0, cost: 390.0, unit: "吨", loc: "V0702C", tankCap: 80, tanks: [
  6182. { id: "V0702C", qty: 42, cap: 80 }
  6183. ] },
  6184. { wh: "h2so4-rg", name: "试剂硫酸", grade: "reagent", conc: 98, qty: 86.5, cost: 1280.0, unit: "吨", loc: "V0702A / V0702B / V0705B", tankCap: 240, tanks: [
  6185. { id: "V0702A", qty: 32, cap: 80 }, { id: "V0702B", qty: 30, cap: 80 }, { id: "V0705B", qty: 24.5, cap: 80 }
  6186. ] },
  6187. { wh: "h2so4-30", name: "30%工业硫酸", grade: "industrial", conc: 30, qty: 186.4, cost: 168.0, unit: "吨", loc: "V0705A", tankCap: 400, tanks: [
  6188. { id: "V0705A", qty: 186.4, cap: 400 }
  6189. ] },
  6190. { wh: "h2so4-30", name: "30%试剂酸", grade: "reagent", conc: 30, qty: 42.0, cost: 460.0, unit: "吨", loc: "30%罐 2#", tankCap: 200 },
  6191. { wh: "h2so4-50", name: "50%工业硫酸", grade: "industrial", conc: 50, qty: 312.8, cost: 248.6, unit: "吨", loc: "V0704", tankCap: 600, tanks: [
  6192. { id: "V0704", qty: 312.8, cap: 600 }
  6193. ] },
  6194. { wh: "h2so4-50", name: "50%试剂酸", grade: "reagent", conc: 50, qty: 58.0, cost: 720.0, unit: "吨", loc: "V0301A / V0301B", tankCap: 250, tanks: [
  6195. { id: "V0301A", qty: 30, cap: 125 }, { id: "V0301B", qty: 28, cap: 125 }
  6196. ] },
  6197. { wh: "h2so4-custom", name: "40%工业硫酸", grade: "industrial", conc: 40, qty: 36.0, cost: 210.0, unit: "吨", loc: "定制罐 1#", tankCap: 300 },
  6198. { wh: "h2so4-custom", name: "70%试剂酸", grade: "reagent", conc: 70, qty: 18.0, cost: 980.0, unit: "吨", loc: "定制罐 2#", tankCap: 200 },
  6199. { wh: "h2so4-custom", name: "32%工业硫酸", grade: "industrial", conc: 32, qty: 24.0, cost: 185.0, unit: "吨", loc: "定制罐 3#", tankCap: 200 },
  6200. { wh: "hcl-31-ind", name: "31%工业盐酸", conc: 31, qty: 118.0, cost: 185.0, unit: "吨", loc: "V0501 / V0502", tankCap: 200, tanks: [
  6201. { id: "V0501", qty: 62, cap: 100 }, { id: "V0502", qty: 56, cap: 100 }
  6202. ] },
  6203. { wh: "hcl-31", name: "31%合成盐酸", conc: 31, qty: 86.0, cost: 220.0, unit: "吨", loc: "V0601 / V0602", tankCap: 200, tanks: [
  6204. { id: "V0601", qty: 44, cap: 100 }, { id: "V0602", qty: 42, cap: 100 }
  6205. ] },
  6206. { wh: "hcl-36", name: "36%分析纯盐酸", conc: 36, qty: 28.0, cost: 860.0, unit: "吨", loc: "V0901 / V0902", tankCap: 120, tanks: [
  6207. { id: "V0901", qty: 16, cap: 60 }, { id: "V0902", qty: 12, cap: 60 }
  6208. ] },
  6209. { wh: "naoh-32", name: "32%液碱", qty: 420.0, cost: 980.0, unit: "吨", loc: "V0801 / V0802 / V0803", tankCap: 450, tanks: [
  6210. { id: "V0801", qty: 145, cap: 150 }, { id: "V0802", qty: 140, cap: 150 }, { id: "V0803", qty: 135, cap: 150 }
  6211. ] },
  6212. { wh: "pack-s98-25", sku: "S-98-25PL", name: "98%工业硫酸", packSpec: "25kg塑桶", qty: 12.6, cost: 520.0, unit: "吨", loc: "S-98-25PL", tankCap: 40 },
  6213. { wh: "pack-s98-5", sku: "S-98-5PL", name: "98%工业硫酸", packSpec: "5kg塑桶", qty: 4.8, cost: 560.0, unit: "吨", loc: "S-98-5PL", tankCap: 20 },
  6214. { wh: "pack-cp97-25", sku: "CP-97-25PL", name: "试剂硫酸 97%", packSpec: "25kg塑桶", qty: 8.2, cost: 1380.0, unit: "吨", loc: "CP-97-25PL", tankCap: 30 },
  6215. { wh: "pack-cp97-5", sku: "CP-97-5PL", name: "试剂硫酸 97%", packSpec: "5kg塑桶", qty: 2.4, cost: 1460.0, unit: "吨", loc: "CP-97-5PL", tankCap: 15 },
  6216. { wh: "pack-h31", sku: "H-31-25PL", name: "31%盐酸", packSpec: "25kg塑桶", qty: 9.5, cost: 280.0, unit: "吨", loc: "H-31-25PL", tankCap: 30 },
  6217. { wh: "pack-ah36", sku: "AH-36-25PL", name: "36%醋酸", packSpec: "25kg塑桶", qty: 6.1, cost: 420.0, unit: "吨", loc: "AH-36-25PL", tankCap: 25 },
  6218. { wh: "pack-c10", sku: "C-10-25PL", name: "10%次氯酸钠", packSpec: "25kg塑桶", qty: 7.3, cost: 310.0, unit: "吨", loc: "C-10-25PL", tankCap: 30 },
  6219. { wh: "pack-c13", sku: "C-13-25PL", name: "13%次氯酸钠", packSpec: "25kg塑桶", qty: 5.8, cost: 340.0, unit: "吨", loc: "C-13-25PL", tankCap: 25 },
  6220. { wh: "solid-jz", name: "片碱", supplier: "君正", qty: 48.0, cost: 3050.0, unit: "吨", loc: "片碱库(君正)" },
  6221. { wh: "solid-hsf", name: "片碱", supplier: "红四方", qty: 37.0, cost: 3120.0, unit: "吨", loc: "片碱库(红四方)" },
  6222. { wh: "solid-soda", name: "纯碱", supplier: "红四方", qty: 52.0, cost: 1680.0, unit: "吨", loc: "纯碱库(红四方)" }
  6223. ];
  6224. const WH_LEDGER = [
  6225. { date: "2026-09-29", doc: "形态转换单", no: "ZX-20260929-004", wh: "h2so4-98", product: "98%工业硫酸", inQty: 0, inCost: 0, outQty: 10.2, outCost: 4182, balQty: 346.8, price: 410.0, balCost: 142188, note: "加水制成 50%工业硫酸 20 吨" },
  6226. { date: "2026-09-29", doc: "形态转换单", no: "ZX-20260929-004", wh: "h2so4-50", product: "50%工业硫酸", inQty: 20.0, inCost: 4182, outQty: 0, outCost: 0, balQty: 312.8, price: 248.6, balCost: 77762, note: "由 98%工业硫酸加水" },
  6227. { date: "2026-09-28", doc: "进货单", no: "JH-20260928-014", wh: "h2so4-98", product: "98%工业硫酸", partner: "湖北兴发化工集团股份有限公司", inQty: 64.2, inCost: 26322, outQty: 0, outCost: 0, balQty: 336.2, price: 410.0, balCost: 137842 },
  6228. { date: "2026-09-28", doc: "销货单", no: "XH-20260928-031", wh: "h2so4-98", product: "98%工业硫酸", partner: "安徽海康药业股份有限公司", inQty: 0, inCost: 0, outQty: 32.0, outCost: 13120, balQty: 272.0, price: 410.0, balCost: 111520, note: "原料酸直接出库" },
  6229. { date: "2026-09-27", doc: "形态转换单", no: "ZX-20260927-003", wh: "h2so4-rg", product: "试剂硫酸", inQty: 0, inCost: 0, outQty: 6.12, outCost: 7834, balQty: 86.5, price: 1280.0, balCost: 110720, note: "加工成 30%试剂酸 20 吨" },
  6230. { date: "2026-09-27", doc: "形态转换单", no: "ZX-20260927-003", wh: "h2so4-30", product: "30%试剂酸", inQty: 20.0, inCost: 7834, outQty: 0, outCost: 0, balQty: 42.0, price: 460.0, balCost: 19320, note: "由试剂硫酸加工" },
  6231. { date: "2026-09-26", doc: "进货单", no: "JH-20260926-011", wh: "h2so4-rg", product: "试剂硫酸", partner: "湖北兴发化工集团股份有限公司", inQty: 18.0, inCost: 23040, outQty: 0, outCost: 0, balQty: 92.6, price: 1280.0, balCost: 118528 },
  6232. { date: "2026-09-26", doc: "形态转换单", no: "ZX-20260926-002", wh: "h2so4-98", product: "98%工业硫酸", inQty: 0, inCost: 0, outQty: 8.16, outCost: 3346, balQty: 304.0, price: 410.0, balCost: 124640, note: "加水制成 40%工业硫酸 20 吨" },
  6233. { date: "2026-09-26", doc: "形态转换单", no: "ZX-20260926-002", wh: "h2so4-custom", product: "40%工业硫酸", inQty: 20.0, inCost: 3346, outQty: 0, outCost: 0, balQty: 36.0, price: 210.0, balCost: 7560, note: "定制浓度" },
  6234. { date: "2026-09-25", doc: "销货单", no: "XH-20260925-019", wh: "h2so4-50", product: "50%工业硫酸", partner: "合肥衡工环境科技有限公司", inQty: 0, inCost: 0, outQty: 28.0, outCost: 6961, balQty: 292.8, price: 248.6, balCost: 72790 },
  6235. { date: "2026-09-25", doc: "形态转换单", no: "ZX-20260925-002", wh: "h2so4-98", product: "98%工业硫酸", inQty: 0, inCost: 0, outQty: 8.0, outCost: 4160, balQty: 318.0, price: 410.0, balCost: 130380, note: "分装 25kg塑桶" },
  6236. { date: "2026-09-25", doc: "形态转换单", no: "ZX-20260925-002", wh: "pack-s98-25", product: "S-98-25PL", inQty: 8.0, inCost: 4160, outQty: 0, outCost: 0, balQty: 12.6, price: 520.0, balCost: 6552, note: "98%工业硫酸分装 25kg塑桶" },
  6237. { date: "2026-09-24", doc: "形态转换单", no: "ZX-20260924-004", wh: "h2so4-98", product: "98%工业硫酸", inQty: 0, inCost: 0, outQty: 6.12, outCost: 2510, balQty: 297.9, price: 410.0, balCost: 122139, note: "加水制成 30%工业硫酸 20 吨" },
  6238. { date: "2026-09-24", doc: "形态转换单", no: "ZX-20260924-004", wh: "h2so4-30", product: "30%工业硫酸", inQty: 20.0, inCost: 2510, outQty: 0, outCost: 0, balQty: 186.4, price: 168.0, balCost: 31315, note: "由 98%工业硫酸加水" },
  6239. { date: "2026-09-23", doc: "形态转换单", no: "ZX-20260923-007", wh: "h2so4-rg", product: "试剂硫酸", inQty: 0, inCost: 0, outQty: 4.0, outCost: 5120, balQty: 82.5, price: 1280.0, balCost: 105600, note: "分装 25kg塑桶" },
  6240. { date: "2026-09-23", doc: "形态转换单", no: "ZX-20260923-007", wh: "pack-cp97-25", product: "CP-97-25PL", inQty: 4.0, inCost: 5120, outQty: 0, outCost: 0, balQty: 8.2, price: 1380.0, balCost: 11316, note: "试剂硫酸分装 25kg塑桶" },
  6241. { date: "2026-09-24", doc: "进货单", no: "JH-20260924-009", wh: "naoh-32", product: "32%液碱", partner: "内蒙古君正能源化工集团股份有限公司", inQty: 58.0, inCost: 56840, outQty: 0, outCost: 0, balQty: 420.0, price: 980.0, balCost: 411600 },
  6242. { date: "2026-09-24", doc: "销货单", no: "XH-20260924-022", wh: "naoh-32", product: "32%液碱", partner: "安徽海康药业股份有限公司", inQty: 0, inCost: 0, outQty: 26.5, outCost: 25970, balQty: 420.0, price: 980.0, balCost: 411600, note: "V0801 / V0802 / V0803" },
  6243. { date: "2026-09-23", doc: "其他入库单", no: "QR-20260923-004", wh: "solid-jz", product: "片碱", partner: "内蒙古君正能源化工集团股份有限公司", inQty: 12.0, inCost: 36600, outQty: 0, outCost: 0, balQty: 48.0, price: 3050.0, balCost: 146400, note: "君正货源" },
  6244. { date: "2026-09-23", doc: "进货单", no: "JH-20260923-008", wh: "solid-hsf", product: "片碱", partner: "安徽红四方股份有限公司", inQty: 10.0, inCost: 31200, outQty: 0, outCost: 0, balQty: 37.0, price: 3120.0, balCost: 115440, note: "红四方货源" },
  6245. { date: "2026-09-22", doc: "材料出库单", no: "CL-20260922-005", wh: "naoh-32", product: "32%液碱", inQty: 0, inCost: 0, outQty: 15.0, outCost: 14700, balQty: 362.0, price: 980.0, balCost: 354760 },
  6246. { date: "2026-09-22", doc: "形态转换单", no: "ZX-20260922-001", wh: "hcl-31", product: "31%合成盐酸", inQty: 0, inCost: 0, outQty: 2.5, outCost: 550, balQty: 83.5, price: 220.0, balCost: 18370, note: "分装 25kg塑桶" },
  6247. { date: "2026-09-22", doc: "形态转换单", no: "ZX-20260922-001", wh: "pack-h31", product: "H-31-25PL", inQty: 2.5, inCost: 700, outQty: 0, outCost: 0, balQty: 9.5, price: 280.0, balCost: 2660, note: "31%合成盐酸分装 25kg塑桶" },
  6248. { date: "2026-09-20", doc: "形态转换单", no: "ZX-20260920-008", wh: "hcl-31-ind", product: "31%工业盐酸", inQty: 0, inCost: 0, outQty: 3.0, outCost: 555, balQty: 115.0, price: 185.0, balCost: 21275, note: "分装 25kg塑桶" },
  6249. { date: "2026-09-20", doc: "形态转换单", no: "ZX-20260920-008", wh: "pack-h31", product: "H-31-25PL", inQty: 3.0, inCost: 840, outQty: 0, outCost: 0, balQty: 9.5, price: 280.0, balCost: 2660, note: "31%工业盐酸分装 25kg塑桶" },
  6250. { date: "2026-09-21", doc: "其他出库单", no: "QC-20260921-002", wh: "solid-soda", product: "纯碱", inQty: 0, inCost: 0, outQty: 3.0, outCost: 5040, balQty: 52.0, price: 1680.0, balCost: 87360, note: "红四方纯碱出库" },
  6251. { date: "2026-09-20", doc: "销货单", no: "XH-20260920-018", wh: "h2so4-rg", product: "试剂硫酸", partner: "合肥衡工环境科技有限公司", inQty: 0, inCost: 0, outQty: 8.0, outCost: 10240, balQty: 80.6, price: 1280.0, balCost: 103168, note: "原料试剂酸直接出库" },
  6252. { date: "2026-09-22", doc: "进货单", no: "JH-20260922-006", wh: "hcl-31-ind", product: "31%工业盐酸", partner: "铜陵锦帆物资有限责任公司", inQty: 28.0, inCost: 5180, outQty: 0, outCost: 0, balQty: 118.0, price: 185.0, balCost: 21830 },
  6253. { date: "2026-09-21", doc: "销货单", no: "XH-20260921-016", wh: "hcl-31", product: "31%合成盐酸", partner: "合肥衡工环境科技有限公司", inQty: 0, inCost: 0, outQty: 18.0, outCost: 3960, balQty: 86.0, price: 220.0, balCost: 18920 },
  6254. { date: "2026-09-19", doc: "进货单", no: "JH-20260919-005", wh: "hcl-36", product: "36%分析纯盐酸", partner: "湖北兴发化工集团股份有限公司", inQty: 8.0, inCost: 6880, outQty: 0, outCost: 0, balQty: 28.0, price: 860.0, balCost: 24080 },
  6255. ];
  6256. let currentWh = "h2so4";
  6257. let currentWhDoc = "库存成本明细账";
  6258. let currentWhView = "info";
  6259. let whDocSeq = 10;
  6260. const WH_PROC_PARENTS = ["h2so4", "hcl", "pack", "naoh"];
  6261. const WH_PROCESS = [
  6262. { no: "JG-20260929-001", date: "2026-09-29", mode: "dilute", rawName: "98%工业硫酸", rawWh: "h2so4-98", rawQty: 10.2, water: 9.8, product: "50%工业硫酸", toWh: "h2so4-50", outQty: 20, finIn: 20 },
  6263. { no: "JG-20260927-002", date: "2026-09-27", mode: "dilute", rawName: "试剂硫酸", rawWh: "h2so4-rg", rawQty: 6.12, water: 13.88, product: "30%试剂酸", toWh: "h2so4-30", outQty: 20, finIn: 20 },
  6264. { no: "JG-20260926-003", date: "2026-09-26", mode: "dilute", rawName: "98%工业硫酸", rawWh: "h2so4-98", rawQty: 8.16, water: 11.84, product: "40%工业硫酸", toWh: "h2so4-custom", outQty: 20, finIn: 20 },
  6265. { no: "JG-20260924-004", date: "2026-09-24", mode: "dilute", rawName: "98%工业硫酸", rawWh: "h2so4-98", rawQty: 6.12, water: 13.88, product: "30%工业硫酸", toWh: "h2so4-30", outQty: 20, finIn: 20 },
  6266. { no: "JG-20260925-005", date: "2026-09-25", mode: "convert", rawName: "98%工业硫酸", rawWh: "h2so4-98", rawQty: 8.0, water: 0, product: "S-98-25PL", toWh: "pack-s98-25", outQty: 8.0, finIn: 8.0 },
  6267. { no: "JG-20260923-007", date: "2026-09-23", mode: "convert", rawName: "试剂硫酸", rawWh: "h2so4-rg", rawQty: 4.0, water: 0, product: "CP-97-25PL", toWh: "pack-cp97-25", outQty: 4.0, finIn: 4.0 },
  6268. { no: "JG-20260922-006", date: "2026-09-22", mode: "convert", rawName: "31%合成盐酸", rawWh: "hcl-31", rawQty: 2.5, water: 0, product: "H-31-25PL", toWh: "pack-h31", outQty: 2.5, finIn: 2.5 },
  6269. { no: "JG-20260920-008", date: "2026-09-20", mode: "convert", rawName: "31%工业盐酸", rawWh: "hcl-31-ind", rawQty: 3.0, water: 0, product: "H-31-25PL", toWh: "pack-h31", outQty: 3.0, finIn: 3.0 }
  6270. ];
  6271. function whIdsOf(id) {
  6272. const node = WH_TREE.find((w) => w.id === id);
  6273. if (node) return [id, ...node.children.map((c) => c.id)];
  6274. return [id];
  6275. }
  6276. function whNameOf(id) {
  6277. for (const w of WH_TREE) {
  6278. if (w.id === id) return w.name;
  6279. const c = w.children.find((x) => x.id === id);
  6280. if (c) return w.name + " / " + c.name;
  6281. }
  6282. return id;
  6283. }
  6284. function warehouseSelectHtml(selected) {
  6285. return WH_TREE.map((w) => {
  6286. if (w.children.length) {
  6287. return `<optgroup label="${w.name}">${w.children.map((c) =>
  6288. `<option value="${c.id}" ${selected === c.id ? "selected" : ""}>${c.name}</option>`
  6289. ).join("")}</optgroup>`;
  6290. }
  6291. return `<option value="${w.id}" ${selected === w.id ? "selected" : ""}>${w.name}</option>`;
  6292. }).join("");
  6293. }
  6294. function planTypeName(p) {
  6295. return p.type === "入直单" ? "入库单" : p.type;
  6296. }
  6297. function isInboundPlan(p) {
  6298. return p.type === "入直单" || p.type === "入库单";
  6299. }
  6300. function isDirectSale(p) {
  6301. return p.type === "直销单";
  6302. }
  6303. function warehouseForPlan(p) {
  6304. if (isDirectSale(p)) return "";
  6305. if (p.archived && p.warehouseId) return p.warehouseId;
  6306. return suggestWarehouse(p.product, p.spec) || p.warehouseId || "";
  6307. }
  6308. function parseConc(text) {
  6309. const m = String(text || "").match(/(\d+(?:\.\d+)?)\s*[%%]/);
  6310. return m ? Number(m[1]) : null;
  6311. }
  6312. function acidGradeOf(text) {
  6313. const p = String(text || "");
  6314. if (p.includes("试剂") || p.includes("精制")) return "reagent";
  6315. return "industrial";
  6316. }
  6317. function gradeName(g) {
  6318. return g === "reagent" ? "试剂酸" : "工业酸";
  6319. }
  6320. function suggestWarehouse(product, spec) {
  6321. const p = `${product || ""}${spec || ""}`.replace(/\s/g, "");
  6322. if (p.includes("分析纯") && p.includes("盐酸") || (p.includes("36%") && p.includes("盐酸") && !/塑桶|醋酸/.test(p))) return "hcl-36";
  6323. if (p.includes("合成盐酸")) return "hcl-31";
  6324. if (p.includes("工业盐酸") || (p.includes("31%") && p.includes("盐酸") && !/塑桶|H-31/.test(p))) return "hcl-31-ind";
  6325. if (p.includes("烧碱") || p.includes("液碱")) return "naoh-32";
  6326. if (p.includes("纯碱")) return "solid-soda";
  6327. if (p.includes("片碱") && (p.includes("君正") || p.includes("JZ"))) return "solid-jz";
  6328. if (p.includes("片碱") && (p.includes("红四方") || p.includes("HSF"))) return "solid-hsf";
  6329. if (p.includes("片碱")) return "solid";
  6330. if (/S-98|CP-97|H-31|AH-36|C-10|C-13|盐酸|醋酸|次氯|塑桶/.test(p)) return "pack";
  6331. if (p.includes("硫磺酸")) return "h2so4-sul";
  6332. const isAcid = /硫酸|试剂酸|硫磺酸/.test(p);
  6333. if (!isAcid) return "";
  6334. const conc = parseConc(p);
  6335. const grade = acidGradeOf(p);
  6336. if (conc != null) {
  6337. if (conc >= 95) {
  6338. if (grade === "reagent") return "h2so4-rg";
  6339. return "h2so4-98";
  6340. }
  6341. if (Math.abs(conc - 30) < 0.6) return "h2so4-30";
  6342. if (Math.abs(conc - 50) < 0.6) return "h2so4-50";
  6343. return "h2so4-custom";
  6344. }
  6345. if (grade === "reagent") return "h2so4-rg";
  6346. return "h2so4-98";
  6347. }
  6348. function warehouseCell(p) {
  6349. if (isDirectSale(p)) return `<span class="muted">无中转仓库</span>`;
  6350. const wh = warehouseForPlan(p);
  6351. if (!wh) return `<span class="muted">未匹配品名</span>`;
  6352. const stock = matchStock(wh, `${p.product || ""}${p.spec || ""}`);
  6353. const tank = stock ? `<div class="muted">${stock.loc}${stock.grade ? " · " + gradeName(stock.grade) : ""}</div>` : "";
  6354. return `<b>${whNameOf(wh)}</b>${tank}`;
  6355. }
  6356. function stockActCell(p) {
  6357. if (isDirectSale(p)) return `<span class="wh-act none">不记库存</span>`;
  6358. if (isInboundPlan(p)) return `<span class="wh-act in">入库 · 进货</span>`;
  6359. return `<span class="wh-act out">出库 · 销货</span>`;
  6360. }
  6361. function gradeHtml(s) {
  6362. if (s.grade === "industrial") return `<span class="grade ind">工业酸</span>`;
  6363. if (s.grade === "reagent") return `<span class="grade rg">试剂酸</span>`;
  6364. return "";
  6365. }
  6366. function matchStock(whId, product) {
  6367. const list = WH_STOCK.filter((s) => s.wh === whId);
  6368. if (!list.length) return null;
  6369. const compact = String(product || "").replace(/\s/g, "");
  6370. const exact = list.find((s) => {
  6371. const n = s.name.replace(/\s/g, "");
  6372. return compact.includes(n) || n.includes(compact);
  6373. });
  6374. if (exact) return exact;
  6375. const conc = parseConc(compact);
  6376. const grade = acidGradeOf(compact);
  6377. if (list.some((s) => s.grade)) {
  6378. return list.find((s) => s.grade === grade && (conc == null || Number(s.conc) === conc))
  6379. || list.find((s) => s.grade === grade && conc != null && Math.abs(Number(s.conc) - conc) < 0.6)
  6380. || list.find((s) => s.grade === grade)
  6381. || list.find((s) => conc != null && Number(s.conc) === conc)
  6382. || list[0];
  6383. }
  6384. if (conc != null) {
  6385. const byc = list.find((s) => parseConc(s.name) === conc);
  6386. if (byc) return byc;
  6387. }
  6388. return list[0];
  6389. }
  6390. function assignTankLoc(wh, grade) {
  6391. const map = {
  6392. "h2so4-30": { industrial: "V0705A", reagent: "30%罐 2#" },
  6393. "h2so4-50": { industrial: "V0704", reagent: "V0301A / V0301B" },
  6394. "h2so4-custom": { industrial: "定制罐 1#", reagent: "定制罐 2#" },
  6395. "h2so4-98": { industrial: "V0701A" },
  6396. "h2so4-sul": { industrial: "V0702C" },
  6397. "h2so4-rg": { reagent: "V0702A" }
  6398. };
  6399. return (map[wh] && map[wh][grade]) || "储罐";
  6400. }
  6401. function stockTankRows(s) {
  6402. if (s.tanks && s.tanks.length) {
  6403. return s.tanks.map((t) => ({
  6404. id: t.id,
  6405. qty: t.qty,
  6406. cap: t.cap || 80,
  6407. note: t.note || "",
  6408. label: t.note ? `${t.id}(${t.note})` : t.id
  6409. }));
  6410. }
  6411. return [{ id: s.loc, qty: s.qty, cap: s.tankCap || 200, note: "", label: s.loc }];
  6412. }
  6413. function tankChipList(stock) {
  6414. const labels = [];
  6415. stock.forEach((s) => {
  6416. stockTankRows(s).forEach((t) => {
  6417. String(t.label || "").split(/\s*[/、]\s*/).forEach((part) => {
  6418. const v = part.trim();
  6419. if (v && !labels.includes(v)) labels.push(v);
  6420. });
  6421. });
  6422. });
  6423. const numbered = labels.filter((x) => /V\d/i.test(x));
  6424. const rest = labels.filter((x) => !/V\d/i.test(x));
  6425. return numbered.concat(rest);
  6426. }
  6427. function tankGroupHtml(whId, stock) {
  6428. const chip = (id) => `<span class="chip ghost">${id}</span>`;
  6429. const all = whId === "h2so4";
  6430. const groups = [];
  6431. if (all || whId === "h2so4-98") {
  6432. groups.push(["98%工业硫酸库", ["V0701A", "V0701B", "V0701C", "V0701D", "V070E", "V0701F"]]);
  6433. }
  6434. if (all || whId === "h2so4-sul") {
  6435. groups.push(["硫磺酸库", ["V0702C"]]);
  6436. }
  6437. if (all || whId === "h2so4-rg") {
  6438. groups.push(["试剂硫酸库", ["V0702A", "V0702B", "V0705B"]]);
  6439. }
  6440. if (all || whId === "h2so4-30") {
  6441. groups.push(["30%工业硫酸库", ["V0705A"]]);
  6442. }
  6443. if (all || whId === "h2so4-50") {
  6444. groups.push(["50%工业硫酸库", ["V0704"]]);
  6445. groups.push(["50%试剂酸", ["V0301A", "V0301B"]]);
  6446. }
  6447. if (whId === "hcl" || whId === "hcl-31-ind") {
  6448. groups.push(["31% 工业盐酸库", ["V0501", "V0502"]]);
  6449. }
  6450. if (whId === "hcl" || whId === "hcl-31") {
  6451. groups.push(["31% 合成盐酸库", ["V0601", "V0602"]]);
  6452. }
  6453. if (whId === "hcl" || whId === "hcl-36") {
  6454. groups.push(["36% 分析纯盐酸库", ["V0901", "V0902"]]);
  6455. }
  6456. if (whId === "naoh" || whId === "naoh-32") {
  6457. groups.push(["32%液碱库", ["V0801", "V0802", "V0803"]]);
  6458. }
  6459. if (all === false && (whId === "pack" || String(whId).startsWith("pack"))) {
  6460. groups.push(["分装品号", ["S-98-25PL", "S-98-5PL", "CP-97-25PL", "CP-97-5PL", "H-31-25PL", "AH-36-25PL", "C-10-25PL", "C-13-25PL"]]);
  6461. }
  6462. if (whId === "solid" || String(whId).startsWith("solid-")) {
  6463. const solidBins = [];
  6464. if (whId === "solid" || whId === "solid-jz") solidBins.push("片碱库(君正)");
  6465. if (whId === "solid" || whId === "solid-hsf") solidBins.push("片碱库(红四方)");
  6466. if (whId === "solid" || whId === "solid-soda") solidBins.push("纯碱库(红四方)");
  6467. groups.push(["固体分仓", solidBins]);
  6468. }
  6469. if (!groups.length) {
  6470. const flat = tankChipList(stock);
  6471. return flat.length ? `<div class="wh-tank">${flat.map(chip).join("")}</div>` : "";
  6472. }
  6473. return groups.map(([lab, ids]) => `
  6474. <div class="tank-group">
  6475. <div class="tg-lab">${lab}</div>
  6476. <div class="wh-tank">${ids.map(chip).join("")}</div>
  6477. </div>`).join("");
  6478. }
  6479. function ensureStock(whId, product) {
  6480. let stock = matchStock(whId, product);
  6481. const compact = String(product || "").replace(/\s/g, "");
  6482. const conc = parseConc(compact);
  6483. const grade = acidGradeOf(compact);
  6484. if (stock && stock.grade && conc != null && Number(stock.conc) !== conc && Math.abs(Number(stock.conc) - conc) >= 0.6) {
  6485. stock = null;
  6486. }
  6487. if (stock) return stock;
  6488. const name = acidProductName(grade, conc == null ? 98 : conc, { wh: whId });
  6489. stock = {
  6490. wh: whId, name, grade, conc: conc == null ? 98 : conc, qty: 0, cost: 0, unit: "吨",
  6491. loc: assignTankLoc(whId, grade), tankCap: 200
  6492. };
  6493. WH_STOCK.push(stock);
  6494. return stock;
  6495. }
  6496. function whMeta(id) {
  6497. for (const w of WH_TREE) {
  6498. if (w.id === id) return w;
  6499. const c = w.children.find((x) => x.id === id);
  6500. if (c) return { ...c, parent: w.name, dot: w.dot };
  6501. }
  6502. return { name: id, cap: 0, unit: "吨" };
  6503. }
  6504. function stockOf(ids) {
  6505. return WH_STOCK.filter((s) => ids.includes(s.wh));
  6506. }
  6507. function ledgerOf(ids) {
  6508. return WH_LEDGER.filter((r) => ids.includes(r.wh));
  6509. }
  6510. function fmtT(n) {
  6511. return Number(n).toLocaleString("zh-CN", { maximumFractionDigits: 1 });
  6512. }
  6513. function fmtY(n) {
  6514. return Number(n).toLocaleString("zh-CN", { maximumFractionDigits: 0 });
  6515. }
  6516. function whLineLabel(line) {
  6517. if (line === "industrial") return `<span class="grade ind">工业酸</span>`;
  6518. if (line === "reagent") return `<span class="grade rg">试剂酸</span>`;
  6519. if (line === "both") return `<span class="grade ind">工业酸</span> <span class="grade rg">试剂酸</span>`;
  6520. return "";
  6521. }
  6522. function whKindLabel(kind) {
  6523. if (kind === "raw") return `<span class="grade raw">原料库</span>`;
  6524. if (kind === "product") return `<span class="grade fin">成品库</span>`;
  6525. return `<span class="grade fin">仓库</span>`;
  6526. }
  6527. function whOpsHtml(ops) {
  6528. return (ops || []).map((o) => `<span class="op-chip ${o.includes("出库") ? "out" : ""}">${o}</span>`).join("");
  6529. }
  6530. function parentWhOf(id) {
  6531. const node = WH_TREE.find((w) => w.id === id);
  6532. if (node) return node;
  6533. return WH_TREE.find((w) => w.children.some((c) => c.id === id));
  6534. }
  6535. function renderWhNav() {
  6536. const parent = parentWhOf(currentWh);
  6537. const tree = currentWhView === "proc" ? WH_TREE.filter((w) => WH_PROC_PARENTS.includes(w.id)) : WH_TREE;
  6538. const mains = tree.map((w) => {
  6539. const on = parent?.id === w.id;
  6540. return `
  6541. <button class="wh-nav-item ${w.dot} ${on ? "on" : ""}" type="button" onclick="selectWarehouse('${w.id}')">
  6542. <span class="wh-dot ${w.dot}"></span>${w.name}
  6543. </button>`;
  6544. }).join("");
  6545. let subs = "";
  6546. if (parent?.children?.length && (currentWhView !== "proc" || WH_PROC_PARENTS.includes(parent.id))) {
  6547. subs = `<div class="wh-nav-subs"><span class="wh-nav-subs-lab">子仓库</span>${parent.children.map((c) => `
  6548. <button class="wh-nav-child ${parent.dot} ${currentWh === c.id ? "on" : ""}" type="button" onclick="selectWarehouse('${c.id}')">
  6549. <span class="wh-dot ${parent.dot}"></span>${c.name}
  6550. </button>`).join("")}</div>`;
  6551. }
  6552. document.getElementById("whNav").innerHTML = `<div class="wh-nav-mains"><span class="wh-nav-lab">仓库</span>${mains}</div>${subs}`;
  6553. }
  6554. function renderWhKpis() {
  6555. const ids = whIdsOf(currentWh);
  6556. const stock = stockOf(ids);
  6557. const qty = stock.reduce((a, s) => a + s.qty, 0);
  6558. const cost = stock.reduce((a, s) => a + s.qty * s.cost, 0);
  6559. const cap = ids.reduce((a, id) => {
  6560. const m = whMeta(id);
  6561. return a + (m.children ? 0 : (m.cap || 0));
  6562. }, 0) || (whMeta(currentWh).cap || 0);
  6563. const use = cap ? Math.min(100, Math.round(qty / cap * 100)) : 0;
  6564. const inQty = ledgerOf(ids).reduce((a, r) => a + r.inQty, 0);
  6565. const outQty = ledgerOf(ids).reduce((a, r) => a + r.outQty, 0);
  6566. const low = stock.filter((s) => s.qty < 80);
  6567. document.getElementById("whKpis").innerHTML = `
  6568. <div class="wh-kpi"><div class="k">结存数量</div><div class="v">${fmtT(qty)}</div><div class="s">${whMeta(currentWh).unit || "吨"} · ${stock.length} 个品名</div></div>
  6569. <div class="wh-kpi"><div class="k">结存成本</div><div class="v">${fmtY(cost)}</div><div class="s">元 · 加权平均</div></div>
  6570. <div class="wh-kpi"><div class="k">库容占用</div><div class="v">${use}%</div><div class="s">${fmtT(qty)} / ${fmtT(cap)} 吨</div><div class="wh-cap"><i class="${use >= 85 ? "warn" : ""}" style="width:${use}%"></i></div></div>
  6571. <div class="wh-kpi"><div class="k">本期入 / 出</div><div class="v">${fmtT(inQty)}<span style="font-size:14px;color:var(--muted);font-weight:500"> / ${fmtT(outQty)}</span></div><div class="s">${low.length ? low.map((s) => s.name + "偏低").join(" · ") : "库存水位正常"}</div></div>`;
  6572. }
  6573. function fmtN(n, d) {
  6574. return Number(n).toLocaleString("zh-CN", { minimumFractionDigits: d, maximumFractionDigits: d });
  6575. }
  6576. function ledgerView(r) {
  6577. const inQty = Number(r.inQty || 0);
  6578. const outQty = Number(r.outQty || 0);
  6579. const inCost = Number(r.inCost || 0);
  6580. const outCost = Number(r.outCost || 0);
  6581. let docType = r.docType || r.doc;
  6582. if (r.doc === "形态转换单") docType = outQty ? "其他出库单" : "其他入库单";
  6583. if (r.doc === "组装单") docType = "其他入库单";
  6584. const bizMap = {
  6585. "销货单": "普通销售", "进货单": "采购入库", "材料出库单": "生产领料",
  6586. "其他出库单": "组装", "其他入库单": "组装", "形态转换单": "组装", "组装单": "组装",
  6587. "产成品入库单": "完工入库", "盘点单": "盘点"
  6588. };
  6589. return {
  6590. whName: whMeta(r.wh).name || whNameOf(r.wh),
  6591. product: r.product,
  6592. unit: r.unit || "吨",
  6593. no: r.no,
  6594. date: r.date,
  6595. postDate: r.postDate || r.date,
  6596. docType,
  6597. bizType: r.bizType || bizMap[r.doc] || bizMap[docType] || "",
  6598. partner: r.partner || "",
  6599. inQty, inCost, outQty, outCost,
  6600. inPrice: inQty ? inCost / inQty : 0,
  6601. outPrice: outQty ? outCost / outQty : 0,
  6602. balQty: Number(r.balQty || 0),
  6603. price: Number(r.price || 0),
  6604. balCost: Number(r.balCost || 0)
  6605. };
  6606. }
  6607. function renderWhTable() {
  6608. const ids = whIdsOf(currentWh);
  6609. const q = (document.getElementById("whQ").value || "").trim().toLowerCase();
  6610. const docSel = document.getElementById("whDocFilter").value;
  6611. let rows = ledgerOf(ids);
  6612. if (currentWhDoc && currentWhDoc !== "库存成本明细账") {
  6613. rows = rows.filter((r) => r.doc === currentWhDoc);
  6614. }
  6615. if (docSel) rows = rows.filter((r) => r.doc === docSel);
  6616. if (q) rows = rows.filter((r) => (r.no + r.product + r.doc + (r.partner || "") + (r.note || "")).toLowerCase().includes(q));
  6617. document.getElementById("whTableTitle").textContent = currentWhDoc === "库存成本明细账"
  6618. ? "库存成本明细账 · " + whNameOf(currentWh)
  6619. : currentWhDoc + " · " + whNameOf(currentWh);
  6620. document.getElementById("whTableCount").textContent = rows.length + " 条";
  6621. if (currentWhDoc === "库存成本明细账") {
  6622. const views = rows.map(ledgerView);
  6623. const sumInQty = views.reduce((a, r) => a + r.inQty, 0);
  6624. const sumInAmt = views.reduce((a, r) => a + r.inCost, 0);
  6625. const sumOutQty = views.reduce((a, r) => a + r.outQty, 0);
  6626. const sumOutAmt = views.reduce((a, r) => a + r.outCost, 0);
  6627. const dash = `<td class="dash">—</td>`;
  6628. const qtyCell = (n, cls) => n ? `<td class="${cls}">${fmtT(n)}</td>` : dash;
  6629. const priceCell = (n, show) => show ? `<td>${fmtN(n, 2)}</td>` : dash;
  6630. const amtCell = (n, show) => show ? `<td class="amt">${fmtN(n, 2)}</td>` : dash;
  6631. document.getElementById("whTable").innerHTML = views.length ? `
  6632. <table class="proc-table wide">
  6633. <thead>
  6634. <tr>
  6635. <th>序号</th>
  6636. <th class="col-name">仓库名称</th>
  6637. <th class="col-name">物料名称</th>
  6638. <th>单号</th>
  6639. <th>日期</th>
  6640. <th>单据</th>
  6641. <th>业务</th>
  6642. <th>往来单位</th>
  6643. <th>入库量</th>
  6644. <th>入库单价</th>
  6645. <th>入库金额</th>
  6646. <th>出库量</th>
  6647. <th>出库单价</th>
  6648. <th>出库金额</th>
  6649. <th>结存量</th>
  6650. <th>结存单价</th>
  6651. <th>结存金额</th>
  6652. </tr>
  6653. </thead>
  6654. <tbody>${views.map((r, i) => `<tr>
  6655. <td>${i + 1}</td>
  6656. <td class="col-name">${r.whName}</td>
  6657. <td class="col-name"><b>${r.product}</b></td>
  6658. <td class="cell-no">${r.no}</td>
  6659. <td>${r.date}</td>
  6660. <td>${r.docType}</td>
  6661. <td>${r.bizType || "—"}</td>
  6662. <td>${r.partner || "—"}</td>
  6663. ${qtyCell(r.inQty, "qty-in")}
  6664. ${priceCell(r.inPrice, r.inQty)}
  6665. ${amtCell(r.inCost, r.inQty)}
  6666. ${qtyCell(r.outQty, "qty-out")}
  6667. ${priceCell(r.outPrice, r.outQty)}
  6668. ${amtCell(r.outCost, r.outQty)}
  6669. <td class="qty-bal">${fmtT(r.balQty)}</td>
  6670. <td>${fmtN(r.price, 2)}</td>
  6671. <td class="amt">${fmtN(r.balCost, 2)}</td>
  6672. </tr>`).join("")}</tbody>
  6673. <tfoot>
  6674. <tr>
  6675. <td colspan="8">合计</td>
  6676. <td class="qty-in">${fmtT(sumInQty)}</td>
  6677. <td class="dash">—</td>
  6678. <td class="amt">${fmtN(sumInAmt, 2)}</td>
  6679. <td class="qty-out">${fmtT(sumOutQty)}</td>
  6680. <td class="dash">—</td>
  6681. <td class="amt">${fmtN(sumOutAmt, 2)}</td>
  6682. <td class="dash">—</td>
  6683. <td class="dash">—</td>
  6684. <td class="dash">—</td>
  6685. </tr>
  6686. </tfoot>
  6687. </table>` : `<div class="empty">没有符合条件的明细。</div>`;
  6688. } else {
  6689. document.getElementById("whTable").innerHTML = rows.length ? `
  6690. <table class="proc-table">
  6691. <thead><tr><th>日期</th><th>单号</th><th>仓库</th><th>品名</th><th>入库</th><th>出库</th><th>金额</th><th>说明</th></tr></thead>
  6692. <tbody>${rows.map((r) => `<tr>
  6693. <td>${r.date}</td><td class="cell-no">${r.no}</td>
  6694. <td class="wh-path">${whNameOf(r.wh)}</td><td>${r.product}</td>
  6695. <td class="num">${r.inQty ? fmtT(r.inQty) : "—"}</td>
  6696. <td class="num">${r.outQty ? fmtT(r.outQty) : "—"}</td>
  6697. <td class="num">${fmtY(r.inCost || r.outCost)}</td>
  6698. <td class="muted">${r.note || "—"}</td>
  6699. </tr>`).join("")}</tbody>
  6700. </table>` : `<div class="empty">当前仓库没有「${currentWhDoc}」。</div>`;
  6701. }
  6702. }
  6703. function selectWarehouse(id) {
  6704. currentWh = id;
  6705. renderWarehouse();
  6706. }
  6707. function showWhView(view) {
  6708. currentWhView = view || "info";
  6709. document.querySelectorAll("#whSubnav button").forEach((b) => b.classList.toggle("on", b.dataset.view === currentWhView));
  6710. document.getElementById("view-wh-info").classList.toggle("hidden", currentWhView !== "info");
  6711. document.getElementById("view-wh-mode").classList.toggle("hidden", currentWhView !== "mode");
  6712. document.getElementById("view-wh-proc").classList.toggle("hidden", currentWhView !== "proc");
  6713. document.getElementById("view-wh-stock").classList.toggle("hidden", currentWhView !== "stock");
  6714. const titles = { info: "库存信息表", mode: "仓库信息表", proc: "加工单明细表", stock: "库存成本明细表" };
  6715. const descs = {
  6716. info: "查看当前仓库在库库存与储罐库存明细表。",
  6717. mode: "仓库档案与作业说明:出入库、稀释加工、精制加工。工业硫酸与试剂硫酸分线,不得混兑。",
  6718. proc: "仓库内加水 / 形态转换形成的加工明细。现场作业请用运输计划下发的任务单。",
  6719. stock: "按仓库查看库存成本明细账。稀释加工走形态转换:原料酸出库、成品入库。"
  6720. };
  6721. document.getElementById("whPageTitle").textContent = titles[currentWhView] || "库存信息表";
  6722. document.getElementById("whPageDesc").textContent = descs[currentWhView] || descs.info;
  6723. const acts = document.getElementById("whPageActions");
  6724. if (currentWhView === "stock") {
  6725. acts.innerHTML = `<button class="btn" type="button" onclick="whSelectDoc('形态转换单')">形态转换</button>
  6726. <button class="btn" type="button" onclick="whSelectDoc('盘点单')">盘点</button>`;
  6727. } else if (currentWhView === "mode") {
  6728. acts.innerHTML = "";
  6729. } else {
  6730. acts.innerHTML = `<button class="btn" type="button" onclick="showWhView('stock')">查看库存</button>`;
  6731. }
  6732. setCrumb("仓储管理", titles[currentWhView] || "库存信息表");
  6733. markNav(`[data-wh-view="${currentWhView}"]`);
  6734. document.querySelector('[data-group="warehouse"]')?.classList.add("open");
  6735. if (currentWhView === "proc") {
  6736. const p = parentWhOf(currentWh);
  6737. if (!WH_PROC_PARENTS.includes(p?.id)) {
  6738. currentWh = "h2so4";
  6739. renderWhNav();
  6740. renderWhProc();
  6741. } else {
  6742. renderWhNav();
  6743. }
  6744. } else {
  6745. renderWhNav();
  6746. }
  6747. }
  6748. function subWhDirHtml() {
  6749. const node = WH_TREE.find((w) => w.id === currentWh);
  6750. const children = node?.children || [];
  6751. if (!children.length) return "";
  6752. return `
  6753. <article class="card">
  6754. <div class="card-h"><h2>子库档案</h2><span class="muted">${children.length} 个仓</span></div>
  6755. <div class="card-b" style="overflow:auto">
  6756. <table class="wh-dir">
  6757. <thead><tr><th>仓库编码</th><th>名称</th><th>类型</th><th>产线</th><th>允许作业</th><th>结存</th><th>仓管</th></tr></thead>
  6758. <tbody>${children.map((c) => {
  6759. const qty = stockOf([c.id]).reduce((a, s) => a + s.qty, 0);
  6760. return `<tr class="row-link" onclick="selectWarehouse('${c.id}')">
  6761. <td class="cell-no">${c.code}</td>
  6762. <td><b>${c.name}</b></td>
  6763. <td>${whKindLabel(c.kind)}</td>
  6764. <td>${whLineLabel(c.line)}</td>
  6765. <td>${whOpsHtml(c.ops)}</td>
  6766. <td class="num">${fmtT(qty)}</td>
  6767. <td>${c.manager}</td>
  6768. </tr>`;
  6769. }).join("")}</tbody>
  6770. </table>
  6771. </div>
  6772. </article>`;
  6773. }
  6774. const INFO_STOCK_ITEMS = [
  6775. { wh: "h2so4-98", name: "98%工业硫酸", tone: "tone-ind" },
  6776. { wh: "h2so4-sul", name: "98%硫磺酸", tone: "tone-ind" },
  6777. { wh: "h2so4-rg", name: "试剂硫酸", tone: "tone-rg" },
  6778. { wh: "h2so4-50", name: "50%工业硫酸", tone: "tone-ind" },
  6779. { wh: "h2so4-30", name: "30%工业硫酸", tone: "tone-ind" },
  6780. { wh: "hcl-31", name: "31%合成盐酸", tone: "tone-hcl" }
  6781. ];
  6782. function inStockCardHtml() {
  6783. const cards = INFO_STOCK_ITEMS.map((it) => {
  6784. const s = WH_STOCK.find((x) => x.wh === it.wh && x.name === it.name);
  6785. if (!s) return "";
  6786. const chips = stockTankRows(s).map((t) => `<span>${t.label}</span>`).join("");
  6787. return `
  6788. <article class="wh-mini ${it.tone}" onclick="selectWarehouse('${s.wh}')">
  6789. <div class="wh-mini-top">
  6790. <div class="wh-mini-name"><i></i><b>${s.name}</b></div>
  6791. <div class="qty">${fmtT(s.qty)}<span>${s.unit}</span></div>
  6792. </div>
  6793. <div class="tanks">${chips}</div>
  6794. </article>`;
  6795. }).join("");
  6796. return `<div class="wh-stock-cards">${cards}</div>`;
  6797. }
  6798. function renderWhInfo() {
  6799. const ids = whIdsOf(currentWh);
  6800. const html = `${inStockCardHtml()}${tankTableHtml(ids)}`;
  6801. document.getElementById("view-wh-info").innerHTML = html || `<article class="card"><div class="card-b"><div class="empty">该仓暂无库存明细。</div></div></article>`;
  6802. }
  6803. function renderWhProc() {
  6804. document.getElementById("view-wh-proc").innerHTML = processListHtml() + convertListHtml();
  6805. }
  6806. function renderWhMode() {
  6807. const isAcid = currentWh === "h2so4" || String(currentWh).startsWith("h2so4-");
  6808. const structHtml = isAcid ? `
  6809. <div class="wh-tree-box">
  6810. <div class="wh-branch">
  6811. <h4>原料库 · 可出库,也可加工</h4>
  6812. <button type="button" onclick="selectWarehouse('h2so4-98')"><span>98%工业硫酸库</span><span class="grade ind">工业酸</span></button>
  6813. <button type="button" onclick="selectWarehouse('h2so4-sul')"><span>硫磺酸库</span><span class="grade ind">工业酸</span></button>
  6814. <button type="button" onclick="selectWarehouse('h2so4-rg')"><span>试剂硫酸库</span><span class="grade rg">试剂酸</span></button>
  6815. </div>
  6816. <div class="wh-branch">
  6817. <h4>成品库 · 加水浓度</h4>
  6818. <button type="button" onclick="selectWarehouse('h2so4-30')"><span>30%工业硫酸库</span><span class="muted">V0705A</span></button>
  6819. <button type="button" onclick="selectWarehouse('h2so4-50')"><span>50%工业硫酸库</span><span class="muted">V0704</span></button>
  6820. <button type="button" onclick="selectWarehouse('h2so4-custom')"><span>定制浓度库</span><span class="muted">其他浓度</span></button>
  6821. </div>
  6822. </div>` : "";
  6823. document.getElementById("view-wh-mode").innerHTML = `
  6824. ${subWhDirHtml()}
  6825. ${isAcid ? `
  6826. <article class="card">
  6827. <div class="card-h"><h2>出入库 · 稀释加工 · 精制加工</h2><span class="grade raw">工业 / 试剂分线,不得混兑</span></div>
  6828. <div class="card-b">
  6829. <div class="wh-mode" style="margin-bottom:12px">
  6830. <h3>原料酸 · 98% 工业硫酸</h3>
  6831. <p>存放于 98%工业硫酸库。可以仓库出库直销;也可以稀释加工成 30%、50% 工业硫酸或其他定制浓度。工业酸不得与试剂酸混用。</p>
  6832. <div class="wh-proc">
  6833. <div class="box"><b>98%工业硫酸</b><span>98%工业硫酸库 · 可直销出库</span></div>
  6834. <div class="arr">→</div>
  6835. <div class="box"><b>稀释加工</b><span>硫酸质量守恒</span></div>
  6836. <div class="arr">→</div>
  6837. <div class="box"><b>30% / 50% / 定制</b><span>工业硫酸成品库</span></div>
  6838. </div>
  6839. </div>
  6840. <div class="wh-mode">
  6841. <h3>原料酸 · 试剂硫酸</h3>
  6842. <p>存放于试剂硫酸库。可以仓库出库直销;也可以稀释加工成 30%、50% 试剂酸,或其他定制浓度。试剂酸线独立储罐、独立转换,不与工业酸互兑。</p>
  6843. <div class="wh-proc">
  6844. <div class="box"><b>试剂硫酸</b><span>试剂硫酸库 · 可直销出库</span></div>
  6845. <div class="arr">→</div>
  6846. <div class="box"><b>稀释加工</b><span>浓度按订单定制</span></div>
  6847. <div class="arr">→</div>
  6848. <div class="box"><b>30% / 50% / 定制</b><span>试剂酸成品库</span></div>
  6849. </div>
  6850. </div>
  6851. ${structHtml}
  6852. </div>
  6853. </article>` : ""}`;
  6854. }
  6855. function tankTableHtml(ids) {
  6856. const stocks = stockOf(ids).filter((s) => s.tanks || s.grade || s.sku || s.supplier || s.wh === "naoh" || s.wh === "naoh-32" || String(s.wh).startsWith("hcl") || s.wh === "pack" || String(s.wh).startsWith("solid"));
  6857. if (!stocks.length) return "";
  6858. const rows = [];
  6859. stocks.forEach((s) => {
  6860. stockTankRows(s).forEach((t) => rows.push({ s, t }));
  6861. });
  6862. return `<article class="card">
  6863. <div class="card-h"><h2>${ids.some((id) => id === "pack" || String(id).startsWith("solid")) ? "仓位 / 品号明细" : "储罐库存明细表"}</h2><span class="muted">${rows.length} 条</span></div>
  6864. <div class="card-b" style="overflow:auto">
  6865. <table>
  6866. <thead><tr><th>储罐位号</th><th>所属仓库</th><th>产线</th><th>储存品名</th><th>设计容积</th><th>现存量</th><th>占用</th><th>可收</th><th>状态</th></tr></thead>
  6867. <tbody>${rows.map(({ s, t }) => {
  6868. const cap = t.cap || s.tankCap || 80;
  6869. const use = cap ? Math.min(100, Math.round(t.qty / cap * 100)) : 0;
  6870. const room = Math.max(0, cap - t.qty);
  6871. const st = use >= 90 ? "将满" : t.qty < 15 ? "偏低" : "正常";
  6872. const cls = use >= 90 || t.qty < 15 ? "st-due" : "st-run";
  6873. return `<tr>
  6874. <td class="cell-no">${s.sku || t.label}</td>
  6875. <td class="wh-path">${whNameOf(s.wh)}</td>
  6876. <td>${gradeHtml(s) || (s.supplier ? `<span class="grade fin">${s.supplier}</span>` : "—")}</td>
  6877. <td class="wh-stock-name">${s.name}${s.packSpec ? " · " + s.packSpec : ""}${s.supplier ? "(" + s.supplier + ")" : ""}</td>
  6878. <td class="num">${fmtT(cap)} 吨</td>
  6879. <td class="num">${fmtT(t.qty)} 吨</td>
  6880. <td><div class="tank-use"><span class="bar"><i class="${use >= 90 ? "warn" : ""}" style="width:${use}%"></i></span>${use}%</div></td>
  6881. <td class="num">${fmtT(room)}</td>
  6882. <td><span class="st ${cls}">${st}</span></td>
  6883. </tr>`;
  6884. }).join("")}</tbody>
  6885. </table>
  6886. </div>
  6887. </article>`;
  6888. }
  6889. function processListHtml() {
  6890. const ids = whIdsOf(currentWh);
  6891. const docs = WH_PROCESS.filter((d) => ids.includes(d.rawWh) || ids.includes(d.toWh));
  6892. const sum = (key) => docs.reduce((a, d) => a + Number(d[key] || 0), 0);
  6893. const finSum = docs.reduce((a, d) => a + Number(d.finIn != null ? d.finIn : d.outQty || 0), 0);
  6894. const waterSum = docs.reduce((a, d) => a + Number(d.mode === "convert" ? 0 : d.water || 0), 0);
  6895. const body = docs.length ? docs.map((d, i) => {
  6896. const kind = d.mode === "convert" ? "形态转换" : "稀释加工";
  6897. return `<tr>
  6898. <td>${String(i + 1).padStart(2, "0")}</td>
  6899. <td>${d.date}</td>
  6900. <td class="cell-no">${d.no}</td>
  6901. <td>${kind}</td>
  6902. <td class="cell-no">${d.planNo || "—"}</td>
  6903. <td class="col-name">${whNameOf(d.rawWh)}</td>
  6904. <td>${d.rawName}</td>
  6905. <td>${fmtT(d.rawQty)}</td>
  6906. <td>${d.mode === "convert" ? "—" : fmtT(d.water || 0)}</td>
  6907. <td class="col-name">${whNameOf(d.toWh)}</td>
  6908. <td>${d.product}</td>
  6909. <td>${fmtT(d.outQty)}</td>
  6910. <td>${fmtT(d.finIn != null ? d.finIn : d.outQty)}</td>
  6911. </tr>`;
  6912. }).join("") : `<tr><td colspan="13" class="muted">暂无加工单。</td></tr>`;
  6913. const foot = docs.length ? `<tfoot><tr>
  6914. <td colspan="7">合计</td>
  6915. <td>${fmtT(sum("rawQty"))}</td>
  6916. <td>${fmtT(waterSum)}</td>
  6917. <td colspan="2"></td>
  6918. <td>${fmtT(sum("outQty"))}</td>
  6919. <td>${fmtT(finSum)}</td>
  6920. </tr></tfoot>` : "";
  6921. return `<article class="card">
  6922. <div class="card-h"><h2>加工单明细表</h2><span class="muted">${docs.length} 张</span></div>
  6923. <div class="card-b" style="overflow:auto">
  6924. <table class="proc-table wide">
  6925. <thead><tr>
  6926. <th>序号</th>
  6927. <th>加工日期</th>
  6928. <th>加工单号</th>
  6929. <th>类型</th>
  6930. <th>来源计划</th>
  6931. <th>投料仓库</th>
  6932. <th>投料品名</th>
  6933. <th>投料量(t)</th>
  6934. <th>加水量(t)</th>
  6935. <th>产出仓库</th>
  6936. <th>产出品名</th>
  6937. <th>产出数量(t)</th>
  6938. <th>产成品入库(t)</th>
  6939. </tr></thead>
  6940. <tbody>${body}</tbody>
  6941. ${foot}
  6942. </table>
  6943. </div>
  6944. </article>`;
  6945. }
  6946. function convertListHtml() {
  6947. const ids = whIdsOf(currentWh);
  6948. const rows = WH_LEDGER.filter((r) => r.doc === "形态转换单" && ids.includes(r.wh));
  6949. const body = rows.length ? rows.map((r, i) => `<tr>
  6950. <td>${String(i + 1).padStart(2, "0")}</td>
  6951. <td>${r.date}</td>
  6952. <td class="cell-no">${r.no}</td>
  6953. <td class="col-name">${whNameOf(r.wh)}</td>
  6954. <td>${r.product}</td>
  6955. <td>${r.inQty ? fmtT(r.inQty) : "—"}</td>
  6956. <td>${r.outQty ? fmtT(r.outQty) : "—"}</td>
  6957. <td>${r.note || "—"}</td>
  6958. </tr>`).join("") : `<tr><td colspan="8" class="muted">暂无形态转换。</td></tr>`;
  6959. return `<article class="card">
  6960. <div class="card-h"><h2>形态转换明细</h2><span class="muted">${rows.length} 笔</span></div>
  6961. <div class="card-b" style="overflow:auto">
  6962. <table class="proc-table wide">
  6963. <thead><tr>
  6964. <th>序号</th>
  6965. <th>日期</th>
  6966. <th>转换单号</th>
  6967. <th>仓库名称</th>
  6968. <th>物料名称</th>
  6969. <th>入库(t)</th>
  6970. <th>出库(t)</th>
  6971. <th>说明</th>
  6972. </tr></thead>
  6973. <tbody>${body}</tbody>
  6974. </table>
  6975. </div>
  6976. </article>`;
  6977. }
  6978. function acidTargetWh(pct) {
  6979. if (pct === 30) return "h2so4-30";
  6980. if (pct === 50) return "h2so4-50";
  6981. return "h2so4-custom";
  6982. }
  6983. function acidProductName(grade, pct, raw) {
  6984. if (raw && raw.wh === "h2so4-sul" && pct === 98) return "98%硫磺酸";
  6985. if (grade === "reagent") return pct === 98 ? "试剂硫酸" : pct + "%试剂酸";
  6986. return pct === 98 ? "98%工业硫酸" : pct + "%工业硫酸";
  6987. }
  6988. function acidRaws() {
  6989. return WH_STOCK.filter((s) => s.wh === "h2so4-98" || s.wh === "h2so4-sul" || s.wh === "h2so4-rg");
  6990. }
  6991. function fillAcidTargets(raw, keep) {
  6992. const sel = document.getElementById("acidForm").target;
  6993. sel.innerHTML = `<option value="30">加水 · 30%</option>
  6994. <option value="50">加水 · 50%</option>
  6995. <option value="custom">加水 · 其他定制浓度</option>`;
  6996. if (keep && [...sel.options].some((o) => o.value === keep)) sel.value = keep;
  6997. else sel.value = "50";
  6998. }
  6999. function openAcidDlg() {
  7000. const form = document.getElementById("acidForm");
  7001. const raws = acidRaws();
  7002. const prefer = currentWh === "h2so4-rg" ? "h2so4-rg" : currentWh === "h2so4-sul" ? "h2so4-sul" : "h2so4-98";
  7003. form.rawId.innerHTML = raws.map((s) =>
  7004. `<option value="${s.wh}|${s.name}" ${s.wh === prefer ? "selected" : ""}>${s.name} · 结存 ${fmtT(s.qty)} 吨</option>`
  7005. ).join("");
  7006. form.outQty.value = "20";
  7007. form.customPct.value = "";
  7008. onAcidRawChange();
  7009. document.getElementById("acidDlg").showModal();
  7010. }
  7011. function closeAcidDlg() { document.getElementById("acidDlg").close(); }
  7012. function onAcidRawChange() {
  7013. const form = document.getElementById("acidForm");
  7014. const [wh, name] = (form.rawId.value || "").split("|");
  7015. const raw = WH_STOCK.find((s) => s.wh === wh && s.name === name);
  7016. fillAcidTargets(raw, form.target.value);
  7017. onAcidTarget();
  7018. }
  7019. function onAcidTarget() {
  7020. const custom = document.getElementById("acidForm").target.value === "custom";
  7021. document.getElementById("acidCustomRow").style.display = custom ? "" : "none";
  7022. updateAcidCalc();
  7023. }
  7024. function acidCalc() {
  7025. const form = document.getElementById("acidForm");
  7026. const [wh, name] = (form.rawId.value || "").split("|");
  7027. const raw = WH_STOCK.find((s) => s.wh === wh && s.name === name);
  7028. if (!raw) return null;
  7029. const tgt = form.target.value;
  7030. const outQty = Number(form.outQty.value);
  7031. if (!outQty || outQty <= 0) return { raw, error: "请填写成品数量" };
  7032. const pct = tgt === "custom" ? Number(form.customPct.value) : Number(tgt);
  7033. if (!pct || pct <= 0 || pct >= raw.conc) return { raw, error: "请填写有效目标浓度(须低于原料浓度)" };
  7034. const rawQty = +(outQty * pct / raw.conc).toFixed(3);
  7035. const water = +(outQty - rawQty).toFixed(3);
  7036. return {
  7037. raw, mode: "dilute", pct, outQty, rawQty, water, loss: 0,
  7038. toWh: acidTargetWh(pct), product: acidProductName(raw.grade, pct, raw), grade: raw.grade,
  7039. loc: assignTankLoc(acidTargetWh(pct), raw.grade),
  7040. unitCost: +(raw.cost * raw.conc / pct).toFixed(2)
  7041. };
  7042. }
  7043. function updateAcidCalc() {
  7044. const c = acidCalc();
  7045. const box = document.getElementById("acidCalc");
  7046. if (!c) { box.textContent = "请选择原料酸。"; return; }
  7047. if (c.error) { box.textContent = c.error; return; }
  7048. box.innerHTML = `加水稀释:消耗 <b>${c.raw.name} ${fmtT(c.rawQty)} 吨</b>(库存 ${fmtT(c.raw.qty)})+ 工艺水 <b>${fmtT(c.water)} 吨</b>
  7049. → 得到 <b>${c.product} ${fmtT(c.outQty)} 吨</b>,入 <b>${whNameOf(c.toWh)}</b>。产线(工业 / 试剂)保持不变。`;
  7050. }
  7051. function saveAcidConvert() {
  7052. const c = acidCalc();
  7053. if (!c || c.error) { toast(c?.error || "请完整填写"); return; }
  7054. if (c.rawQty > c.raw.qty + 1e-9) { toast(`原料酸不足,仅剩 ${fmtT(c.raw.qty)} 吨`); return; }
  7055. c.raw.qty = +(c.raw.qty - c.rawQty).toFixed(3);
  7056. let fin = WH_STOCK.find((s) => s.wh === c.toWh && s.name === c.product);
  7057. if (!fin) {
  7058. fin = { wh: c.toWh, name: c.product, grade: c.grade, conc: c.pct, qty: 0, cost: c.unitCost, unit: "吨", loc: c.loc };
  7059. WH_STOCK.push(fin);
  7060. }
  7061. fin.qty = +(fin.qty + c.outQty).toFixed(3);
  7062. const no = "ZX-" + today.replace(/-/g, "") + "-" + String(++whDocSeq).padStart(3, "0");
  7063. const cost = c.rawQty * c.raw.cost;
  7064. const noteOut = `稀释加工成 ${c.product} ${fmtT(c.outQty)} 吨`;
  7065. const noteIn = `由 ${c.raw.name} 加水 / 加工`;
  7066. WH_LEDGER.unshift({
  7067. date: today, doc: "形态转换单", no, wh: c.raw.wh, product: c.raw.name,
  7068. inQty: 0, inCost: 0, outQty: c.rawQty, outCost: cost,
  7069. balQty: c.raw.qty, price: c.raw.cost, balCost: c.raw.qty * c.raw.cost,
  7070. note: noteOut
  7071. });
  7072. WH_LEDGER.unshift({
  7073. date: today, doc: "形态转换单", no, wh: c.toWh, product: c.product,
  7074. inQty: c.outQty, inCost: cost, outQty: 0, outCost: 0,
  7075. balQty: fin.qty, price: fin.cost, balCost: fin.qty * fin.cost,
  7076. note: noteIn
  7077. });
  7078. WH_PROCESS.unshift({
  7079. no, date: today, batch: "TL-" + today.replace(/-/g, "") + "-" + String(whDocSeq).padStart(3, "0"),
  7080. mode: "dilute", rawName: c.raw.name, rawWh: c.raw.wh, rawQty: c.rawQty, rawConc: c.raw.conc,
  7081. water: c.water, loss: c.loss || 0, product: c.product, toWh: c.toWh, outQty: c.outQty,
  7082. finIn: c.outQty, diluteBal: fin.qty, grade: c.grade, loc: c.loc
  7083. });
  7084. closeAcidDlg();
  7085. toast(`已转换:${c.raw.name} ${fmtT(c.rawQty)} 吨 → ${c.product} ${fmtT(c.outQty)} 吨`);
  7086. renderWarehouse();
  7087. }
  7088. function whSelectDoc(doc) {
  7089. if (doc === "要货单") {
  7090. toast("要货单未启用,不进入库存成本明细账");
  7091. return;
  7092. }
  7093. currentWhDoc = doc;
  7094. const sel = document.getElementById("whDocFilter");
  7095. sel.value = doc === "库存成本明细账" ? "" : doc;
  7096. renderWhTable();
  7097. }
  7098. function onWhDocFilter() {
  7099. const v = document.getElementById("whDocFilter").value;
  7100. currentWhDoc = v || "库存成本明细账";
  7101. renderWhTable();
  7102. }
  7103. function renderWarehouse() {
  7104. renderWhNav();
  7105. renderWhInfo();
  7106. renderWhMode();
  7107. renderWhProc();
  7108. renderWhKpis();
  7109. renderWhTable();
  7110. showWhView(currentWhView);
  7111. }
  7112. function setCrumb(parent, child) {
  7113. document.getElementById("crumbMod").textContent = parent;
  7114. const tail = document.getElementById("crumbTail");
  7115. if (!child || child === parent) {
  7116. tail.classList.add("hidden");
  7117. } else {
  7118. tail.classList.remove("hidden");
  7119. document.getElementById("crumb").textContent = child;
  7120. }
  7121. }
  7122. function markNav(sel) {
  7123. document.querySelectorAll(".side-item").forEach((b) => b.classList.remove("active"));
  7124. const el = document.querySelector(sel);
  7125. if (!el) return;
  7126. el.classList.add("active");
  7127. el.closest(".nav-group")?.classList.add("open");
  7128. }
  7129. function hideAllMods() {
  7130. ["mod-placeholder", "mod-product", "mod-quote", "mod-contract", "mod-fleet", "mod-plan", "mod-cert", "mod-expense", "mod-duty", "mod-staff", "mod-warehouse", "mod-archive", "mod-task"].forEach((id) => {
  7131. document.getElementById(id).classList.add("hidden");
  7132. });
  7133. }
  7134. function showPage(page) {
  7135. const meta = PLACE_PAGES[page];
  7136. if (!meta) return;
  7137. currentMod = page;
  7138. hideAllMods();
  7139. document.getElementById("mod-placeholder").classList.remove("hidden");
  7140. document.getElementById("phTitle").textContent = meta.title;
  7141. document.getElementById("phDesc").textContent = meta.desc;
  7142. document.getElementById("phCardTitle").textContent = meta.title;
  7143. document.getElementById("phComing").textContent = meta.coming || "当前演示重点在运输计划与购买证管理。";
  7144. setCrumb(meta.parent, meta.parent === meta.title ? "" : meta.title);
  7145. markNav(`[data-page="${page}"]`);
  7146. }
  7147. function showMod(mod) {
  7148. currentMod = mod;
  7149. hideAllMods();
  7150. const box = document.getElementById("mod-" + mod);
  7151. if (box) box.classList.remove("hidden");
  7152. markNav(`[data-mod="${mod}"]`);
  7153. if (mod === "fleet") {
  7154. showView(currentFleetView || "etc");
  7155. return;
  7156. }
  7157. if (mod === "product") {
  7158. setCrumb("CRM管理", "产品管理");
  7159. renderProducts();
  7160. }
  7161. if (mod === "quote") {
  7162. setCrumb("CRM管理", "报价单管理");
  7163. renderQuotes();
  7164. }
  7165. if (mod === "contract") {
  7166. setCrumb("CRM管理", "合同管理");
  7167. renderContracts();
  7168. }
  7169. if (mod === "plan") {
  7170. setCrumb("运输管理", "运输计划");
  7171. renderPlans();
  7172. }
  7173. if (mod === "cert") {
  7174. setCrumb("运输管理", "购买证管理");
  7175. showCertView(currentCertId ? "detail" : "wait");
  7176. }
  7177. if (mod === "expense") {
  7178. setCrumb("人事管理", "报销管理");
  7179. renderExpense();
  7180. }
  7181. if (mod === "duty") {
  7182. setCrumb("人事管理", "值班管理");
  7183. renderDuty();
  7184. }
  7185. if (mod === "staff") {
  7186. setCrumb("人事管理", "员工档案");
  7187. renderStaff();
  7188. }
  7189. if (mod === "warehouse") {
  7190. renderWarehouse();
  7191. return;
  7192. }
  7193. if (mod === "archive") {
  7194. setCrumb("运输管理", "运输归档");
  7195. renderArchive();
  7196. }
  7197. if (mod === "task") {
  7198. setCrumb("运输管理", "任务单管理");
  7199. renderTasks();
  7200. }
  7201. }
  7202. function daysBetween(a, b) {
  7203. return Math.round((new Date(b) - new Date(a)) / 86400000);
  7204. }
  7205. function expMoney(n) {
  7206. return "¥" + Number(n || 0).toLocaleString("zh-CN", { minimumFractionDigits: 2 });
  7207. }
  7208. function expFinalRole(x) {
  7209. return "总经理";
  7210. }
  7211. function expOverdue(x) {
  7212. if (!x.date || ["已归档", "已驳回"].includes(x.status)) return false;
  7213. return daysBetween(x.date, today) > 45;
  7214. }
  7215. function expBudgetLeft(dept) {
  7216. const b = EXP_BUDGET[dept] || { total: 0, used: 0 };
  7217. return b.total - b.used;
  7218. }
  7219. function expOpenLoan(user) {
  7220. return expLoans.find((l) => l.user === user && l.status === "未清" && l.remain > 0);
  7221. }
  7222. function expStatusPill(st) {
  7223. const map = {
  7224. "草稿": "st-stop", "已驳回": "st-due", "待财务初审": "st-fix", "待批准": "st-fix",
  7225. "待终审": "st-fix", "待付款": "st-run", "待归档": "st-run", "已归档": "st-stop",
  7226. "待批准": "st-fix", "已批准": "st-run"
  7227. };
  7228. const label = st === "待终审" ? "待领导审核" : st;
  7229. return `<span class="st ${map[st] || "st-stop"}">${label}</span>`;
  7230. }
  7231. function expLog(x, who, act, note) {
  7232. x.logs = x.logs || [];
  7233. const hh = new Date().getHours().toString().padStart(2, "0");
  7234. const mm = new Date().getMinutes().toString().padStart(2, "0");
  7235. x.logs.push({ t: today + " " + hh + ":" + mm, who, act, note: note || "" });
  7236. }
  7237. function expById(id) { return expenses.find((x) => x.id === id); }
  7238. function kindTag(k) {
  7239. const sq = k === "备用金" || k === "冲备用金";
  7240. return `<span class="kind-tag ${sq ? "sq" : "bx"}">${k}</span>`;
  7241. }
  7242. function prePending(p) { return p.status === "待财务初审" || p.status === "待批准"; }
  7243. function renderExpense() {
  7244. const flowEl = document.getElementById("expFlow");
  7245. flowEl.classList.remove("hidden");
  7246. const bxDraft = expenses.filter((x) => x.status === "草稿" || x.status === "已驳回").length;
  7247. const bxFin = expenses.filter((x) => x.status === "待财务初审").length;
  7248. const bxLead = expenses.filter((x) => x.status === "待终审").length;
  7249. const bxPay = expenses.filter((x) => x.status === "待付款").length;
  7250. const bxArch = expenses.filter((x) => x.status === "待归档").length;
  7251. const sqFin = preExps.filter(prePending).length;
  7252. const sqOk = preExps.filter((p) => p.status === "已批准").length;
  7253. const sqMine = preExps.length;
  7254. const bxOn = (key) => {
  7255. if (key === "我的报销") return expTab === "我的报销";
  7256. if (key === "待财务初审") return expTab === "待财务初审" && expFinKind !== "备用金";
  7257. return expTab === key;
  7258. };
  7259. const sqOn = (key) => {
  7260. if (key === "申请") return expTab === "备用金申请";
  7261. if (key === "待财务初审") return expTab === "待财务初审" && expFinKind !== "报销";
  7262. if (key === "冲销") return expTab === "借款";
  7263. return false;
  7264. };
  7265. const expCell = (it, last) => {
  7266. if (it.ghost) {
  7267. return `<div class="exp-cell ghost${last ? " last" : ""}"><span class="t"><b>${it.name}</b><em>${it.hint}</em></span></div>`;
  7268. }
  7269. return `<button class="exp-cell ${it.on ? "on" : ""}${last ? " last" : ""}" type="button" onclick="${it.click}">
  7270. <span class="t"><b>${it.name}</b><em>${it.hint}</em></span>
  7271. <span class="c">${it.n}</span>
  7272. </button>`;
  7273. };
  7274. const bxCells = [
  7275. { hint: "贴发票填报", name: "经办填报", n: bxDraft, on: bxOn("我的报销"), click: "setExpTab('我的报销')" },
  7276. { hint: "核发票预算", name: "财务核票", n: bxFin, on: bxOn("待财务初审"), click: "setExpTab('待财务初审','报销')" },
  7277. { hint: "总经理审", name: "领导审核", n: bxLead, on: bxOn("待终审"), click: "setExpTab('待终审')" },
  7278. { hint: "打款或冲销", name: "出纳付款", n: bxPay, on: bxOn("待付款"), click: "setExpTab('待付款')" },
  7279. { hint: "凭证装订", name: "制单归档", n: bxArch, on: bxOn("待归档"), click: "setExpTab('待归档')" }
  7280. ].map((it, i, a) => expCell(it, i === a.length - 1)).join("");
  7281. const sqCells = [
  7282. { hint: "无票先借", name: "提出申请", n: sqMine, on: sqOn("申请"), click: "setExpTab('备用金申请')" },
  7283. { hint: "核事由额度", name: "财务核额", n: sqFin, on: sqOn("待财务初审"), click: "setExpTab('待财务初审','备用金')" },
  7284. { hint: "通过后可支", name: "批准待支", n: sqOk, on: expTab === "备用金申请" && !sqFin, click: "setExpTab('备用金申请')" },
  7285. { hint: "未清余额", name: "报销冲销", n: expLoans.filter((l) => l.status === "未清").length, on: sqOn("冲销"), click: "setExpTab('借款')" },
  7286. { ghost: true, name: "汇入报销", hint: "贴票后走上线" }
  7287. ].map((it, i, a) => expCell(it, i === a.length - 1)).join("");
  7288. flowEl.innerHTML = `
  7289. <div class="exp-map">
  7290. <div class="exp-map-grid">
  7291. <div class="exp-map-head">
  7292. <div></div>
  7293. <div class="exp-colh"><b>1</b>发起</div>
  7294. <div class="exp-colh"><b>2</b>财务初审</div>
  7295. <div class="exp-colh"><b>3</b>审用</div>
  7296. <div class="exp-colh"><b>4</b>资金</div>
  7297. <div class="exp-colh"><b>5</b>收口</div>
  7298. </div>
  7299. <div class="exp-map-row bx">
  7300. <div class="exp-map-lab"><b>报销</b><span>事后凭票</span></div>
  7301. ${bxCells}
  7302. </div>
  7303. <div class="exp-map-row sq">
  7304. <div class="exp-map-lab"><b>备用金</b><span>事前借款</span></div>
  7305. ${sqCells}
  7306. </div>
  7307. </div>
  7308. </div>`;
  7309. const overdue = expenses.filter(expOverdue);
  7310. const rejectN = expenses.filter((x) => x.status === "已驳回").length;
  7311. const uncleared = expLoans.filter((l) => l.status === "未清").length;
  7312. document.getElementById("expAlerts").innerHTML = `
  7313. <div class="alert ${overdue.length ? "red" : "ok"}">逾期未结 ${overdue.length} 笔(业务结束超 45 天)</div>
  7314. <div class="alert ${rejectN ? "red" : "ok"}">待补资料重提 ${rejectN} 笔</div>
  7315. <div class="alert ${uncleared ? "" : "ok"}">未清借款 ${uncleared} 笔 · 前款不清后款不借</div>`;
  7316. const hints = {
  7317. "全部": "报销凭发票事后结算;备用金是借款核额后先支出事后凭票报销冲销。",
  7318. "我的报销": "经办人工作台:贴发票填报。若已有备用金,结算选冲销,勿再走一笔新付款。",
  7319. "备用金申请": "事前申请,财务初审通过后方可支出,事后必须凭票报销冲销。",
  7320. "待财务初审": "报销看发票,通过后去领导审核;备用金只核额度,通过后即可支出。",
  7321. "待终审": "仅报销单。总经理可同意或写明原因驳回。备用金不经领导审核。",
  7322. "待付款": "仅报销单。审批通过后出纳对公支付或打个人卡;有备用金/借款则冲销。",
  7323. "待归档": "仅报销单。会计录入凭证,报销单、发票、附件装订归档。",
  7324. "借款": "备用金/借款台账。报销时选择冲销;未结清不得再借。"
  7325. };
  7326. document.getElementById("expHint").textContent = hints[expTab] || "";
  7327. const bar = document.getElementById("expToolbar");
  7328. if (expTab === "备用金申请") {
  7329. bar.innerHTML = `<button class="btn btn-primary" type="button" onclick="openPreExpDlg()">+ 备用金申请</button>
  7330. <input id="expQ" type="search" placeholder="申请号 / 申请人 / 事项" oninput="renderExpense()" />`;
  7331. renderPreExpTable();
  7332. return;
  7333. }
  7334. if (expTab === "借款") {
  7335. bar.innerHTML = `<span class="muted">报销结算选「冲销借款」后在此核销余额</span>
  7336. <input id="expQ" type="search" placeholder="借款号 / 经办人" oninput="renderExpense()" />`;
  7337. renderLoanTable();
  7338. return;
  7339. }
  7340. if (expTab === "待财务初审") {
  7341. const nBx = expenses.filter((x) => x.status === "待财务初审").length;
  7342. const nSq = preExps.filter(prePending).length;
  7343. bar.innerHTML = `
  7344. <div class="plan-tabs" id="expFinTabs">
  7345. <button class="${!expFinKind ? "on" : ""}" type="button" onclick="setExpTab('待财务初审','')">全部 ${nBx + nSq}</button>
  7346. <button class="${expFinKind === "报销" ? "on" : ""}" type="button" onclick="setExpTab('待财务初审','报销')">报销 ${nBx}</button>
  7347. <button class="${expFinKind === "备用金" ? "on" : ""}" type="button" onclick="setExpTab('待财务初审','备用金')">备用金 ${nSq}</button>
  7348. </div>
  7349. <span class="split"></span>
  7350. <input id="expQ" type="search" placeholder="单号 / 经办人 / 事由" oninput="renderFinanceReview()" />`;
  7351. renderFinanceReview();
  7352. return;
  7353. }
  7354. bar.innerHTML = `
  7355. <button class="btn btn-primary" type="button" onclick="openExpenseDlg()">+ 填报报销单</button>
  7356. <button class="btn" type="button" onclick="openPreExpDlg()">备用金申请</button>
  7357. <span class="split"></span>
  7358. <input id="expQ" type="search" placeholder="单号 / 经办人 / 部门 / 事由" oninput="renderExpenseList()" />
  7359. <select id="expSt" onchange="renderExpenseList()">
  7360. <option value="">全部状态</option>
  7361. <option>草稿</option><option>待财务初审</option>
  7362. <option value="待终审">待领导审核</option><option>待付款</option><option>待归档</option>
  7363. <option>已归档</option><option>已驳回</option>
  7364. </select>`;
  7365. renderExpenseList();
  7366. }
  7367. function setExpTab(tab, kind) {
  7368. if (tab === "pre") tab = "备用金申请";
  7369. expTab = tab;
  7370. expFinKind = tab === "待财务初审" ? (kind || "") : "";
  7371. document.querySelectorAll("#expTabs button").forEach((b) => b.classList.toggle("on", b.dataset.tab === tab));
  7372. renderExpense();
  7373. }
  7374. function renderExpenseList() {
  7375. const qEl = document.getElementById("expQ");
  7376. const stEl = document.getElementById("expSt");
  7377. const q = (qEl?.value || "").trim();
  7378. const st = stEl?.value || "";
  7379. let rows = expenses.slice();
  7380. if (expTab === "我的报销") rows = rows.filter((x) => ["草稿", "已驳回"].includes(x.status) || x.user === "周建军");
  7381. else if (["待财务初审", "待终审", "待付款", "待归档"].includes(expTab)) rows = rows.filter((x) => x.status === expTab);
  7382. if (st) rows = rows.filter((x) => x.status === st);
  7383. if (q) rows = rows.filter((x) => [x.no, x.user, x.dept, x.type, x.remark, x.payWay].join(" ").includes(q));
  7384. document.getElementById("expCount").textContent = `共 ${rows.length} 条`;
  7385. document.getElementById("expTable").innerHTML = rows.length ? `<table class="erp-table" style="min-width:1180px">
  7386. <thead><tr><th>单号</th><th>经办人</th><th>部门</th><th>类别</th><th>金额</th><th>发生日</th><th>票据</th><th>结算</th><th>终审角色</th><th>状态</th></tr></thead>
  7387. <tbody>${rows.map((x) => `<tr>
  7388. <td class="cell-no row-link" onclick="openExpDetail(${x.id})">${kindTag("报销")}${x.preNo ? kindTag("冲备用金") : ""}${x.no}${expOverdue(x) ? '<span class="need-tag">逾期</span>' : ""}${x.overBudget ? '<span class="need-tag bao">超预算</span>' : ""}</td>
  7389. <td>${x.user}</td><td>${x.dept}</td><td>${x.type}</td>
  7390. <td class="num">${expMoney(x.amount)}</td><td>${x.date}</td>
  7391. <td>${(x.invoices || []).length} 张</td>
  7392. <td>${x.payWay}${x.loanNo ? `<div class="muted">${x.loanNo}</div>` : ""}</td>
  7393. <td>${expFinalRole(x)}</td>
  7394. <td>${expStatusPill(x.status)}</td>
  7395. </tr>`).join("")}</tbody></table>` : `<div class="empty">本节点暂无单据</div>`;
  7396. }
  7397. function expRowActs(x) {
  7398. const d = `<button class="btn btn-sm" type="button" onclick="openExpDetail(${x.id})">详情</button>`;
  7399. if (x.status === "草稿" || x.status === "已驳回") return `${d}<button class="btn btn-sm btn-primary" type="button" onclick="expSubmit(${x.id})">提交</button>`;
  7400. if (x.status === "待财务初审") return `${d}<button class="btn btn-sm btn-primary" type="button" onclick="expAdvance(${x.id})">初审通过</button><button class="btn btn-sm" type="button" onclick="openExpReject(${x.id})">退回</button>`;
  7401. if (x.status === "待终审") return `${d}<button class="btn btn-sm btn-primary" type="button" onclick="expAdvance(${x.id})">同意</button><button class="btn btn-sm" type="button" onclick="openExpReject(${x.id})">驳回</button>`;
  7402. if (x.status === "待付款") return `${d}<button class="btn btn-sm btn-primary" type="button" onclick="expAdvance(${x.id})">${x.payWay === "冲销借款" ? "冲借款" : "付款"}</button>`;
  7403. if (x.status === "待归档") return `${d}<button class="btn btn-sm btn-primary" type="button" onclick="expAdvance(${x.id})">制单归档</button>`;
  7404. return d;
  7405. }
  7406. function renderFinanceReview() {
  7407. const q = (document.getElementById("expQ")?.value || "").trim();
  7408. const bx = expenses.filter((x) => x.status === "待财务初审").map((x) => ({
  7409. kind: "报销", no: x.no, user: x.user, dept: x.dept, matter: x.type, amount: x.amount, date: x.date,
  7410. extra: (x.invoices || []).length + " 张发票" + (x.preNo ? " · 冲 " + x.preNo : ""), id: x.id
  7411. }));
  7412. const sq = preExps.filter(prePending).map((p) => ({
  7413. kind: "备用金", no: p.no, user: p.user, dept: p.dept, matter: p.type, amount: p.amount, date: p.date,
  7414. extra: p.remark, id: p.no
  7415. }));
  7416. const match = (r) => !q || [r.no, r.user, r.dept, r.matter, r.extra].join(" ").includes(q);
  7417. const bxRows = bx.filter(match);
  7418. const sqRows = sq.filter(match);
  7419. const showBx = expFinKind !== "备用金";
  7420. const showSq = expFinKind !== "报销";
  7421. const nBx = showBx ? bxRows.length : 0;
  7422. const nSq = showSq ? sqRows.length : 0;
  7423. const finTable = (kind, rows) => {
  7424. if (!rows.length) return `<div class="empty">${kind}暂无待初审</div>`;
  7425. const bx = kind === "报销";
  7426. return `<table class="erp-table" style="min-width:1080px">
  7427. <thead><tr><th>单号</th><th>经办人</th><th>部门</th><th>事项</th><th>金额</th><th>日期</th><th>初审看什么</th><th>通过后</th></tr></thead>
  7428. <tbody>${rows.map((r) => `<tr>
  7429. <td class="cell-no${bx ? " row-link" : ""}"${bx ? ` onclick="openExpDetail(${r.id})"` : ""}>${kindTag(r.kind)}${r.no}</td>
  7430. <td>${r.user}</td><td>${r.dept}</td><td>${r.matter}</td>
  7431. <td class="num">${expMoney(r.amount)}</td><td>${r.date}</td>
  7432. <td>${bx ? "发票真伪、附件、预算" : "事由与额度(无发票)"}<div class="muted">${r.extra || ""}</div></td>
  7433. <td>${bx ? "进入领导审核" : "批准先支出,事后凭票报销冲销"}</td>
  7434. </tr>`).join("")}</tbody></table>`;
  7435. };
  7436. document.getElementById("expCount").textContent = `待初审 ${nBx + nSq} 条(报销 ${nBx} · 备用金 ${nSq})`;
  7437. document.getElementById("expTable").innerHTML = `
  7438. ${showBx ? `<div class="fin-sec">${finTable("报销", bxRows)}</div>` : ""}
  7439. ${showSq ? `<div class="fin-sec">${finTable("备用金", sqRows)}</div>` : ""}`;
  7440. }
  7441. function renderPreExpTable() {
  7442. const q = (document.getElementById("expQ")?.value || "").trim();
  7443. const rows = preExps.filter((p) => !q || [p.no, p.user, p.dept, p.type, p.remark].join(" ").includes(q));
  7444. document.getElementById("expCount").textContent = `共 ${rows.length} 条备用金申请`;
  7445. document.getElementById("expTable").innerHTML = `<table class="erp-table" style="min-width:880px">
  7446. <thead><tr><th>申请号</th><th>申请人</th><th>部门</th><th>事项</th><th>预估金额</th><th>日期</th><th>状态</th><th>事由</th></tr></thead>
  7447. <tbody>${rows.map((p) => `<tr>
  7448. <td class="cell-no">${kindTag("备用金")}${p.no}</td><td>${p.user}</td><td>${p.dept}</td><td>${p.type}</td>
  7449. <td>${expMoney(p.amount)}</td><td>${p.date}</td><td>${expStatusPill(p.status)}</td><td>${p.remark}</td>
  7450. </tr>`).join("")}</tbody></table>`;
  7451. }
  7452. function renderLoanTable() {
  7453. const q = (document.getElementById("expQ")?.value || "").trim();
  7454. const rows = expLoans.filter((l) => !q || [l.no, l.user, l.dept].join(" ").includes(q));
  7455. document.getElementById("expCount").textContent = `共 ${rows.length} 笔借款`;
  7456. document.getElementById("expTable").innerHTML = `<table class="erp-table" style="min-width:880px">
  7457. <thead><tr><th>借款号</th><th>经办人</th><th>部门</th><th>借款额</th><th>未冲余额</th><th>日期</th><th>状态</th></tr></thead>
  7458. <tbody>${rows.map((l) => `<tr>
  7459. <td class="cell-no">${l.no}</td><td>${l.user}</td><td>${l.dept}</td>
  7460. <td>${expMoney(l.amount)}</td><td>${expMoney(l.remain)}</td><td>${l.date}</td>
  7461. <td>${l.status === "未清" ? '<span class="st st-fix">未清</span>' : '<span class="st st-run">已清</span>'}</td>
  7462. </tr>`).join("")}</tbody></table>`;
  7463. }
  7464. function previewExpFiles(input) {
  7465. expFiles = [...(input.files || [])].map((f) => f.name);
  7466. document.getElementById("expFileChips").innerHTML = expFiles.map((n) => `<span class="chip">${n}</span>`).join("") || "";
  7467. }
  7468. function expStaffDept(name) {
  7469. return EXP_STAFF.find((s) => s.name === name)?.dept || "行政人事";
  7470. }
  7471. function syncExpTitle() {
  7472. const f = document.getElementById("expenseForm");
  7473. if (!f) return;
  7474. const user = f.user.value;
  7475. const d = (f.date.value || today).replaceAll("-", "");
  7476. f.title.value = `${user}报销单${d}${String(expSeq).padStart(3, "0")}`;
  7477. }
  7478. function onExpUserChange() { syncExpTitle(); }
  7479. function expLineSum() {
  7480. return expLines.reduce((s, r) => s + Number(r.amount || 0), 0);
  7481. }
  7482. function renderExpLines() {
  7483. const body = document.getElementById("expLineBody");
  7484. const opts = `<option value="">请选择费用项目</option>` + EXP_TYPES.map((t) => `<option>${t}</option>`).join("");
  7485. body.innerHTML = expLines.map((r, i) => `<tr>
  7486. <td>${i + 1}</td>
  7487. <td><select class="${r.type ? "" : "placeholder"}" required onchange="expLines[${i}].type=this.value;this.classList.toggle('placeholder',!this.value)">${opts}</select></td>
  7488. <td><input type="number" min="0" step="0.01" value="${Number(r.amount || 0).toFixed(2)}" oninput="expLines[${i}].amount=this.value;updateExpSums()" /></td>
  7489. <td><input placeholder="请输入报销事由" value="${(r.reason || "").replaceAll('"', "&quot;")}" oninput="expLines[${i}].reason=this.value" /></td>
  7490. <td>
  7491. <div class="exp-datewrap ${r.date ? "has" : ""}">
  7492. <svg viewBox="0 0 24 24"><rect x="4" y="6" width="16" height="14" rx="2"/><path d="M8 4v4M16 4v4M4 11h16"/></svg>
  7493. <input type="date" value="${r.date || ""}" onchange="expLines[${i}].date=this.value;this.parentElement.classList.toggle('has',!!this.value)" aria-label="发生时间,可选" />
  7494. <span class="exp-date-ph">可选</span>
  7495. </div>
  7496. </td>
  7497. </tr>`).join("");
  7498. document.querySelectorAll("#expLineBody select").forEach((sel, i) => { sel.value = expLines[i].type || ""; sel.classList.toggle("placeholder", !sel.value); });
  7499. updateExpSums();
  7500. }
  7501. function addExpLine() {
  7502. expLines.push({ type: "", amount: 0, reason: "", date: "" });
  7503. renderExpLines();
  7504. }
  7505. function removeExpLine(i) {
  7506. expLines.splice(i, 1);
  7507. if (!expLines.length) expLines.push({ type: "", amount: 0, reason: "", date: "" });
  7508. renderExpLines();
  7509. }
  7510. function selectedInvoices() {
  7511. return invWallet.filter((x) => selectedInvIds.includes(x.id));
  7512. }
  7513. function updateExpSums() {
  7514. document.getElementById("expLineSum").textContent = expMoney(expLineSum());
  7515. const picked = selectedInvoices();
  7516. const used = picked.reduce((s, x) => s + x.amount, 0);
  7517. const bal = invWallet.reduce((s, x) => s + x.amount, 0) - used;
  7518. document.getElementById("expInvBal").textContent = picked.length ? expMoney(Math.max(0, bal)) : expMoney(0);
  7519. document.getElementById("expInvSum").textContent = picked.length ? expMoney(used) : expMoney(0);
  7520. document.getElementById("expInvChips").innerHTML = picked.map((x) =>
  7521. `<span class="chip">${x.name} ${expMoney(x.amount)}</span>`).join("");
  7522. }
  7523. function openInvPick() {
  7524. const box = document.getElementById("invPickList");
  7525. box.innerHTML = invWallet.map((x) => {
  7526. const on = selectedInvIds.includes(x.id);
  7527. return `<button class="inv-pick-item ${on ? "on" : ""}" type="button" data-id="${x.id}" onclick="toggleInvPick(${x.id})">
  7528. <input type="checkbox" ${on ? "checked" : ""} onclick="event.stopPropagation();toggleInvPick(${x.id})" />
  7529. <div><b>${x.name}</b><div class="muted">${x.date}</div></div>
  7530. <span class="amt">${expMoney(x.amount)}</span>
  7531. </button>`;
  7532. }).join("");
  7533. document.getElementById("invPickDialog").showModal();
  7534. }
  7535. function toggleInvPick(id) {
  7536. if (selectedInvIds.includes(id)) selectedInvIds = selectedInvIds.filter((x) => x !== id);
  7537. else selectedInvIds.push(id);
  7538. openInvPick();
  7539. }
  7540. function confirmInvPick() {
  7541. document.getElementById("invPickDialog").close();
  7542. updateExpSums();
  7543. }
  7544. function openExpenseDlg(preNo) {
  7545. const f = document.getElementById("expenseForm");
  7546. f.reset();
  7547. f.user.innerHTML = EXP_STAFF.map((s) => `<option value="${s.name}">${s.name}</option>`).join("");
  7548. f.user.value = "吴九华";
  7549. f.date.value = today;
  7550. expFiles = [];
  7551. selectedInvIds = [];
  7552. expLines = [{ type: "", amount: 0, reason: "", date: "" }];
  7553. document.getElementById("expFileChips").innerHTML = "";
  7554. if (preNo) {
  7555. const p = preExps.find((x) => x.no === preNo);
  7556. if (p) {
  7557. f.user.value = p.user;
  7558. f.remark.value = p.remark;
  7559. expLines = [{ type: p.type === "出差" ? "差旅费" : p.type === "招待" ? "招待费" : p.type === "采购" ? "采购费" : "其他", amount: p.amount, reason: p.remark, date: today }];
  7560. }
  7561. }
  7562. syncExpTitle();
  7563. renderExpLines();
  7564. document.getElementById("expenseDialog").showModal();
  7565. }
  7566. function closeExpenseDlg() { document.getElementById("expenseDialog").close(); }
  7567. function openPreExpDlg() {
  7568. document.getElementById("preExpForm").reset();
  7569. document.getElementById("preExpForm").user.value = "周建军";
  7570. document.getElementById("preExpDialog").showModal();
  7571. }
  7572. function savePreExp() {
  7573. const f = document.getElementById("preExpForm");
  7574. if (!f.reportValidity()) return;
  7575. const no = "SQ-" + today.replaceAll("-", "") + "-" + String(preSeq++).padStart(3, "0");
  7576. preExps.unshift({ no, user: f.user.value.trim(), dept: f.dept.value, type: f.type.value, amount: Number(f.amount.value), date: today, status: "待财务初审", remark: f.remark.value.trim() });
  7577. document.getElementById("preExpDialog").close();
  7578. expTab = "待财务初审";
  7579. expFinKind = "备用金";
  7580. updateBadges();
  7581. renderExpense();
  7582. toast("备用金已提交财务初审,通过后再支出");
  7583. }
  7584. function approvePreExp(no, ok) {
  7585. const p = preExps.find((x) => x.no === no);
  7586. if (!p) return;
  7587. p.status = ok ? "已批准" : "已驳回";
  7588. updateBadges();
  7589. renderExpense();
  7590. toast(ok ? "备用金财务初审通过,可以先支出,事后凭票报销冲销" : "备用金申请已驳回");
  7591. }
  7592. function saveExpense(submit) {
  7593. const f = document.getElementById("expenseForm");
  7594. if (!f.reportValidity()) return;
  7595. const lines = expLines.filter((r) => r.type || Number(r.amount) > 0 || (r.reason || "").trim());
  7596. if (!lines.length || lines.some((r) => !r.type || Number(r.amount) <= 0 || !(r.reason || "").trim())) {
  7597. toast("请完整填写费用项目、金额和报销事由");
  7598. return;
  7599. }
  7600. const user = f.user.value.trim();
  7601. const dept = expStaffDept(user);
  7602. const amt = expLineSum();
  7603. const type = lines[0].type;
  7604. const remark = (f.remark.value.trim() || lines.map((r) => r.reason).filter(Boolean).join(";"));
  7605. const invoices = selectedInvoices().map((x) => x.name).concat(expFiles);
  7606. const loan = expOpenLoan(user);
  7607. const payWay = loan ? "冲销借款" : "个人银行卡";
  7608. const loanNo = loan ? loan.no : "";
  7609. const over = amt > expBudgetLeft(dept);
  7610. const bizDate = lines.map((r) => r.date).filter(Boolean).sort()[0] || f.date.value;
  7611. const late = daysBetween(bizDate, today) > 45;
  7612. const no = "BX-" + (f.date.value || today).replaceAll("-", "") + "-" + String(expSeq++).padStart(3, "0");
  7613. const x = {
  7614. id: Date.now(), no, user, dept, type, amount: amt, title: f.title.value.trim(),
  7615. date: bizDate, submit: submit ? today : "", status: submit ? (late ? "已驳回" : "待财务初审") : "草稿",
  7616. remark, invoices, lines: lines.map((r) => ({ ...r, amount: Number(r.amount) })),
  7617. payWay, loanNo, bank: "", preNo: "", overBudget: over, logs: []
  7618. };
  7619. if (!submit) expLog(x, user, "保存草稿", f.title.value.trim());
  7620. else if (late) expLog(x, "系统", "逾期退回", "业务结束已超过 45 天,按制度不予受理");
  7621. else expLog(x, user, "提交报销", (x.invoices.length || 0) + " 张附件 / 发票");
  7622. expenses.unshift(x);
  7623. closeExpenseDlg();
  7624. expTab = submit && !late ? "待财务初审" : "我的报销";
  7625. updateBadges();
  7626. renderExpense();
  7627. toast(late ? "已超过报销时限,系统退回" : submit ? "已提交审批" : "已保存草稿");
  7628. }
  7629. function expSubmit(id) {
  7630. const x = expById(id);
  7631. if (!x) return;
  7632. if (!(x.invoices || []).length) { toast("请先在详情中确认已贴发票"); openExpDetail(id); return; }
  7633. if (expOverdue(x)) {
  7634. x.status = "已驳回";
  7635. expLog(x, "系统", "逾期退回", "业务结束已超过 45 天,按制度不予受理");
  7636. toast("逾期不予受理");
  7637. } else {
  7638. x.status = "待财务初审";
  7639. x.submit = today;
  7640. expLog(x, x.user, "提交报销", "补资料后重新发起");
  7641. toast("已提交财务初审");
  7642. }
  7643. updateBadges();
  7644. renderExpense();
  7645. }
  7646. function expAdvance(id) {
  7647. const x = expById(id);
  7648. if (!x) return;
  7649. if (x.status === "待财务初审") {
  7650. x.status = "待终审";
  7651. x.finalRole = expFinalRole(x);
  7652. expLog(x, "财务会计", "财务初审通过", x.overBudget ? "票据合规,超预算转总经理专项终审" : "发票、附件、制度与预算核验通过");
  7653. toast("财务初审通过,进入领导审核");
  7654. } else if (x.status === "待终审") {
  7655. x.status = "待付款";
  7656. expLog(x, expFinalRole(x), "终审同意", "");
  7657. toast("终审同意,请出纳付款");
  7658. } else if (x.status === "待付款") {
  7659. if (x.payWay === "冲销借款" && x.loanNo) {
  7660. const l = expLoans.find((i) => i.no === x.loanNo);
  7661. if (l) {
  7662. l.remain = Math.max(0, l.remain - x.amount);
  7663. if (l.remain === 0) l.status = "已清";
  7664. }
  7665. expLog(x, "出纳", "已冲借款", `冲销 ${x.loanNo}`);
  7666. toast("已冲销借款");
  7667. } else {
  7668. expLog(x, "出纳", "已付款", x.payWay + " " + expMoney(x.amount));
  7669. toast("出纳已付款");
  7670. }
  7671. x.status = "待归档";
  7672. } else if (x.status === "待归档") {
  7673. x.status = "已归档";
  7674. x.voucher = "记-" + today.replaceAll("-", "").slice(0, 6) + "-" + String(80 + (x.id % 20));
  7675. expLog(x, "会计", "制单归档", "凭证 " + x.voucher + ",发票装订归档");
  7676. toast("已制单归档");
  7677. }
  7678. updateBadges();
  7679. renderExpense();
  7680. const dlg = document.getElementById("expDetailDialog");
  7681. if (dlg.open) openExpDetail(id);
  7682. }
  7683. function openExpReject(id) {
  7684. expRejectId = id;
  7685. document.getElementById("expRejectForm").reset();
  7686. document.getElementById("expRejectDlg").showModal();
  7687. }
  7688. function confirmExpReject() {
  7689. const f = document.getElementById("expRejectForm");
  7690. if (!f.reportValidity()) return;
  7691. const x = expById(expRejectId);
  7692. if (!x) return;
  7693. const reason = f.reason.value.trim();
  7694. const who = x.status === "待财务初审" ? "财务会计" : x.status === "待终审" ? expFinalRole(x) : (x.dept + "负责人");
  7695. const act = x.status === "待财务初审" ? "退回补资料" : "驳回";
  7696. x.status = "已驳回";
  7697. expLog(x, who, act, reason);
  7698. document.getElementById("expRejectDlg").close();
  7699. document.getElementById("expDetailDialog").close();
  7700. updateBadges();
  7701. renderExpense();
  7702. toast("已退回经办人,请补资料后重提");
  7703. }
  7704. function openExpDetail(id) {
  7705. const x = expById(id);
  7706. if (!x) return;
  7707. document.getElementById("expDetailTitle").textContent = x.no + " · " + x.status;
  7708. const left = expBudgetLeft(x.dept);
  7709. document.getElementById("expDetailBody").innerHTML = `
  7710. <div class="split-h">
  7711. <div>
  7712. <h4>报销信息</h4>
  7713. <div class="kv">
  7714. ${kv("经办人", x.user)}${kv("部门", x.dept)}${kv("类别", x.type)}
  7715. ${kv("金额", expMoney(x.amount))}${kv("发生日", x.date)}${kv("提交日", x.submit || "—")}
  7716. ${kv("结算", x.payWay)}${kv("借款", x.loanNo || "无")}${kv("账户", x.bank || "—")}
  7717. ${kv("备用金申请", x.preNo || "无")}${kv("终审角色", expFinalRole(x))}
  7718. ${kv("预算剩余", expMoney(left) + (x.overBudget ? "(本单超预算)" : ""))}
  7719. ${kv("凭证", x.voucher || "—")}
  7720. </div>
  7721. <p style="margin-top:10px">${x.remark || ""}</p>
  7722. </div>
  7723. <div>
  7724. <h4>原始单据</h4>
  7725. <div class="chips">${(x.invoices || []).map((n) => `<span class="chip">${n}</span>`).join("") || `<span class="muted">尚未贴票</span>`}</div>
  7726. <h4 style="margin-top:16px">审批轨迹</h4>
  7727. <div class="exp-log">${(x.logs || []).map((l) => `<div class="row"><div class="muted">${l.t}</div><div>${l.who}</div><div><b>${l.act}</b><div class="muted">${l.note || ""}</div></div></div>`).join("") || `<div class="muted">暂无</div>`}</div>
  7728. </div>
  7729. </div>`;
  7730. document.getElementById("expDetailFoot").innerHTML = `
  7731. <button class="btn" type="button" onclick="document.getElementById('expDetailDialog').close()">关闭</button>
  7732. ${expRowActs(x).replace(`<button class="btn btn-sm" type="button" onclick="openExpDetail(${x.id})">详情</button>`, "")}`;
  7733. document.getElementById("expDetailDialog").showModal();
  7734. }
  7735. function dutyPad(n) { return String(n).padStart(2, "0"); }
  7736. function dutyDateStr(y, m, d) { return `${y}-${dutyPad(m)}-${dutyPad(d)}`; }
  7737. function dutyOfMonth() {
  7738. const prefix = `${dutyYM.y}-${dutyPad(dutyYM.m)}-`;
  7739. return dutyShifts.filter((s) => s.date.startsWith(prefix));
  7740. }
  7741. function dutyChipText(s) {
  7742. const names = (s.people || []).join("、");
  7743. const post = s.post ? ` · ${s.post}` : "";
  7744. return `${DUTY_TYPE[s.type] || s.type} ${names}${post}`;
  7745. }
  7746. function renderDuty() {
  7747. document.getElementById("dutyMonthLab").textContent = `${dutyYM.y}年${dutyYM.m}月`;
  7748. const monthRows = dutyOfMonth();
  7749. const countType = (t) => monthRows.filter((s) => s.type === t).reduce((n, s) => n + (s.people || []).length, 0);
  7750. const nAm = countType("am");
  7751. const nPm = countType("pm");
  7752. const nWatch = countType("watch");
  7753. const nOther = countType("other");
  7754. const nSum = nAm + nPm + nWatch + nOther;
  7755. document.getElementById("dutyPills").innerHTML = `
  7756. <span class="duty-pill"><i class="am"></i>早班 <b>${nAm}</b></span>
  7757. <span class="duty-pill"><i class="pm"></i>晚班 <b>${nPm}</b></span>
  7758. <span class="duty-pill"><i class="watch"></i>值班 <b>${nWatch}</b></span>
  7759. <span class="duty-sum">本月合计 <b>${nSum}</b></span>`;
  7760. const peopleMap = {};
  7761. monthRows.forEach((s) => (s.people || []).forEach((p) => { peopleMap[p] = (peopleMap[p] || 0) + 1; }));
  7762. const peopleOrder = [...DUTY_STAFF].filter((p) => peopleMap[p]).sort((a, b) => peopleMap[b] - peopleMap[a] || DUTY_STAFF.indexOf(a) - DUTY_STAFF.indexOf(b));
  7763. document.getElementById("dutyPeople").innerHTML = peopleOrder.length
  7764. ? `<span>本月人员</span>` + peopleOrder.map((p) => `<em>${p}</em> <b>${peopleMap[p]}</b>`).join("")
  7765. : `<span>本月人员</span><span>暂无</span>`;
  7766. const y = dutyYM.y, m = dutyYM.m;
  7767. const first = new Date(y, m - 1, 1);
  7768. const start = new Date(y, m - 1, 1 - first.getDay());
  7769. const last = new Date(y, m, 0);
  7770. const endPad = 6 - last.getDay();
  7771. const end = new Date(y, m - 1, last.getDate() + endPad);
  7772. const cells = [];
  7773. for (let t = start.getTime(); t <= end.getTime(); t += 86400000) {
  7774. const dt = new Date(t);
  7775. const yy = dt.getFullYear(), mm = dt.getMonth() + 1, dd = dt.getDate();
  7776. const key = dutyDateStr(yy, mm, dd);
  7777. const out = mm !== m;
  7778. const shifts = dutyShifts.filter((s) => s.date === key);
  7779. const chips = shifts.map((s) => `<span class="duty-chip ${s.type}">${dutyChipText(s)}</span>`).join("");
  7780. const empty = !out && !shifts.length ? `<span class="duty-empty">未排班,点击配置</span>` : "";
  7781. cells.push(`<button class="duty-day${out ? " out" : ""}${key === DUTY_TODAY ? " today" : ""}" type="button" onclick="openDutyDay('${key}')">
  7782. <div class="num">${dd}</div>${chips}${empty}
  7783. </button>`);
  7784. }
  7785. document.getElementById("dutyGrid").innerHTML = cells.join("");
  7786. }
  7787. function shiftDutyMonth(delta) {
  7788. dutyYM.m += delta;
  7789. if (dutyYM.m < 1) { dutyYM.m = 12; dutyYM.y -= 1; }
  7790. if (dutyYM.m > 12) { dutyYM.m = 1; dutyYM.y += 1; }
  7791. renderDuty();
  7792. }
  7793. function goDutyToday() {
  7794. const [y, m] = DUTY_TODAY.split("-").map(Number);
  7795. dutyYM = { y, m };
  7796. renderDuty();
  7797. }
  7798. function copyDutyLink() {
  7799. const url = location.href.split("#")[0] + "#/duty/schedule";
  7800. if (navigator.clipboard?.writeText) navigator.clipboard.writeText(url).catch(() => {});
  7801. toast("已复制值班排班链接");
  7802. }
  7803. function renderDutyStaffPick() {
  7804. document.getElementById("dutyStaffPick").innerHTML = DUTY_STAFF.map((n) =>
  7805. `<button class="chip pick${dutyPickPeople.includes(n) ? " on" : ""}" type="button" onclick="toggleDutyPerson('${n}')">${n}</button>`
  7806. ).join("");
  7807. }
  7808. function toggleDutyPerson(name) {
  7809. if (dutyPickPeople.includes(name)) dutyPickPeople = dutyPickPeople.filter((x) => x !== name);
  7810. else dutyPickPeople.push(name);
  7811. renderDutyStaffPick();
  7812. }
  7813. function renderDutyExist() {
  7814. const rows = dutyShifts.filter((s) => s.date === dutyPickDate);
  7815. const box = document.getElementById("dutyExist");
  7816. box.innerHTML = rows.length
  7817. ? `<div class="muted" style="margin-bottom:8px">当日已排</div><div class="duty-exist">${rows.map((s) =>
  7818. `<div class="row"><span class="duty-chip ${s.type}" style="width:auto;margin:0">${dutyChipText(s)}</span>
  7819. <button class="btn btn-sm" type="button" onclick="removeDutyShift(${s.id})">删除</button></div>`
  7820. ).join("")}</div>`
  7821. : `<div class="muted">当日尚未排班</div>`;
  7822. }
  7823. function openDutyDay(date) {
  7824. dutyPickDate = date;
  7825. dutyPickPeople = [];
  7826. const f = document.getElementById("dutyForm");
  7827. f.type.value = "am";
  7828. f.post.value = "配酸";
  7829. document.getElementById("dutyDlgTitle").textContent = date + " 值班配置";
  7830. renderDutyStaffPick();
  7831. renderDutyExist();
  7832. document.getElementById("dutyDialog").showModal();
  7833. }
  7834. function saveDutyShift() {
  7835. if (!dutyPickDate) return;
  7836. if (!dutyPickPeople.length) { toast("请选择人员"); return; }
  7837. const f = document.getElementById("dutyForm");
  7838. const type = f.type.value;
  7839. const post = (f.post.value || "").trim();
  7840. const hit = dutyShifts.find((s) => s.date === dutyPickDate && s.type === type);
  7841. if (hit) {
  7842. hit.people = [...dutyPickPeople];
  7843. hit.post = post;
  7844. } else {
  7845. dutyShifts.push({ id: ++dutySeq, date: dutyPickDate, type, people: [...dutyPickPeople], post });
  7846. }
  7847. dutyPickPeople = [];
  7848. renderDutyStaffPick();
  7849. renderDutyExist();
  7850. renderDuty();
  7851. toast("已保存排班");
  7852. }
  7853. function removeDutyShift(id) {
  7854. dutyShifts = dutyShifts.filter((s) => s.id !== id);
  7855. renderDutyExist();
  7856. renderDuty();
  7857. toast("已删除");
  7858. }
  7859. const STAFF_DEPTS = ["行政人事", "财务部", "销售部", "运输部", "仓储部", "生产部", "配酸车间"];
  7860. let staffTab = "全部";
  7861. let staffPage = 1;
  7862. let staffPageSize = 20;
  7863. let staffSeq = 2025030;
  7864. let selectedStaffIds = new Set();
  7865. let staffDetailId = null;
  7866. let staffs = [
  7867. { id: 1, no: "SX-2018001", name: "吴九华", gender: "女", dept: "行政人事", post: "人事主管", phone: "13805581201", hire: "2018-03-12", contractEnd: "2027-03-11", kind: "劳动合同", status: "在职", edu: "本科", native: "安徽安庆", emergency: "吴父", emergPhone: "13905580011", remark: "负责档案、考勤与值班排班。" },
  7868. { id: 2, no: "SX-2016002", name: "万金强", gender: "男", dept: "销售部", post: "销售负责人", phone: "13905582002", hire: "2016-05-08", contractEnd: "2027-05-07", kind: "劳动合同", status: "在职", edu: "本科", native: "安徽安庆", emergency: "万妻", emergPhone: "13905582012", remark: "客户合同与报价对接。" },
  7869. { id: 3, no: "SX-2017003", name: "周建军", gender: "男", dept: "运输部", post: "驾驶员", phone: "13705583003", hire: "2017-08-01", contractEnd: "2026-10-18", kind: "劳动合同", status: "在职", edu: "高中", native: "湖北宜昌", emergency: "周母", emergPhone: "13705583013", remark: "鄂E·危A123 主驾,危货从业资格在期。" },
  7870. { id: 4, no: "SX-2019004", name: "陈伟", gender: "男", dept: "运输部", post: "驾驶员", phone: "13705584004", hire: "2019-04-16", contractEnd: "2027-04-15", kind: "劳动合同", status: "在职", edu: "中专", native: "安徽桐城", emergency: "陈妻", emergPhone: "13705584014", remark: "鄂E·危B618 主驾。" },
  7871. { id: 5, no: "SX-2018005", name: "胡明", gender: "男", dept: "生产部", post: "生产班长", phone: "13605585005", hire: "2018-09-10", contractEnd: "2027-09-09", kind: "劳动合同", status: "在职", edu: "大专", native: "安徽怀宁", emergency: "胡父", emergPhone: "13605585015", remark: "配酸、分装现场负责人。" },
  7872. { id: 6, no: "SX-2019006", name: "吴涛", gender: "男", dept: "销售部", post: "业务员", phone: "13505586006", hire: "2019-11-02", contractEnd: "2027-11-01", kind: "劳动合同", status: "在职", edu: "大专", native: "安徽安庆", emergency: "吴兄", emergPhone: "13505586016", remark: "" },
  7873. { id: 7, no: "SX-2020007", name: "郑浩", gender: "男", dept: "仓储部", post: "仓管员", phone: "13405587007", hire: "2020-03-20", contractEnd: "2027-03-19", kind: "劳动合同", status: "在职", edu: "大专", native: "安徽潜山", emergency: "郑妻", emergPhone: "13405587017", remark: "硫酸库、液碱库。" },
  7874. { id: 8, no: "SX-2024008", name: "刘洋", gender: "男", dept: "仓储部", post: "仓管员", phone: "13305588008", hire: "2024-08-01", contractEnd: "2026-10-25", kind: "劳动合同", status: "试用", edu: "大专", native: "安徽太湖", emergency: "刘父", emergPhone: "13305588018", remark: "试用期至 2026-11-01。" },
  7875. { id: 9, no: "SX-2018009", name: "孙斌", gender: "男", dept: "运输部", post: "押运员", phone: "13205589009", hire: "2018-06-18", contractEnd: "2026-10-28", kind: "劳动合同", status: "在职", edu: "高中", native: "安徽宿松", emergency: "孙妻", emergPhone: "13205589019", remark: "危货押运证将到期,需复审。" },
  7876. { id: 10, no: "SX-2017010", name: "吴自国", gender: "男", dept: "配酸车间", post: "配酸工", phone: "13105580110", hire: "2017-02-14", contractEnd: "2028-02-13", kind: "劳动合同", status: "在职", edu: "高中", native: "安徽安庆", emergency: "吴妻", emergPhone: "13105580120", remark: "值班排班骨干。" },
  7877. { id: 11, no: "SX-2018011", name: "刘茂生", gender: "男", dept: "配酸车间", post: "配酸工", phone: "13105580111", hire: "2018-01-08", contractEnd: "2028-01-07", kind: "劳动合同", status: "在职", edu: "中专", native: "安徽安庆", emergency: "刘妻", emergPhone: "13105580121", remark: "" },
  7878. { id: 12, no: "SX-2019012", name: "吴自泰", gender: "男", dept: "配酸车间", post: "配酸工", phone: "13105580112", hire: "2019-05-06", contractEnd: "2027-05-05", kind: "劳动合同", status: "在职", edu: "高中", native: "安徽安庆", emergency: "吴父", emergPhone: "13105580122", remark: "" },
  7879. { id: 13, no: "SX-2020013", name: "黄天宝", gender: "男", dept: "配酸车间", post: "配酸工", phone: "13105580113", hire: "2020-07-01", contractEnd: "2027-06-30", kind: "劳动合同", status: "在职", edu: "中专", native: "安徽望江", emergency: "黄兄", emergPhone: "13105580123", remark: "" },
  7880. { id: 14, no: "SX-2023014", name: "刘丹", gender: "女", dept: "配酸车间", post: "值班员", phone: "15905580114", hire: "2023-04-10", contractEnd: "2027-04-09", kind: "劳动合同", status: "在职", edu: "大专", native: "安徽安庆", emergency: "刘母", emergPhone: "15905580124", remark: "周末值班。" },
  7881. { id: 15, no: "SX-2025015", name: "汪璇", gender: "女", dept: "行政人事", post: "人事文员", phone: "15805580115", hire: "2025-09-01", contractEnd: "2026-11-30", kind: "劳动合同", status: "试用", edu: "本科", native: "安徽合肥", emergency: "汪父", emergPhone: "15805580125", remark: "协助档案与考勤。" },
  7882. { id: 16, no: "SX-2022016", name: "吴良芹", gender: "女", dept: "配酸车间", post: "值班员", phone: "15705580116", hire: "2022-03-21", contractEnd: "2027-03-20", kind: "劳动合同", status: "在职", edu: "高中", native: "安徽安庆", emergency: "吴夫", emergPhone: "15705580126", remark: "" },
  7883. { id: 17, no: "SX-2016017", name: "李强", gender: "男", dept: "运输部", post: "押运员", phone: "15605580117", hire: "2016-11-11", contractEnd: "2027-11-10", kind: "劳动合同", status: "在职", edu: "高中", native: "湖北宜昌", emergency: "李妻", emergPhone: "15605580127", remark: "鄂E·危A123 押运。" },
  7884. { id: 18, no: "SX-2017018", name: "王磊", gender: "男", dept: "运输部", post: "押运员", phone: "15505580118", hire: "2017-09-01", contractEnd: "2027-08-31", kind: "劳动合同", status: "在职", edu: "中专", native: "安徽枞阳", emergency: "王父", emergPhone: "15505580128", remark: "" },
  7885. { id: 19, no: "SX-2018019", name: "陈静", gender: "女", dept: "财务部", post: "会计", phone: "13905580119", hire: "2018-01-15", contractEnd: "2027-01-14", kind: "劳动合同", status: "在职", edu: "本科", native: "安徽安庆", emergency: "陈母", emergPhone: "13905580129", remark: "报销初审、备用金核额。" },
  7886. { id: 20, no: "SX-2020020", name: "赵鹏", gender: "男", dept: "运输部", post: "押运员", phone: "13705580120", hire: "2020-06-08", contractEnd: "2027-06-07", kind: "劳动合同", status: "在职", edu: "高中", native: "安徽岳西", emergency: "赵妻", emergPhone: "13705580130", remark: "" },
  7887. { id: 21, no: "SX-2021021", name: "王辉", gender: "男", dept: "运输部", post: "调度员", phone: "13805580121", hire: "2021-02-22", contractEnd: "2027-02-21", kind: "劳动合同", status: "在职", edu: "大专", native: "安徽安庆", emergency: "王妻", emergPhone: "13805580131", remark: "运输计划与任务单调度。" },
  7888. { id: 22, no: "SX-2015022", name: "张伟", gender: "男", dept: "生产部", post: "操作工", phone: "13605580122", hire: "2015-04-01", contractEnd: "2025-03-31", kind: "劳动合同", status: "离职", edu: "初中", native: "安徽安庆", emergency: "张父", emergPhone: "13605580132", remark: "2025-04-01 离职,档案留存。" }
  7889. ];
  7890. function staffById(id) { return staffs.find((x) => x.id === id); }
  7891. function staffContractSoon(x) {
  7892. if (!x.contractEnd || x.status === "离职") return false;
  7893. const d = daysBetween(today, x.contractEnd);
  7894. return d >= 0 && d <= 30;
  7895. }
  7896. function staffStatusPill(st) {
  7897. if (st === "在职") return `<span class="st st-run">在职</span>`;
  7898. if (st === "试用") return `<span class="st st-fix">试用</span>`;
  7899. return `<span class="st st-stop">离职</span>`;
  7900. }
  7901. function filteredStaff() {
  7902. const q = (document.getElementById("staffQ")?.value || "").trim();
  7903. const dept = document.getElementById("staffDept")?.value || "";
  7904. let rows = staffs.slice();
  7905. if (staffTab !== "全部") rows = rows.filter((x) => x.status === staffTab);
  7906. if (dept) rows = rows.filter((x) => x.dept === dept);
  7907. if (q) rows = rows.filter((x) => [x.no, x.name, x.post, x.phone, x.dept].join(" ").includes(q));
  7908. return rows;
  7909. }
  7910. function setStaffTab(tab) {
  7911. staffTab = tab;
  7912. staffPage = 1;
  7913. document.querySelectorAll("#staffTabs button").forEach((b) => b.classList.toggle("on", b.dataset.tab === tab));
  7914. renderStaff();
  7915. }
  7916. function toggleStaff(id, on) {
  7917. if (on) selectedStaffIds.add(id); else selectedStaffIds.delete(id);
  7918. renderStaff();
  7919. }
  7920. function toggleStaffAll(on) {
  7921. const rows = filteredStaff();
  7922. const start = (staffPage - 1) * staffPageSize;
  7923. rows.slice(start, start + staffPageSize).forEach((x) => on ? selectedStaffIds.add(x.id) : selectedStaffIds.delete(x.id));
  7924. renderStaff();
  7925. }
  7926. function fillStaffDeptSelect(val) {
  7927. const sel = document.getElementById("staffDeptSelect");
  7928. if (sel) {
  7929. sel.innerHTML = STAFF_DEPTS.map((d) => `<option>${d}</option>`).join("");
  7930. if (val) sel.value = val;
  7931. }
  7932. const filter = document.getElementById("staffDept");
  7933. if (filter && !filter.dataset.ready) {
  7934. filter.innerHTML = `<option value="">全部部门</option>` + STAFF_DEPTS.map((d) => `<option>${d}</option>`).join("");
  7935. filter.dataset.ready = "1";
  7936. }
  7937. }
  7938. function renderStaff() {
  7939. fillStaffDeptSelect();
  7940. const onJob = staffs.filter((x) => x.status === "在职").length;
  7941. const trial = staffs.filter((x) => x.status === "试用").length;
  7942. const left = staffs.filter((x) => x.status === "离职").length;
  7943. const due = staffs.filter(staffContractSoon).length;
  7944. document.getElementById("staffStats").innerHTML = `
  7945. <div class="stat"><div class="k">在职</div><div class="v">${onJob}</div><div class="s">劳动合同 / 劳务在岗</div></div>
  7946. <div class="stat"><div class="k">试用</div><div class="v">${trial}</div><div class="s">试用期内人员</div></div>
  7947. <div class="stat"><div class="k">离职</div><div class="v">${left}</div><div class="s">档案留存</div></div>
  7948. <div class="stat"><div class="k">合同将到期</div><div class="v">${due}</div><div class="s">30 日内到期</div></div>`;
  7949. const rows = filteredStaff();
  7950. const pages = Math.max(1, Math.ceil(rows.length / staffPageSize));
  7951. if (staffPage > pages) staffPage = pages;
  7952. const start = (staffPage - 1) * staffPageSize;
  7953. const pageRows = rows.slice(start, start + staffPageSize);
  7954. const allOn = pageRows.length && pageRows.every((x) => selectedStaffIds.has(x.id));
  7955. document.getElementById("staffSelHint").textContent = `已选 ${selectedStaffIds.size} 项`;
  7956. document.getElementById("staffTable").innerHTML = pageRows.length ? `<table class="erp-table staff-table">
  7957. <thead><tr>
  7958. <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="toggleStaffAll(this.checked)" /></th>
  7959. <th>工号</th><th>姓名</th><th>性别</th><th>部门</th><th>岗位</th><th>手机</th><th>入职日期</th><th>合同到期</th><th>用工类型</th><th>状态</th>
  7960. </tr></thead>
  7961. <tbody>${pageRows.map((x) => `<tr>
  7962. <td class="chk"><input type="checkbox" ${selectedStaffIds.has(x.id) ? "checked" : ""} onchange="toggleStaff(${x.id}, this.checked)" /></td>
  7963. <td class="cell-no">${x.no}</td>
  7964. <td><span class="staff-name"><span class="staff-av">${x.name.slice(0, 1)}</span><button class="quote-link" type="button" onclick="openStaffDetail(${x.id})">${x.name}</button></span></td>
  7965. <td>${x.gender}</td><td>${x.dept}</td><td>${x.post}</td><td>${x.phone}</td>
  7966. <td>${x.hire}</td>
  7967. <td class="${staffContractSoon(x) ? "staff-due" : ""}">${x.contractEnd || "—"}${staffContractSoon(x) ? " 将到期" : ""}</td>
  7968. <td>${x.kind}</td>
  7969. <td>${staffStatusPill(x.status)}</td>
  7970. </tr>`).join("")}</tbody></table>` : `<div class="empty">暂无符合条件的员工</div>`;
  7971. const show = Math.min(pages, 6);
  7972. const nums = [];
  7973. for (let n = 1; n <= show; n++) nums.push(n);
  7974. document.getElementById("staffPager").innerHTML = `
  7975. <span class="muted">共 ${rows.length} 条</span>
  7976. <select class="page-size" onchange="staffPageSize=+this.value;staffPage=1;renderStaff()">
  7977. <option value="20" ${staffPageSize === 20 ? "selected" : ""}>20条/页</option>
  7978. <option value="50" ${staffPageSize === 50 ? "selected" : ""}>50条/页</option>
  7979. </select>
  7980. <button type="button" onclick="staffPage=Math.max(1, staffPage-1);renderStaff()">‹</button>
  7981. ${nums.map((n) => `<button type="button" class="${n === staffPage ? "on" : ""}" onclick="staffPage=${n};renderStaff()">${n}</button>`).join("")}
  7982. <button type="button" onclick="staffPage=Math.min(${pages}, staffPage+1);renderStaff()">›</button>
  7983. <span class="muted">前往</span>
  7984. <input class="quote-goto" type="number" min="1" max="${pages}" value="${staffPage}" onchange="staffPage=Math.min(${pages}, Math.max(1, +this.value||1));renderStaff()" />
  7985. <span class="muted">页</span>`;
  7986. }
  7987. function openStaffDlg(id) {
  7988. const f = document.getElementById("staffForm");
  7989. f.reset();
  7990. const x = id ? staffById(id) : null;
  7991. fillStaffDeptSelect(x ? x.dept : "行政人事");
  7992. document.getElementById("staffDlgTitle").textContent = x ? "编辑员工" : "新增员工";
  7993. f.id.value = x ? x.id : "";
  7994. if (x) {
  7995. f.name.value = x.name; f.no.value = x.no; f.gender.value = x.gender;
  7996. f.post.value = x.post; f.phone.value = x.phone; f.hire.value = x.hire;
  7997. f.contractEnd.value = x.contractEnd; f.kind.value = x.kind; f.status.value = x.status;
  7998. f.edu.value = x.edu; f.native.value = x.native; f.emergency.value = x.emergency;
  7999. f.emergPhone.value = x.emergPhone; f.remark.value = x.remark || "";
  8000. } else {
  8001. f.hire.value = today; f.status.value = "试用"; f.kind.value = "劳动合同";
  8002. }
  8003. document.getElementById("staffDialog").showModal();
  8004. }
  8005. function saveStaff() {
  8006. const f = document.getElementById("staffForm");
  8007. if (!f.name.value.trim() || !f.post.value.trim()) { toast("请填写姓名和岗位"); return; }
  8008. const id = Number(f.id.value);
  8009. const row = {
  8010. name: f.name.value.trim(), no: f.no.value.trim(), gender: f.gender.value, dept: f.dept.value,
  8011. post: f.post.value.trim(), phone: f.phone.value.trim(), hire: f.hire.value, contractEnd: f.contractEnd.value,
  8012. kind: f.kind.value, status: f.status.value, edu: f.edu.value, native: f.native.value.trim(),
  8013. emergency: f.emergency.value.trim(), emergPhone: f.emergPhone.value.trim(), remark: f.remark.value.trim()
  8014. };
  8015. if (id) {
  8016. const x = staffById(id);
  8017. if (x) Object.assign(x, row);
  8018. } else {
  8019. if (!row.no) row.no = "SX-" + (++staffSeq);
  8020. staffs.unshift({ id: Date.now(), ...row });
  8021. }
  8022. document.getElementById("staffDialog").close();
  8023. renderStaff();
  8024. toast("已保存员工档案");
  8025. }
  8026. function openStaffDetail(id) {
  8027. const x = staffById(id);
  8028. if (!x) return;
  8029. staffDetailId = id;
  8030. document.getElementById("staffDetailTitle").textContent = x.name + " · " + x.no;
  8031. document.getElementById("staffDetailBody").innerHTML = `
  8032. <div class="split-h">
  8033. <div>
  8034. <h4>基本信息</h4>
  8035. <div class="kv">
  8036. ${kv("工号", x.no)}${kv("姓名", x.name)}${kv("性别", x.gender)}
  8037. ${kv("部门", x.dept)}${kv("岗位", x.post)}${kv("手机", x.phone)}
  8038. ${kv("学历", x.edu)}${kv("籍贯", x.native || "—")}
  8039. </div>
  8040. </div>
  8041. <div>
  8042. <h4>合同与联系</h4>
  8043. <div class="kv">
  8044. ${kv("状态", x.status)}${kv("用工类型", x.kind)}
  8045. ${kv("入职日期", x.hire || "—")}${kv("合同到期", x.contractEnd || "—")}
  8046. ${kv("紧急联系人", x.emergency || "—")}${kv("联系人电话", x.emergPhone || "—")}
  8047. </div>
  8048. <p style="margin-top:12px" class="muted">${x.remark || "无备注"}</p>
  8049. </div>
  8050. </div>`;
  8051. document.getElementById("staffDetailDialog").showModal();
  8052. }
  8053. function editStaffFromDetail() {
  8054. document.getElementById("staffDetailDialog").close();
  8055. if (staffDetailId) openStaffDlg(staffDetailId);
  8056. }
  8057. function editSelectedStaff() {
  8058. const ids = [...selectedStaffIds];
  8059. if (ids.length !== 1) { toast("请勾选一名员工后编辑"); return; }
  8060. openStaffDlg(ids[0]);
  8061. }
  8062. function leaveSelectedStaff() {
  8063. const ids = [...selectedStaffIds];
  8064. if (!ids.length) { toast("请先勾选员工"); return; }
  8065. let n = 0;
  8066. ids.forEach((id) => {
  8067. const x = staffById(id);
  8068. if (x && x.status !== "离职") { x.status = "离职"; x.remark = (x.remark ? x.remark + ";" : "") + today + " 办理离职"; n++; }
  8069. });
  8070. selectedStaffIds.clear();
  8071. renderStaff();
  8072. toast(n ? `已办理 ${n} 人离职` : "所选人员已是离职状态");
  8073. }
  8074. function showCertView(name, id) {
  8075. if (name === "detail") {
  8076. if (id != null) currentCertId = id;
  8077. if (currentCertId == null) {
  8078. toast("请先从列表选择一条证件");
  8079. name = "list";
  8080. }
  8081. }
  8082. const waitEl = document.getElementById("view-cert-wait");
  8083. if (waitEl) waitEl.classList.toggle("hidden", name !== "wait");
  8084. document.getElementById("view-cert-list").classList.toggle("hidden", name !== "list");
  8085. document.getElementById("view-cert-detail").classList.toggle("hidden", name !== "detail");
  8086. document.querySelectorAll("#certSubnav button").forEach((b) => b.classList.toggle("on", b.dataset.view === name));
  8087. document.getElementById("navDetail").disabled = currentCertId == null;
  8088. document.getElementById("crumb").textContent = name === "detail" ? "证件信息" : name === "wait" ? "待关联计划" : "证件列表";
  8089. if (name === "wait") renderWaitPlans();
  8090. if (name === "list") renderCertList();
  8091. if (name === "detail") renderCertDetail();
  8092. }
  8093. function kv(lab, val) {
  8094. return `<div class="lab">${lab}</div><div class="val">${val || "—"}</div>`;
  8095. }
  8096. function certThumbHtml(src, alt) {
  8097. if (!src) return "";
  8098. return `<button type="button" class="cert-thumb" onclick="openCertImg(this)"><img src="${src}" alt="${alt || "证件"}"></button>`;
  8099. }
  8100. function openCertImg(el) {
  8101. const img = el && (el.tagName === "IMG" ? el : el.querySelector("img"));
  8102. if (!img || !img.src) return;
  8103. const view = document.getElementById("certImgView");
  8104. const dlg = document.getElementById("certImgDialog");
  8105. view.src = img.src;
  8106. view.alt = img.alt || "证件原图";
  8107. if (dlg.open) dlg.close();
  8108. dlg.showModal();
  8109. }
  8110. function closeCertImg(ev) {
  8111. const dlg = document.getElementById("certImgDialog");
  8112. if (!dlg.open) return;
  8113. if (!ev || ev.target === dlg || ev.target === document.getElementById("certImgView")) dlg.close();
  8114. }
  8115. function renderWaitPlans() {
  8116. const waitList = waitListPlans();
  8117. const waitLinkAll = plans.filter(isWaitLinkPlan);
  8118. const todayN = waitLinkAll.filter(isWaitTodayPlan).length;
  8119. const weekN = waitLinkAll.filter(isWaitWeekPlan).length;
  8120. const buyWarnN = certs.filter((c) => buyWarn(c)).length;
  8121. const waitStats = document.getElementById("waitStats");
  8122. if (waitStats) {
  8123. waitStats.innerHTML = [
  8124. ["今日待关联计划", todayN, "送货或计划日期为今天", ""],
  8125. ["本周待关联计划", weekN, "本周一至周日", ""],
  8126. ["购买证预警", buyWarnN, "余量 ≤ 自设预警量", "filterCertSt('余量预警')"]
  8127. ].map(([k, v, s, fn]) => `<div class="stat${fn ? " stat-click" : ""}" ${fn ? `onclick="${fn}"` : ""}><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");
  8128. }
  8129. const waitBox = document.getElementById("certWaitPlans");
  8130. if (!waitBox) return;
  8131. const allWait = plans.filter(isWaitListPlan);
  8132. const nIn = allWait.filter((p) => planCertScene(p) === "in").length;
  8133. const nOut = allWait.filter((p) => planCertScene(p) === "out").length;
  8134. const nDirect = allWait.filter((p) => planCertScene(p) === "direct").length;
  8135. const tabs = [
  8136. ["", `全部 ${allWait.length}`],
  8137. ["in", `入库单 ${nIn}`],
  8138. ["out", `出库单 ${nOut}`],
  8139. ["direct", `直销单 ${nDirect}`]
  8140. ].map(([k, lab]) => `<button type="button" class="${waitLinkSceneTab === k ? "on" : ""}" onclick="setWaitLinkScene('${k}')">${lab}</button>`).join("");
  8141. waitBox.innerHTML = `<div class="wait-scene-tabs">${tabs}</div>` + (waitList.length ? `<div class="wait-table-wrap"><table class="erp-table plan-table wait-table">
  8142. <thead><tr>
  8143. <th class="col-op-buy">购买证</th>
  8144. <th class="col-op-trans">运输证</th>
  8145. <th class="idx">序号</th>
  8146. <th class="col-no">计划编号</th>
  8147. <th>跟进状态</th>
  8148. <th>计划日期</th>
  8149. <th>送货时间</th>
  8150. <th>单据类型</th>
  8151. <th>车牌号</th>
  8152. <th>驾押人员</th>
  8153. <th>装货客户</th>
  8154. <th class="col-ship">送货客户</th>
  8155. <th>产品名称</th>
  8156. <th>产品规格</th>
  8157. <th>包装规格</th>
  8158. <th>运输证总量</th>
  8159. <th>本次计划数量</th>
  8160. <th>已发数量</th>
  8161. <th>余量</th>
  8162. <th>合同</th>
  8163. </tr></thead>
  8164. <tbody>${waitList.map((p, i) => waitLinkRowHtml(p, i)).join("")}</tbody>
  8165. </table></div>` : `<div class="empty">${waitLinkSceneTab ? "该场景暂无已安排、已选车号的易制毒计划。" : "暂无已安排、已选车号的易制毒运输计划。"}</div>`);
  8166. }
  8167. function renderCertList() {
  8168. const q = (document.getElementById("certQ").value || "").trim();
  8169. const st = document.getElementById("certSt").value;
  8170. const warnN = certs.filter((c) => certStatusOf(c) === "余量预警" || certStatusOf(c) === "即将到期").length;
  8171. const transN = certs.filter((c) => transWarn(c)).length;
  8172. const buyWarnN = certs.filter((c) => buyWarn(c)).length;
  8173. const overN = certs.filter((c) => certOver(c)).length;
  8174. const vehN = certs.reduce((s, c) => s + vehicleWarnCount(c), 0);
  8175. const todayN = certs.filter((c) => c.created === today).length;
  8176. const todayTransN = certs.reduce((s, c) => s + transOf(c).filter((t) => t.created === today).length, 0);
  8177. const waitLink = plans.filter(isWaitLinkPlan).length;
  8178. const vehRowsAll = certs.flatMap((c) => vehicleRowsOf(c).map((r) => ({ ...r, cert: c })));
  8179. const vehWarnRows = vehRowsAll.filter((r) => r.level);
  8180. const stepEl = document.getElementById("certSteps");
  8181. if (stepEl) {
  8182. const step2on = waitLink > 0;
  8183. const step3on = vehWarnRows.length > 0 || buyWarnN || transN || overN;
  8184. stepEl.innerHTML = [
  8185. ["1", "上传证件", "上传购买证、运输证,核对证载数量与车号", `${certs.length} 本购买证 / ${todayTransN || certs.reduce((s, c) => s + transCount(c), 0)} 张运输证`, "openCertDlg(false)", ""],
  8186. ["2", "人工关联计划", "核对证件、车辆后点击确认关联", waitLink ? `${waitLink} 条待确认` : "暂无待关联计划", "focusCertWait()", step2on ? "on" : ""],
  8187. ["3", "余量预警", "完成后扣减已完成量;购买证或运输证余量 ≤ 自设预警量时预警", vehWarnRows.length ? `${vehWarnRows.length} 车次预警` : "关联、完成后自动计算", "focusCertVehWarn()", step3on ? "warn" : ""]
  8188. ].map(([n, title, desc, s, fn, cls]) => `<div class="cert-step ${cls}" onclick="${fn}"><span class="n">${n}</span><div><b>${title}</b><p>${desc}</p><div class="s">${s}</div></div></div>`).join("");
  8189. }
  8190. document.getElementById("listAlerts").innerHTML = `
  8191. ${overN ? `<div class="alert red">关联计划量已超过购买证或运输证证载 ${overN} 条</div>` : ""}
  8192. ${vehN ? `<div class="alert red">关联车辆计划量预警 ${vehN} 车次(超证载 / 购买证或运输证余量 ≤ 自设预警量)</div>` : ""}
  8193. ${transN ? `<div class="alert red">运输证余量预警 ${transN} 条</div>` : ""}
  8194. ${buyWarnN ? `<div class="alert red">购买证余量预警 ${buyWarnN} 条</div>` : ""}
  8195. ${warnN && !buyWarnN ? `<div class="alert red">购买证即将到期 ${warnN} 条</div>` : ""}
  8196. <div class="alert ok">在册购买证 ${certs.length} 条 · 待关联计划 ${waitLink} 条</div>`;
  8197. document.getElementById("listStats").innerHTML = [
  8198. ["今日上传证件", todayN + " / " + todayTransN, "购买证 / 运输证", "openCertDlg(false)"],
  8199. ["购买证预警", buyWarnN, "余量 ≤ 自设预警量", "filterCertSt('余量预警')"],
  8200. ["运输证预警", transN, "余量 ≤ 自设预警量", "filterCertSt('运输证预警')"],
  8201. ["车辆计划预警", vehN, "各车运输计划量对照两证", "focusCertVehWarn()"]
  8202. ].map(([k, v, s, fn]) => `<div class="stat${fn ? " stat-click" : ""}" ${fn ? `onclick="${fn}"` : ""}><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");
  8203. renderWaitPlans();
  8204. const rows = certs.filter((c) => {
  8205. const blob = [c.buyNo, transNos(c).join(" "), c.product, c.buyer, c.seller,
  8206. transOf(c).map((t) => t.plates).join(" "), transOf(c).map((t) => t.consignee).join(" "),
  8207. transOf(c).map((t) => t.carrier).join(" ")].join(" ");
  8208. if (q && !blob.includes(q)) return false;
  8209. if (st && certStatusOf(c) !== st) return false;
  8210. return true;
  8211. });
  8212. document.getElementById("listCount").textContent = `共 ${rows.length} 条`;
  8213. document.getElementById("certListTable").innerHTML = rows.length ? `<table>
  8214. <thead><tr><th>状态</th><th>购买证号</th><th>运输证</th><th>品名</th><th>购买单位</th><th>运输公司</th><th>证载数量</th><th>关联计划量</th><th>已完成量</th><th>分派车辆</th><th>购买证余量</th><th>运输证余量</th></tr></thead>
  8215. <tbody>${rows.map((c) => {
  8216. const nos = transNos(c);
  8217. const carriers = [...new Set(transOf(c).map((t) => t.carrier).filter(Boolean))];
  8218. const cars = allDispatches(c);
  8219. const warnItems = transOf(c).filter(transItemWarn);
  8220. return `<tr class="row-link" onclick="showCertView('detail', ${c.id})">
  8221. <td>${certStHtml(certStatusOf(c))}</td>
  8222. <td class="cell-no">${c.buyNo}</td>
  8223. <td>${nos.length ? nos.map((n) => `<div>${n}</div>`).join("") : "—"}<div class="muted">${transCount(c)} 张</div></td>
  8224. <td>${c.product}<div class="muted">${c.ctrl || ""}</div></td>
  8225. <td>${c.buyer || "—"}</td>
  8226. <td>${carriers.join("<br>") || "—"}</td>
  8227. <td>${c.buyQty} ${c.unit}</td>
  8228. <td class="${remainGap(c) < -1e-6 ? "staff-due" : ""}">${fmtTon(holdingOf(c))} ${c.unit}<div class="muted">${openLinkedPlans(c).length} 条在途</div></td>
  8229. <td>${fmtTon(deductedOf(c))} ${c.unit}</td>
  8230. <td class="plate-cell">${cars.length ? `<table class="plate-mini"><thead><tr><th>车牌号</th><th>运输证总量</th><th>计划量</th><th>已完成量</th><th>余量</th></tr></thead><tbody>${cars.map((d) => {
  8231. const t = findTrans(c, d.transId);
  8232. const vh = vehiclePlanHold(d.plate, d.transId);
  8233. const done = vehicleDeduct(d.plate, d.transId);
  8234. const over = vehicleHold(d.plate, d.transId) - Number(d.qty || 0) > 1e-6;
  8235. const total = t ? Number(t.transQty || 0) : 0;
  8236. const left = t ? transItemGap(t) : 0;
  8237. const leftWarn = t && left <= transWarnQty(t) + 1e-9;
  8238. return `<tr><td>${d.plate}${over ? `<span class="need-tag">超</span>` : ""}</td><td>${fmtTon(total)}${c.unit}</td><td>${fmtTon(vh)}${c.unit}</td><td>${fmtTon(done)}${c.unit}</td><td class="${leftWarn ? "staff-due" : ""}">${fmtTon(left)}${c.unit}</td></tr>`;
  8239. }).join("")}</tbody></table>` : "—"}</td>
  8240. <td>${remainHtml(c)}</td>
  8241. <td>${transRemainHtml(c)}${warnItems.length ? `<div class="muted">${warnItems.length} 张预警</div>` : ""}</td>
  8242. </tr>`;
  8243. }).join("")}</tbody></table>` : `<div class="empty">没有记录。请先上传购买证及运输证。</div>`;
  8244. updateBadges();
  8245. }
  8246. function filterCertSt(st) {
  8247. const sel = document.getElementById("certSt");
  8248. if (sel) sel.value = st || "";
  8249. showCertView("list");
  8250. }
  8251. function focusCertWait() {
  8252. showCertView("wait");
  8253. }
  8254. function focusCertVehWarn() {
  8255. showCertView("list");
  8256. }
  8257. function waitCertOpts(p, selectedId, kind) {
  8258. const pred = kind === "procure" ? certIsProcure : certIsShipBuy;
  8259. const opts = [`<option value="">请选择</option>`];
  8260. certs.forEach((c) => {
  8261. const hit = pred(p, c);
  8262. if (!hit && c.id !== selectedId) return;
  8263. if (certVoided(c) && c.id !== selectedId) return;
  8264. opts.push(`<option value="${c.id}" ${c.id === selectedId ? "selected" : ""} ${certVoided(c) ? "disabled" : ""}>${c.buyNo} · ${certVoided(c) ? "已核销" : "余 " + fmtTon(remain(c)) + c.unit}${hit ? " ✓" : ""}</option>`);
  8265. });
  8266. return opts.join("");
  8267. }
  8268. function waitBuyLinked(p) {
  8269. if (planNeedProcureCert(p)) return !!(p.procureCertId && p.shipCertId);
  8270. return !!(p.certId || p.shipCertId);
  8271. }
  8272. function waitOpBtns(p) {
  8273. const buyOn = waitBuyLinked(p);
  8274. const transOn = !!p.transId;
  8275. const buyBtn = buyOn
  8276. ? `<button class="btn btn-sm wait-btn-buy wait-on" type="button" data-wait-op="cancel-buy" data-plan-id="${p.id}" title="已关联,需先取消关联才能修改" onclick="cancelWaitBuyLink(${p.id})">已关联</button>`
  8277. : `<button class="btn btn-sm wait-btn-buy" type="button" data-wait-op="buy" data-plan-id="${p.id}" title="点击关联购买证" onclick="openWaitBuyDlg(${p.id})">待关联</button>`;
  8278. const transBtn = transOn
  8279. ? `<button class="btn btn-sm wait-btn-trans wait-on" type="button" data-wait-op="cancel-trans" data-plan-id="${p.id}" title="已关联,需先取消关联才能修改" onclick="cancelWaitTransLink(${p.id})">已关联</button>`
  8280. : `<button class="btn btn-sm wait-btn-trans" type="button" data-wait-op="trans" data-plan-id="${p.id}" title="点击关联运输证" onclick="openWaitTransDlg(${p.id})">待关联</button>`;
  8281. return `<td class="col-op-buy">${buyBtn}</td><td class="col-op-trans">${transBtn}</td>`;
  8282. }
  8283. function cancelWaitBuyLink(planId) {
  8284. const p = plans.find((x) => x.id === planId);
  8285. if (!p || !waitBuyLinked(p)) return;
  8286. const extra = p.transId ? "将同时取消已关联的运输证。" : "";
  8287. if (!confirm(`已关联购买证不可直接修改。${extra}确定取消关联购买证?`)) return;
  8288. p.certId = null;
  8289. p.shipCertId = null;
  8290. p.procureCertId = null;
  8291. p.transId = null;
  8292. delete waitLinkPicks[planId];
  8293. toast(`${p.no} 已取消购买证关联`);
  8294. renderCertList();
  8295. updateBadges();
  8296. }
  8297. function cancelWaitTransLink(planId) {
  8298. const p = plans.find((x) => x.id === planId);
  8299. if (!p || !p.transId) return;
  8300. if (!confirm("已关联运输证不可直接修改。确定取消关联运输证?")) return;
  8301. p.transId = null;
  8302. if (waitLinkPicks[planId]) waitLinkPicks[planId].transId = "";
  8303. toast(`${p.no} 已取消运输证关联`);
  8304. renderCertList();
  8305. updateBadges();
  8306. }
  8307. function waitPlanTransOf(p) {
  8308. if (!p || !p.transId) return { c: null, t: null };
  8309. const ids = [p.certId, p.shipCertId, p.procureCertId].filter(Boolean);
  8310. for (const id of ids) {
  8311. const c = certs.find((x) => x.id === id);
  8312. const t = c ? findTrans(c, p.transId) : null;
  8313. if (t) return { c, t };
  8314. }
  8315. for (const c of certs) {
  8316. const t = findTrans(c, p.transId);
  8317. if (t) return { c, t };
  8318. }
  8319. return { c: null, t: null };
  8320. }
  8321. function waitLinkRowHtml(p, i) {
  8322. const { c, t } = waitPlanTransOf(p);
  8323. const unit = (c && c.unit) || p.unit || "吨";
  8324. const transTotal = t ? Number(t.transQty || 0) : null;
  8325. const sent = t ? transItemDeduct(t) : null;
  8326. const left = t ? transItemGap(t) : null;
  8327. const warnAt = t ? transWarnQty(t) : 0;
  8328. const leftWarn = left != null && left <= warnAt + 1e-9;
  8329. return `<tr>
  8330. ${waitOpBtns(p)}
  8331. <td class="idx">${(i || 0) + 1}</td>
  8332. <td class="cell-no col-no">${p.no}</td>
  8333. <td>${planStHtml(p)}</td>
  8334. <td>${p.date}</td>
  8335. <td>${planDeliverAt(p)}</td>
  8336. <td>${planTypeName(p)}</td>
  8337. <td>${p.tractor || "—"}</td>
  8338. <td>${planCrew(p)}</td>
  8339. <td class="clip">${p.loadFrom || "—"}</td>
  8340. <td class="col-ship">${p.customer || p.settler || "—"}</td>
  8341. <td>${p.product || "—"}${p.ctrl === "易制毒" ? `<span class="prod-tag red">易制毒</span>` : ""}</td>
  8342. <td>${p.spec || "—"}</td>
  8343. <td>${p.pack || "—"}</td>
  8344. <td class="num">${transTotal == null ? "—" : `${fmtTon(transTotal)} ${unit}`}</td>
  8345. <td class="num qty">${p.planQty}${p.unit ? " " + p.unit : ""}</td>
  8346. <td class="num">${sent == null ? "—" : `${fmtTon(sent)} ${unit}`}</td>
  8347. <td class="num ${leftWarn ? "staff-due" : ""}">${left == null ? "—" : `${fmtTon(left)} ${unit}`}</td>
  8348. <td class="clip">${planContractNo(p)}</td>
  8349. </tr>`;
  8350. }
  8351. function waitPlanBarHtml(p) {
  8352. return `<div class="wait-plan-bar">
  8353. <span>计划 <b>${p.no}</b></span>
  8354. <span>${planTypeName(p)}</span>
  8355. <span>${p.product || "—"}${p.ctrl === "易制毒" ? `<span class="prod-tag red">易制毒</span>` : ""}</span>
  8356. <span>计划量 <b>${p.planQty}${p.unit ? " " + p.unit : ""}</b></span>
  8357. <span>车牌 <b>${p.tractor || "—"}</b></span>
  8358. <span>装货 ${p.loadFrom || "—"}</span>
  8359. <span>送货 ${p.customer || p.settler || "—"}</span>
  8360. </div>`;
  8361. }
  8362. function waitBuyCardHtml(c, extra, pickHtml) {
  8363. if (!c) return `<div class="empty">暂无购买证信息</div>`;
  8364. return `<div class="cert-pane buy">
  8365. <div class="cert-pane-h">
  8366. ${pickHtml || ""}
  8367. <span class="cert-tag buy">购买证</span>
  8368. <h2>购买备案证明</h2>
  8369. ${extra || ""}
  8370. </div>
  8371. <article class="card">
  8372. <div class="card-h"><h2>${c.buyNo || "未编号"}</h2></div>
  8373. <div class="card-b"><div class="kv">
  8374. ${kv("编号", c.buyNo)}${kv("核发日期", c.buyDate)}
  8375. ${kv("购买单位", c.buyer)}${kv("法定代表人", c.buyerLegal)}
  8376. ${kv("电话", c.buyerTel)}${kv("住所", c.buyerAddr)}
  8377. ${kv("销售单位", c.seller)}${kv("销售方法人", c.sellerLegal)}
  8378. ${kv("购买物品", c.product)}${kv("用途", c.usage)}
  8379. ${kv("数量", (c.buyQty || "—") + " " + (c.unit || ""))}${kv("有效次数", c.buyTimes)}
  8380. ${kv("有效期限", (c.buyFrom || "") + " 至 " + (c.buyDue || ""))}
  8381. ${kv("公安机关", c.buyPolice)}${kv("经办人", c.buyHandler)}
  8382. ${kv("余量", fmtTon(remain(c)) + " " + (c.unit || ""))}
  8383. </div>${c.buyUrl ? `<img src="${c.buyUrl}" alt="购买证" style="margin-top:12px;max-height:160px;object-fit:contain;width:100%">` : ""}</div>
  8384. </article>
  8385. </div>`;
  8386. }
  8387. function waitTransCardHtml(c, t, plate, actionHtml) {
  8388. const hit = plate && (platesOfTrans(t).includes(plate) || String(t.plates || "").includes(plate));
  8389. const left = transItemRemain(t);
  8390. const warn = transItemWarn(t);
  8391. const used = transItemHold(t);
  8392. const tPct = Math.min(100, (used / (Number(t.transQty || 1) || 1)) * 100);
  8393. return `<article class="card wait-trans-sheet${hit ? " hit" : ""}">
  8394. <div class="card-h">
  8395. <h2>${t.transNo || "未编号"}</h2>
  8396. ${hit ? `<span class="match-rec veh">匹配 ${plate}</span>` : ""}
  8397. ${actionHtml || ""}
  8398. </div>
  8399. <div class="card-b"><div class="kv">
  8400. ${kv("编号", t.transNo)}${kv("核发日期", t.transDate)}
  8401. ${kv("货主", t.owner)}${kv("法定代表人", t.ownerLegal)}
  8402. ${kv("运输物品", c.product)}${kv("数量", (t.transQty || "—") + " " + (c.unit || ""))}
  8403. ${kv("包装", t.pack)}${kv("购买许可证号", t.permitNo)}
  8404. ${kv("运输单位", t.carrier)}${kv("运输方式", t.mode)}
  8405. ${kv("车号(次)", t.plates || platesOfTrans(t).join("、") || "—")}${kv("收货单位", t.consignee)}
  8406. ${kv("运输路线", t.route)}${kv("申请单位", t.applicantUnit)}
  8407. ${kv("有效期限", (t.transFrom || "") + " 至 " + (t.transDue || ""))}
  8408. ${kv("公安机关", t.transPolice)}${kv("经办人", t.transHandler)}
  8409. </div>
  8410. <div style="margin-top:14px;color:var(--muted);font-size:12px">本证进度(已完成 ${transItemDeduct(t)} + 在途计划 ${transItemPlan(t)} / 运输证 ${t.transQty || 0})${warn ? ` · 余量 ${left} 吨预警` : ""}</div>
  8411. <div class="qty-bar"><div class="qty-fill${warn ? " warn" : ""}" style="width:${tPct}%"></div></div>
  8412. ${t.transUrl ? `<img src="${t.transUrl}" alt="运输证" style="margin-top:12px;max-height:160px;object-fit:contain;width:100%">` : ""}
  8413. </div>
  8414. </article>`;
  8415. }
  8416. function waitDemoCertsForPlan(p) {
  8417. return certs.filter((c) => certSelectable(c) && (!p.ctrl || !c.ctrl || p.ctrl === c.ctrl));
  8418. }
  8419. function waitBuyCertTableHtml(p, kind, selectedId) {
  8420. const pred = kind === "procure" ? certIsProcure : certIsShipBuy;
  8421. const title = kind === "procure" ? "装货方购买证" : "送货方购买证";
  8422. const name = kind === "procure" ? "waitProcureCert" : "waitShipCert";
  8423. let rows = certs.filter((c) => (pred(p, c) || c.id === selectedId) && (!certVoided(c) || c.id === selectedId));
  8424. if (!rows.length) rows = waitDemoCertsForPlan(p);
  8425. if (!rows.length) return `<div class="empty">没有可关联的${title}(已核销证件不可选)。</div>`;
  8426. const live = rows.filter((c) => !certVoided(c));
  8427. const picked = (selectedId && !certVoided(certs.find((x) => x.id === selectedId)) && selectedId)
  8428. || (kind === "procure" ? (suggestProcureCert(p) || {}).id : (suggestShipCert(p) || {}).id)
  8429. || (live[0] && live[0].id);
  8430. if (!live.length) return `<div class="empty">匹配的${title}均已核销,无法再关联。</div>`;
  8431. return live.map((c) => {
  8432. const hit = pred(p, c);
  8433. const pick = `<label class="wait-cert-pick"><input type="radio" name="${name}" value="${c.id}" ${c.id === picked ? "checked" : ""} onchange="refreshWaitBuyTransPane()" /></label>`;
  8434. const extra = `<span class="muted">${title}</span>${hit ? `<span class="match-rec">已匹配</span>` : ""}`;
  8435. return waitBuyCardHtml(c, extra, pick);
  8436. }).join("");
  8437. }
  8438. function refreshWaitBuyTransPane() {
  8439. const p = plans.find((x) => x.id === waitBuyPlanId);
  8440. const box = document.getElementById("waitBuyTransPane");
  8441. if (!p || !box) return;
  8442. const ids = [...document.querySelectorAll("#waitBuyDialog [name=waitProcureCert]:checked, #waitBuyDialog [name=waitShipCert]:checked")]
  8443. .map((el) => +el.value).filter(Boolean);
  8444. const uniq = [...new Set(ids)];
  8445. if (!uniq.length) {
  8446. box.innerHTML = `<div class="empty">请先在左侧选择购买证,右侧带出该证下的运输证信息。</div>`;
  8447. return;
  8448. }
  8449. const plate = p.tractor || "";
  8450. const cards = [];
  8451. const seen = new Set();
  8452. uniq.forEach((id) => {
  8453. const c = certs.find((x) => x.id === id);
  8454. if (!certSelectable(c)) return;
  8455. const list = transOf(c).filter((t) => transSelectable(c, t));
  8456. const matched = list.filter((t) => plate && (platesOfTrans(t).includes(plate) || String(t.plates || "").includes(plate)));
  8457. const show = matched.length ? matched : list;
  8458. if (!show.length && list.length) show.push(...list);
  8459. show.forEach((t) => {
  8460. if (seen.has(t.id)) return;
  8461. seen.add(t.id);
  8462. cards.push(waitTransCardHtml(c, t, plate));
  8463. });
  8464. });
  8465. const oneSide = planNeedProcureCert(p);
  8466. box.innerHTML = `<div class="cert-pane trans">
  8467. <div class="cert-pane-h">
  8468. <span class="cert-tag trans">运输证</span>
  8469. <h2>运输备案证明</h2>
  8470. <span class="muted">${oneSide ? "任选一方" : `${cards.length} 张`}${plate ? ` · ${plate}` : ""}</span>
  8471. </div>
  8472. ${cards.length ? cards.join("") : `<div class="empty">所选购买证下暂无对应车辆的运输证。</div>`}
  8473. </div>`;
  8474. }
  8475. function openWaitBuyDlg(planId) {
  8476. const p = plans.find((x) => x.id === planId);
  8477. if (!p) return;
  8478. if (waitBuyLinked(p)) { cancelWaitBuyLink(planId); return; }
  8479. if (!certs.length) { toast("请先上传购买证及运输证"); openCertDlg(false); return; }
  8480. waitBuyPlanId = planId;
  8481. const scene = planCertScene(p);
  8482. const needProcure = planNeedProcureCert(p);
  8483. document.getElementById("waitBuyHint").textContent = needProcure
  8484. ? `${p.no} · 直销单 · 装货方、送货方均需关联购买证;运输证只需选择一方购买证下对应车辆的运输证`
  8485. : `${p.no} · ${planSceneLabel(scene)} · 请选择送货方购买证,右侧同步查看该证下运输证`;
  8486. const shipSel = p.shipCertId || (suggestShipCert(p) || {}).id;
  8487. const procSel = p.procureCertId || p.certId || (suggestProcureCert(p) || {}).id;
  8488. const pickHtml = needProcure
  8489. ? waitBuyCertTableHtml(p, "procure", procSel) + `<div style="height:14px"></div>` + waitBuyCertTableHtml(p, "ship", shipSel)
  8490. : waitBuyCertTableHtml(p, "ship", shipSel);
  8491. document.getElementById("waitBuyBody").innerHTML = waitPlanBarHtml(p) + `<div class="wait-dlg-split">
  8492. <div class="wait-info-stack">${pickHtml}</div>
  8493. <div class="wait-info-stack" id="waitBuyTransPane"></div>
  8494. </div>`;
  8495. refreshWaitBuyTransPane();
  8496. const buyDlg = document.getElementById("waitBuyDialog");
  8497. if (buyDlg.open) buyDlg.close();
  8498. buyDlg.showModal();
  8499. }
  8500. function closeWaitBuyDlg() {
  8501. document.getElementById("waitBuyDialog").close();
  8502. waitBuyPlanId = null;
  8503. }
  8504. function saveWaitBuyCert() {
  8505. const p = plans.find((x) => x.id === waitBuyPlanId);
  8506. if (!p) return;
  8507. const shipEl = document.querySelector("#waitBuyDialog [name=waitShipCert]:checked");
  8508. const procEl = document.querySelector("#waitBuyDialog [name=waitProcureCert]:checked");
  8509. const shipId = shipEl ? +shipEl.value : 0;
  8510. const procId = procEl ? +procEl.value : 0;
  8511. if (planNeedProcureCert(p)) {
  8512. if (!procId || !shipId) { toast("直销单请同时选择装货方购买证和送货方购买证"); return; }
  8513. p.procureCertId = procId;
  8514. p.shipCertId = shipId;
  8515. p.certId = procId;
  8516. } else {
  8517. if (!shipId) { toast("请选择送货方购买证"); return; }
  8518. p.shipCertId = shipId;
  8519. p.procureCertId = null;
  8520. p.certId = shipId;
  8521. }
  8522. p.transId = null;
  8523. closeWaitBuyDlg();
  8524. toast(`${p.no} 已关联购买证`);
  8525. renderCertList();
  8526. updateBadges();
  8527. }
  8528. function waitTransRowsForPlan(p) {
  8529. const ids = planNeedProcureCert(p)
  8530. ? [p.procureCertId || p.certId, p.shipCertId]
  8531. : [p.shipCertId || p.certId];
  8532. const uniq = [...new Set(ids.filter(Boolean))];
  8533. const plate = p.tractor || "";
  8534. const rows = [];
  8535. const seen = new Set();
  8536. uniq.forEach((id) => {
  8537. const c = certs.find((x) => x.id === id);
  8538. if (!c) return;
  8539. transOf(c).forEach((t) => {
  8540. if (!transSelectable(c, t)) return;
  8541. const hit = !plate || platesOfTrans(t).includes(plate) || String(t.plates || "").includes(plate);
  8542. if (seen.has(t.id)) return;
  8543. if (hit) {
  8544. seen.add(t.id);
  8545. rows.push({ c, t, hit: true });
  8546. }
  8547. });
  8548. });
  8549. if (!rows.length) {
  8550. uniq.forEach((id) => {
  8551. const c = certs.find((x) => x.id === id);
  8552. if (!c) return;
  8553. transOf(c).forEach((t) => {
  8554. if (!transSelectable(c, t) || seen.has(t.id)) return;
  8555. seen.add(t.id);
  8556. rows.push({ c, t, hit: false });
  8557. });
  8558. });
  8559. }
  8560. return rows;
  8561. }
  8562. function openWaitTransDlg(planId) {
  8563. const p = plans.find((x) => x.id === planId);
  8564. if (!p) return;
  8565. if (!waitBuyLinked(p)) {
  8566. toast(planNeedProcureCert(p) ? "直销单请先关联装货方、送货方购买证" : "请先关联购买证");
  8567. openWaitBuyDlg(planId);
  8568. return;
  8569. }
  8570. if (p.transId) { cancelWaitTransLink(planId); return; }
  8571. waitTransPlanId = planId;
  8572. const plate = p.tractor || "";
  8573. const rows = waitTransRowsForPlan(p);
  8574. const buyIds = [...new Set((planNeedProcureCert(p)
  8575. ? [p.procureCertId || p.certId, p.shipCertId]
  8576. : [p.shipCertId || p.certId]).filter(Boolean))];
  8577. const buyCards = buyIds.map((id) => {
  8578. const c = certs.find((x) => x.id === id);
  8579. if (!c) return "";
  8580. const tag = planNeedProcureCert(p) && (id === p.shipCertId && id !== (p.procureCertId || p.certId))
  8581. ? `<span class="match-rec">送货方</span>`
  8582. : (planNeedProcureCert(p) ? `<span class="match-rec">装货方</span>` : `<span class="match-rec">送货方</span>`);
  8583. return waitBuyCardHtml(c, tag);
  8584. }).join("") || `<div class="empty">请先关联购买证</div>`;
  8585. document.getElementById("waitTransHint").textContent = planNeedProcureCert(p)
  8586. ? `${p.no} · 车牌 ${plate || "—"} · 装货方/送货方购买证已列左侧,运输证只需关联其中一方下对应该车的运输证`
  8587. : `${p.no} · 车牌 ${plate || "—"} · 先核对已关联购买证,再选择该证下对应该车的运输证`;
  8588. const transHtml = rows.length
  8589. ? `<div class="cert-pane trans">
  8590. <div class="cert-pane-h">
  8591. <span class="cert-tag trans">运输证</span>
  8592. <h2>运输备案证明</h2>
  8593. <span class="muted">${rows.length} 张${planNeedProcureCert(p) ? " · 任选一方" : ""}</span>
  8594. </div>
  8595. ${rows.map(({ c, t }) => waitTransCardHtml(c, t, plate,
  8596. `<button class="btn btn-sm btn-primary" type="button" onclick="saveWaitTransCert(${c.id}, ${t.id})">关联此证</button>`
  8597. )).join("")}
  8598. </div>`
  8599. : `<div class="empty">已关联购买证下没有车牌 ${plate || ""} 对应的运输证,请先追加运输证或核对车号。</div>`;
  8600. document.getElementById("waitTransBody").innerHTML = waitPlanBarHtml(p) + `<div class="wait-dlg-split">
  8601. <div class="wait-info-stack">${buyCards}</div>
  8602. <div class="wait-info-stack">${transHtml}</div>
  8603. </div>`;
  8604. const transDlg = document.getElementById("waitTransDialog");
  8605. if (transDlg.open) transDlg.close();
  8606. transDlg.showModal();
  8607. }
  8608. function closeWaitTransDlg() {
  8609. document.getElementById("waitTransDialog").close();
  8610. waitTransPlanId = null;
  8611. }
  8612. function saveWaitTransCert(certId, transId) {
  8613. const p = plans.find((x) => x.id === waitTransPlanId);
  8614. const c = certs.find((x) => x.id === certId);
  8615. const t = c ? findTrans(c, transId) : null;
  8616. if (!p || !c || !t) return;
  8617. const err = attachPlanToCert(p, c, t, true, p.tractor);
  8618. if (err) { toast(err); return; }
  8619. if (planNeedProcureCert(p) && (!p.shipCertId || !(p.procureCertId || p.certId))) {
  8620. toast("直销单还需同时关联装货方、送货方购买证");
  8621. closeWaitTransDlg();
  8622. openWaitBuyDlg(p.id);
  8623. return;
  8624. }
  8625. closeWaitTransDlg();
  8626. toast(`已关联运输证 ${t.transNo} · ${p.tractor || ""}`);
  8627. renderCertList();
  8628. updateBadges();
  8629. }
  8630. function onWaitShipCertChange(planId) {
  8631. const p = plans.find((x) => x.id === planId);
  8632. const sel = document.querySelector(`[data-wait-ship="${planId}"]`);
  8633. if (!p || !sel) return;
  8634. const shipId = +sel.value || "";
  8635. const saved = waitLinkPicks[planId] || {};
  8636. if (planNeedProcureCert(p)) {
  8637. waitLinkPicks[planId] = { ...saved, shipCertId: shipId };
  8638. } else {
  8639. const c = certs.find((x) => x.id === shipId);
  8640. const plate = saved.plate || (c && suggestVehicleFromCert(c, p)) || "";
  8641. const t = transForPlate(c, plate) || suggestTransForPlan(c, p);
  8642. waitLinkPicks[planId] = { certId: shipId, shipCertId: shipId, transId: t && t.id, plate };
  8643. }
  8644. renderCertList();
  8645. }
  8646. function onWaitProcureCertChange(planId) {
  8647. const p = plans.find((x) => x.id === planId);
  8648. const sel = document.querySelector(`[data-wait-procure="${planId}"]`);
  8649. if (!p || !sel) return;
  8650. const saved = waitLinkPicks[planId] || {};
  8651. const certId = +sel.value || "";
  8652. const c = certs.find((x) => x.id === certId);
  8653. const plate = saved.plate || (c && suggestVehicleFromCert(c, p)) || "";
  8654. const t = transForPlate(c, plate) || suggestTransForPlan(c, p);
  8655. waitLinkPicks[planId] = { ...saved, certId, transId: t && t.id, plate };
  8656. renderCertList();
  8657. }
  8658. function onWaitTransChange(planId) {
  8659. const p = plans.find((x) => x.id === planId);
  8660. const sel = document.querySelector(`[data-wait-trans="${planId}"]`);
  8661. if (!p || !sel) return;
  8662. const saved = waitLinkPicks[planId] || {};
  8663. const pick = waitPickOf(p);
  8664. const c = pick.c;
  8665. const t = c ? findTrans(c, +sel.value) : null;
  8666. let plate = saved.plate || pick.plate || "";
  8667. if (t && plate && !platesOfTrans(t).includes(plate) && !String(t.plates || "").includes(plate)) {
  8668. plate = suggestVehicleForPlan(t, p);
  8669. }
  8670. waitLinkPicks[planId] = { ...saved, certId: c && c.id, shipCertId: pick.shipC && pick.shipC.id, transId: +sel.value, plate };
  8671. renderCertList();
  8672. }
  8673. function onWaitVehChange(planId) {
  8674. const p = plans.find((x) => x.id === planId);
  8675. const sel = document.querySelector(`[data-wait-veh="${planId}"]`);
  8676. if (!sel) return;
  8677. const saved = waitLinkPicks[planId] || {};
  8678. const pick = waitPickOf(p);
  8679. const c = pick.c;
  8680. const plate = sel.value;
  8681. const t = transForPlate(c, plate);
  8682. waitLinkPicks[planId] = { ...saved, certId: c && c.id, shipCertId: pick.shipC && pick.shipC.id, transId: t ? t.id : "", plate };
  8683. renderCertList();
  8684. }
  8685. function applyWaitLinkToPlan(p, c, t, plate, shipC) {
  8686. if (planNeedProcureCert(p)) {
  8687. p.procureCertId = c.id;
  8688. p.shipCertId = shipC ? shipC.id : null;
  8689. } else {
  8690. p.shipCertId = (shipC && shipC.id) || c.id;
  8691. p.procureCertId = null;
  8692. }
  8693. return attachPlanToCert(p, c, t, true, plate);
  8694. }
  8695. function confirmWaitLink(planId, silent) {
  8696. const p = plans.find((x) => x.id === planId);
  8697. if (!p) return false;
  8698. if (!certs.length) {
  8699. if (!silent) { toast("请先上传购买证及运输证"); openCertDlg(false); }
  8700. return false;
  8701. }
  8702. const transSel = document.querySelector(`[data-wait-trans="${planId}"]`);
  8703. const vehSel = document.querySelector(`[data-wait-veh="${planId}"]`);
  8704. const shipSel = document.querySelector(`[data-wait-ship="${planId}"]`);
  8705. const procSel = document.querySelector(`[data-wait-procure="${planId}"]`);
  8706. const pick = waitPickOf(p);
  8707. const scene = planCertScene(p);
  8708. let c = pick.c;
  8709. if (planNeedProcureCert(p) && procSel && procSel.value) c = certs.find((x) => x.id === +procSel.value) || c;
  8710. else if (!planNeedProcureCert(p) && shipSel && shipSel.value) c = certs.find((x) => x.id === +shipSel.value) || c;
  8711. const shipC = (shipSel && shipSel.value && certs.find((x) => x.id === +shipSel.value)) || pick.shipC;
  8712. const t = (transSel && c && findTrans(c, +transSel.value)) || pick.t;
  8713. const plate = (vehSel && vehSel.value) || pick.plate;
  8714. if (!c || !t) {
  8715. if (!silent) {
  8716. toast(scene === "direct" ? "直销单请先匹配山峡采购证及运输证" : "请先匹配送货方购买证与运输证");
  8717. linkWaitPlan(planId);
  8718. }
  8719. return false;
  8720. }
  8721. if (planNeedProcureCert(p) && !shipC) {
  8722. if (!silent) toast("直销单还需关联送货方购买证");
  8723. return false;
  8724. }
  8725. const err = applyWaitLinkToPlan(p, c, t, plate, shipC);
  8726. if (err) {
  8727. if (!silent) toast(err);
  8728. return false;
  8729. }
  8730. delete waitLinkPicks[planId];
  8731. if (!silent) {
  8732. const after = afterLinkCalc(c, t, p.tractor, 0, p.id, shipC);
  8733. const flag = after.over ? ",已超证预警" : (after.warn ? `,余量已达自设预警量` : "");
  8734. const extra = planNeedProcureCert(p) && shipC ? `,送货方证 ${shipC.buyNo}` : "";
  8735. toast(`已关联 ${p.no} · ${planSceneLabel(scene)} → ${t.transNo}${p.tractor ? " · " + p.tractor : ""}${extra},本车运输计划量 ${fmtTon(p.vehPlanQty != null ? p.vehPlanQty : p.planQty)}${c.unit}${flag}`);
  8736. afterManualLink();
  8737. }
  8738. return true;
  8739. }
  8740. function afterManualLink() {
  8741. showMod("cert");
  8742. showCertView("list");
  8743. updateBadges();
  8744. setTimeout(() => {
  8745. if (waitLinkPlans().length) focusCertWait();
  8746. else focusCertVehWarn();
  8747. }, 40);
  8748. }
  8749. function confirmAllWaitLinks() {
  8750. const ids = waitLinkPlans().map((p) => p.id);
  8751. let n = 0;
  8752. ids.forEach((id) => {
  8753. const p = plans.find((x) => x.id === id);
  8754. const pick = waitPickOf(p || {});
  8755. if (!p || !pick.c || !pick.t || !pick.matchShip) return;
  8756. if (planCertScene(p) === "direct" && (!pick.matchProcure || !pick.shipC)) return;
  8757. if (confirmWaitLink(id, true)) n += 1;
  8758. });
  8759. if (!n) {
  8760. toast("没有已按客户、车辆自动匹配、可直接确认的计划");
  8761. return;
  8762. }
  8763. toast(`已人工关联 ${n} 条计划,已按购买证 / 运输证计算各车运输计划量`);
  8764. afterManualLink();
  8765. }
  8766. function linkWaitPlan(planId) {
  8767. const p = plans.find((x) => x.id === planId);
  8768. if (!p) return;
  8769. if (!certs.length) {
  8770. toast("请先上传购买证及运输证");
  8771. openCertDlg(false);
  8772. return;
  8773. }
  8774. const c = suggestCertForPlan(p) || certs[0];
  8775. if (!c) {
  8776. toast("没有可改选的购买证,请先上传证件");
  8777. showCertView("list");
  8778. return;
  8779. }
  8780. showCertView("detail", c.id);
  8781. openCertLinkPlanDlg(c.id, [p.id]);
  8782. }
  8783. function renderCertDetail() {
  8784. const c = certs.find((x) => x.id === currentCertId);
  8785. if (!c) { showCertView("list"); return; }
  8786. const list = transOf(c);
  8787. document.getElementById("navDetail").disabled = false;
  8788. document.getElementById("detailTitle").textContent = c.product + " · 购买证 / 运输证";
  8789. document.getElementById("detailSub").textContent =
  8790. `识别生成于 ${c.created} · 购买证 ${c.buyNo} · 运输证 ${list.length} 张`;
  8791. const r = remain(c);
  8792. const tr = transRemain(c);
  8793. const hold = holdingOf(c);
  8794. const done = deductedOf(c);
  8795. const unassignedAll = list.reduce((s, t) => s + unassignedTrans(t), 0);
  8796. const companies = [...new Set(list.map((t) => t.carrier).filter(Boolean))];
  8797. document.getElementById("detailStats").innerHTML = [
  8798. ["购买证数量", c.buyQty + " " + c.unit, "购买备案数量", "tone-buy"],
  8799. ["购买证剩余", (remainGap(c) < 0 ? remainGap(c).toFixed(2) : r) + " " + c.unit, `已完成扣减 ${fmtTon(done)} · 在途计划 ${fmtTon(hold)}`, "tone-buy"],
  8800. ["运输证张数", list.length + " 张", `运量合计 ${transTotal(c)} ${c.unit} · 未分派 ${unassignedAll} ${c.unit}`, "tone-trans"],
  8801. ["运输证剩余", tr + " " + c.unit, transWarn(c) ? `有运输证余量 ≤ 自设预警量` : `余量 ≤ 各证自设预警量时预警`, "tone-trans"]
  8802. ].map(([k, v, s, tone]) => `<div class="stat ${tone || ""}"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");
  8803. const warnItems = list.filter(transItemWarn);
  8804. const linked = linkedPlansOf(c);
  8805. const vehRows = vehicleRowsOf(c);
  8806. const waitN = waitLinkPlans().filter((p) => planMatchesCert(p, c)).length;
  8807. document.getElementById("certRelHint").innerHTML =
  8808. `① 已上传运输证 <em>${list.length}</em> 张 → ② 人工关联计划 <em>${linked.length}</em> 条(在途计划 ${fmtTon(hold)}${c.unit},已完成 ${fmtTon(done)}${c.unit})→ ③ 各车运输计划量 = 受购买证、运输证数量约束;完成后扣减实发量;余量 ≤ 自设预警量时预警。${waitN ? ` 另有 ${waitN} 条品名匹配计划待勾选。` : ""}`;
  8809. const formulaEl = document.getElementById("certQtyFormula");
  8810. if (formulaEl) {
  8811. const buyCls = remainGap(c) < -1e-6 ? "bad" : (buyWarn(c) ? "warn" : "");
  8812. formulaEl.innerHTML = [
  8813. `<span>购买证 <b>${fmtTon(c.buyQty)}</b>${c.unit} − 已完成 <b>${fmtTon(done)}</b>${c.unit} − 在途计划 <b>${fmtTon(hold)}</b>${c.unit} = 余量 <b class="${buyCls}">${fmtTon(remainGap(c))}</b>${c.unit}</span>`,
  8814. ...list.map((t) => {
  8815. const used = transItemPlan(t);
  8816. const shippedT = transItemDeduct(t);
  8817. const g = transItemGap(t);
  8818. const cls = g < -1e-6 ? "bad" : (transItemWarn(t) ? "warn" : "");
  8819. return `<span>运输证 ${t.transNo} <b>${fmtTon(t.transQty)}</b>${c.unit} − 已完成 <b>${fmtTon(shippedT)}</b>${c.unit} − 在途计划 <b>${fmtTon(used)}</b>${c.unit} = 余量 <b class="${cls}">${fmtTon(g)}</b>${c.unit}</span>`;
  8820. })
  8821. ].join("");
  8822. }
  8823. const warnCard = document.getElementById("certWarnQtyCard");
  8824. if (warnCard) {
  8825. warnCard.innerHTML = `<div class="card-h"><h2>余量预警设置</h2><span class="muted">余量 ≤ 该值时预警;默认按证件核准总量的 10%,可自行调整</span></div>
  8826. <div class="card-b"><table class="warn-qty-table">
  8827. <thead><tr><th>证件</th><th>证号</th><th>证载</th><th>当前余量</th><th>预警量</th></tr></thead>
  8828. <tbody>
  8829. <tr>
  8830. <td><span class="cert-tag buy">购买证</span></td>
  8831. <td class="cell-no">${c.buyNo || "—"}</td>
  8832. <td>${fmtTon(c.buyQty)} ${c.unit}</td>
  8833. <td>${fmtTon(remainGap(c))} ${c.unit}</td>
  8834. <td>${warnQtyField(c.id, null, c.unit)}</td>
  8835. </tr>
  8836. ${list.map((t) => `<tr>
  8837. <td><span class="cert-tag trans">运输证</span></td>
  8838. <td class="cell-no">${t.transNo || "—"}</td>
  8839. <td>${fmtTon(t.transQty)} ${c.unit}</td>
  8840. <td>${fmtTon(transItemGap(t))} ${c.unit}</td>
  8841. <td>${warnQtyField(c.id, t.id, c.unit)}</td>
  8842. </tr>`).join("")}
  8843. </tbody>
  8844. </table></div>`;
  8845. }
  8846. document.getElementById("detailWarns").innerHTML = [
  8847. remainGap(c) < -1e-6 ? `<div class="alert red">购买证关联计划量已超证载 ${(-remainGap(c)).toFixed(2)} ${c.unit}。</div>` : "",
  8848. buyWarn(c) && remainGap(c) >= 0 ? `<div class="alert red">购买证余量 ${fmtTon(remain(c))} ${c.unit} ≤ ${fmtTon(buyWarnQty(c))} 吨预警。</div>` : "",
  8849. ...warnItems.map((t) =>
  8850. `<div class="alert red">运输证 ${t.transNo} 余量 ${transItemRemain(t)} ${c.unit}(阈值 ${fmtTon(transWarnQty(t))} 吨)${transItemGap(t) < 0 ? ",已超证" : ",请尽快安排同公司车辆发运"}。</div>`
  8851. ),
  8852. ...vehRows.filter((r) => r.level === "over").map((r) =>
  8853. `<div class="alert red">${r.plate} 关联计划量 ${r.hold} ${c.unit},${r.text}。</div>`
  8854. )
  8855. ].filter(Boolean).join("");
  8856. document.getElementById("certVehicleWarn").innerHTML = vehRows.length ? `<table>
  8857. <thead><tr><th>车辆</th><th>驾驶员</th><th>占用运输证</th><th>关联的运输计划</th><th>两证可运上限</th><th>关联计划量</th><th>已完成扣减</th><th>本车余量</th><th>购买证余量</th><th>运输证余量</th><th>预警</th></tr></thead>
  8858. <tbody>${vehRows.map((r) => {
  8859. const t = findTrans(c, r.transId) || { transQty: 0 };
  8860. const leftVeh = (r.cap || 0) - r.hold;
  8861. const buyLeft = remainGap(c);
  8862. const transLeft = transItemGap(t);
  8863. const nPlan = vehLinkedPlans(r.plate, r.transId).length;
  8864. const plateArg = String(r.plate).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
  8865. return `<tr class="${r.level === "over" ? "veh-over" : ""}">
  8866. <td class="cell-no">${r.plate}</td>
  8867. <td>${r.driver || "—"}</td>
  8868. <td>${r.transNo}<div class="muted">${r.carrier || ""}</div></td>
  8869. <td><button class="plan-n-link${nPlan ? "" : " off"}" type="button" onclick="openVehPlanDlg(${c.id}, ${r.transId}, '${plateArg}')">${nPlan} 条</button></td>
  8870. <td>${fmtTon(r.bound || r.cap || 0)} ${c.unit}<div class="muted">分派 ${r.cap || 0}</div></td>
  8871. <td class="${r.level === "over" ? "staff-due" : ""}">${fmtTon(r.planHold || 0)} ${c.unit}${r.level === "over" ? ` <span class="need-tag">超</span>` : ""}</td>
  8872. <td>${fmtTon(r.shipped || 0)} ${c.unit}</td>
  8873. <td class="${leftVeh < -1e-6 ? "staff-due" : ""}">${leftVeh.toFixed(2)} ${c.unit}</td>
  8874. <td class="${buyLeft < -1e-6 ? "staff-due" : ""}">${buyLeft.toFixed(2)} ${c.unit}</td>
  8875. <td class="${transLeft < -1e-6 ? "staff-due" : ""}">${transLeft.toFixed(2)} ${c.unit}</td>
  8876. <td>${r.level ? `<span class="remain ${r.level === "over" ? "bad" : "warn"}">${r.text}</span>` : `<span class="remain">正常</span>`}</td>
  8877. </tr>`;
  8878. }).join("")}</tbody></table>` : `<div class="empty">上传运输证后会带出车号。人工勾选计划后,各车计划量对照购买证 / 运输证证载预警。</div>`;
  8879. document.getElementById("certLinkedPlans").innerHTML = linked.length ? `<table>
  8880. <thead><tr><th>计划编号</th><th>状态</th><th>品名</th><th>车辆</th><th>占用运输证</th><th>计划量</th><th>已完成扣减</th></tr></thead>
  8881. <tbody>${linked.map((p) => {
  8882. const t = findTrans(c, p.transId);
  8883. const over = t && p.tractor && vehicleOver({ plate: p.tractor, qty: ((t.dispatches || []).find((d) => d.plate === p.tractor) || {}).qty || 0 }, t);
  8884. const doneN = deductQty(p);
  8885. return `<tr class="${over ? "veh-over" : ""}">
  8886. <td class="cell-no">${p.no}</td>
  8887. <td>${planStHtml(p)}</td>
  8888. <td>${p.product}<div class="muted">${p.ctrl || ""}</div></td>
  8889. <td>${p.tractor || "未派车"}<div class="muted">${p.driver || ""}</div></td>
  8890. <td>${t ? t.transNo : "—"}</td>
  8891. <td class="num">${p.planQty} ${p.unit || c.unit}${over ? ` <span class="need-tag">超分派</span>` : ""}<div class="muted">两证可运 ${fmtTon(calcVehPlanQty(c, t, p.tractor, p).qty)}</div></td>
  8892. <td class="num">${doneN ? doneN + " " + (p.unit || c.unit) : "未扣减"}</td>
  8893. </tr>`;
  8894. }).join("")}</tbody></table>` : `<div class="empty">尚未关联计划。</div>`;
  8895. const transCards = list.map((t, i) => {
  8896. const left = transItemRemain(t);
  8897. const warn = transItemWarn(t);
  8898. const used = transItemHold(t);
  8899. const tPct = Math.min(100, (used / (Number(t.transQty || 1) || 1)) * 100);
  8900. return `<article class="card">
  8901. <div class="card-h">
  8902. <h2>${t.transNo || "未编号"}</h2>
  8903. <span class="chip ghost">${t.transFile || "已识别"}</span>
  8904. </div>
  8905. <div class="card-b"><div class="kv">
  8906. ${kv("编号", t.transNo)}${kv("核发日期", t.transDate)}
  8907. ${kv("货主", t.owner)}${kv("法定代表人", t.ownerLegal)}
  8908. ${kv("运输物品", c.product)}${kv("数量", (t.transQty || "—") + " " + c.unit)}
  8909. ${kv("预警量", warnQtyField(c.id, t.id, c.unit))}
  8910. ${kv("包装", t.pack)}${kv("购买许可证号", t.permitNo)}
  8911. ${kv("运输单位", t.carrier)}${kv("运输方式", t.mode)}
  8912. ${kv("车号(次)", t.plates)}${kv("收货单位", t.consignee)}
  8913. ${kv("运输路线", t.route)}${kv("申请单位", t.applicantUnit)}
  8914. ${kv("有效期限", (t.transFrom || "") + " 至 " + (t.transDue || ""))}
  8915. ${kv("公安机关", t.transPolice)}${kv("经办人", t.transHandler)}
  8916. </div>
  8917. <div style="margin-top:14px;color:var(--muted);font-size:12px">本证进度(已完成 ${transItemDeduct(t)} + 在途计划 ${transItemPlan(t)} / 运输证 ${t.transQty || 0})${warn ? ` · 余量 ${left} 吨预警` : ""}</div>
  8918. <div class="qty-bar"><div class="qty-fill${warn ? " warn" : ""}" style="width:${tPct}%"></div></div>
  8919. ${certThumbHtml(t.transUrl, "运输证")}
  8920. </div>
  8921. </article>`;
  8922. }).join("");
  8923. const transBlock = transCards
  8924. ? `<div class="trans-card-row">${transCards}</div>`
  8925. : `<article class="card"><div class="card-b"><div class="empty">尚未识别运输证,请点击「再识别一张运输证」。</div></div></article>`;
  8926. document.getElementById("detailSheet").innerHTML = `
  8927. <div class="cert-duo">
  8928. <div class="cert-pane buy">
  8929. <div class="cert-pane-h">
  8930. <span class="cert-tag buy">购买证</span>
  8931. <h2>购买备案证明</h2>
  8932. </div>
  8933. <article class="card">
  8934. <div class="card-h"><h2>${c.buyNo || "未编号"}</h2><span class="chip ghost">${c.buyFile || "已识别"}</span></div>
  8935. <div class="card-b"><div class="kv">
  8936. ${kv("编号", c.buyNo)}${kv("核发日期", c.buyDate)}
  8937. ${kv("购买单位", c.buyer)}${kv("法定代表人", c.buyerLegal)}
  8938. ${kv("电话", c.buyerTel)}${kv("住所", c.buyerAddr)}
  8939. ${kv("销售单位", c.seller)}${kv("销售方法人", c.sellerLegal)}
  8940. ${kv("购买物品", c.product)}${kv("用途", c.usage)}
  8941. ${kv("数量", c.buyQty + " " + c.unit)}${kv("预警量", warnQtyField(c.id, null, c.unit))}
  8942. ${kv("有效次数", c.buyTimes)}
  8943. ${kv("有效期限", (c.buyFrom || "") + " 至 " + (c.buyDue || ""))}
  8944. ${kv("公安机关", c.buyPolice)}${kv("经办人", c.buyHandler)}
  8945. </div>${certThumbHtml(c.buyUrl, "购买证")}</div>
  8946. </article>
  8947. </div>
  8948. <div class="cert-pane trans">
  8949. <div class="cert-pane-h">
  8950. <span class="cert-tag trans">运输证</span>
  8951. <h2>运输备案证明</h2>
  8952. <span class="muted">${list.length} 张</span>
  8953. </div>
  8954. ${transBlock}
  8955. </div>
  8956. </div>`;
  8957. document.getElementById("dispatchHint").textContent =
  8958. `本购买证挂 ${list.length} 张运输证${companies.length ? "(" + companies.join("、") + ")" : ""}。按各运输证数量分别分派同公司车辆;运输证合计剩余 ${tr}${c.unit}。`;
  8959. document.getElementById("detailDispatch").innerHTML = list.length ? list.map((t) => {
  8960. const cars = t.dispatches || [];
  8961. const left = transItemRemain(t);
  8962. return `<div style="margin-bottom:16px">
  8963. <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px">
  8964. <strong><span class="cert-tag trans" style="margin-right:6px">运输证</span>${t.transNo}</strong>
  8965. <span class="muted">${t.carrier || "—"} · 运量 ${t.transQty || 0}${c.unit} · 已分 ${assignedSum(t)}${c.unit} · 未分 ${unassignedTrans(t)}${c.unit}${transItemWarn(t) ? ` · 余 ${left} 吨预警` : ""}</span>
  8966. <span style="display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)">预警量 ${warnQtyField(c.id, t.id, c.unit)}</span>
  8967. <div class="actions">
  8968. <button class="btn btn-sm" type="button" onclick="splitTransToVehicles(${c.id}, ${t.id})">按本证均分</button>
  8969. <button class="btn btn-sm btn-primary" type="button" onclick="openDispatchDlg(${t.id})">分派车辆</button>
  8970. </div>
  8971. </div>
  8972. ${cars.length ? `<table>
  8973. <thead><tr><th>车牌</th><th>驾驶员</th><th>运输公司</th><th>分派运量</th><th>关联计划量</th><th>已运</th><th>对照预警</th></tr></thead>
  8974. <tbody>${cars.map((d, i) => {
  8975. const hold = vehicleHold(d.plate, t.id);
  8976. const planHold = vehiclePlanHold(d.plate, t.id);
  8977. const shipped = vehicleDeduct(d.plate, t.id);
  8978. const over = vehicleOver(d, t);
  8979. const alert = vehicleAlertOf(c, t, hold, Number(d.qty || 0), planHold, shipped);
  8980. return `<tr class="${over ? "veh-over" : ""}">
  8981. <td class="cell-no">${d.plate}</td><td>${d.driver || "—"}</td>
  8982. <td>${t.carrier || "—"}</td>
  8983. <td><input type="number" step="0.01" min="${shipped}" value="${d.qty}" style="width:88px;height:28px;border:1px solid var(--line);border-radius:6px;padding:0 8px"
  8984. onchange="updateDispatchQty(${t.id}, ${i}, this.value)" /></td>
  8985. <td class="${over ? "staff-due" : ""}">${fmtTon(planHold)} ${c.unit}${over ? ` <span class="need-tag">超</span>` : ""}</td>
  8986. <td>${fmtTon(shipped)} ${c.unit}</td>
  8987. <td>${alert.level ? `<span class="remain ${alert.level === "over" ? "bad" : "warn"}">${alert.text}</span>` : `<span class="remain">正常</span>`}</td>
  8988. </tr>`;
  8989. }).join("")}</tbody>
  8990. </table>` : `<div class="empty" style="padding:16px 0">本运输证尚未分派车辆。证件上的车号会在上传时自动带入。</div>`}
  8991. </div>`;
  8992. }).join("") : `<div class="empty">请先识别运输证,再按证分派车辆。</div>`;
  8993. updateBadges();
  8994. }
  8995. function fillCertForm(data) {
  8996. const form = document.getElementById("certForm");
  8997. Object.entries(data).forEach(([k, v]) => {
  8998. const el = form.querySelector(`[name="${k}"]`);
  8999. if (el) el.value = v;
  9000. });
  9001. }
  9002. function setOcrStatus(kind, text, ok) {
  9003. const el = document.getElementById(kind + "Status");
  9004. if (!el) return;
  9005. el.textContent = text;
  9006. el.classList.toggle("ok", !!ok);
  9007. }
  9008. function transSlotHtml(n) {
  9009. return `<div class="extra-trans" data-slot="${n}">
  9010. <h4><span class="cert-tag trans">运输证</span> 运输证信息(第 ${n + 1} 张)
  9011. <button class="btn btn-sm" type="button" style="margin-left:auto" onclick="this.closest('.extra-trans').remove()">移除</button></h4>
  9012. <label class="upload" style="margin-bottom:10px">
  9013. <input type="file" accept="image/*,.pdf" onchange="recognizeExtraTrans(this)" />
  9014. <span class="plus">+</span><span>上传本张运输证</span>
  9015. </label>
  9016. <img class="ocr-preview hidden extra-preview" alt="运输证预览" />
  9017. <div class="form trans-fields" style="grid-template-columns:1fr 1fr">
  9018. <label><span><span class="star">*</span>编号</span><input name="transNo" required /></label>
  9019. <label>核发日期<input name="transDate" type="date" /></label>
  9020. <label class="span2">货主<input name="owner" /></label>
  9021. <label>法定代表人<input name="ownerLegal" /></label>
  9022. <label>电话<input name="ownerTel" /></label>
  9023. <label class="span2">货主住所<input name="ownerAddr" /></label>
  9024. <label>包装<input name="pack" /></label>
  9025. <label>购买许可/备案证号<input name="permitNo" /></label>
  9026. <label class="span2">运输单位<input name="carrier" /></label>
  9027. <label>住所/电话<input name="carrierAddr" /></label>
  9028. <label>运输方式<input name="mode" /></label>
  9029. <label>车号(次)<input name="plates" /></label>
  9030. <label class="span2">运输路线<input name="route" /></label>
  9031. <label class="span2">收货单位<input name="consignee" /></label>
  9032. <label>运输数量<input name="transQty" type="number" step="0.01" /></label>
  9033. <label>余量预警<input name="warnQty" type="number" step="0.01" min="0" placeholder="核准总量 10%" title="默认按证件核准总量的 10% 预警,可改" /></label>
  9034. <label>有效次数<input name="transTimes" /></label>
  9035. <label>有效开始<input name="transFrom" type="date" /></label>
  9036. <label>有效截止<input name="transDue" type="date" /></label>
  9037. <label class="span2">申请单位<input name="applicantUnit" /></label>
  9038. <label class="span2">公安机关<input name="transPolice" /></label>
  9039. <label>经办人<input name="transHandler" /></label>
  9040. </div>
  9041. </div>`;
  9042. }
  9043. function addTransSlot() {
  9044. extraTransN += 1;
  9045. document.getElementById("extraTransSlots").insertAdjacentHTML("beforeend", transSlotHtml(extraTransN));
  9046. const box = document.querySelector(`#extraTransSlots .extra-trans[data-slot="${extraTransN}"]`);
  9047. bindExtraTransDrop(box);
  9048. }
  9049. function bindCertDrop(el, kind) {
  9050. if (!el || el.dataset.dropBound) return;
  9051. el.dataset.dropBound = "1";
  9052. el.addEventListener("dragover", (e) => { e.preventDefault(); el.classList.add("drag"); });
  9053. el.addEventListener("dragleave", () => el.classList.remove("drag"));
  9054. el.addEventListener("drop", (e) => {
  9055. e.preventDefault();
  9056. el.classList.remove("drag");
  9057. const file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
  9058. if (!file) return;
  9059. const input = document.getElementById(kind === "buy" ? "buyFile" : "transFile");
  9060. if (!input) return;
  9061. const dt = new DataTransfer();
  9062. dt.items.add(file);
  9063. input.files = dt.files;
  9064. recognize(kind, input);
  9065. });
  9066. }
  9067. function bindExtraTransDrop(box) {
  9068. if (!box || box.dataset.dropBound) return;
  9069. box.dataset.dropBound = "1";
  9070. box.addEventListener("dragover", (e) => { e.preventDefault(); box.classList.add("drag"); });
  9071. box.addEventListener("dragleave", () => box.classList.remove("drag"));
  9072. box.addEventListener("drop", (e) => {
  9073. e.preventDefault();
  9074. box.classList.remove("drag");
  9075. const file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
  9076. const input = box.querySelector("input[type=file]");
  9077. if (!file || !input) return;
  9078. const dt = new DataTransfer();
  9079. dt.items.add(file);
  9080. input.files = dt.files;
  9081. recognizeExtraTrans(input);
  9082. });
  9083. }
  9084. function recognize(kind, input) {
  9085. const file = input.files && input.files[0];
  9086. if (!file) return;
  9087. certFiles[kind] = file.name;
  9088. const preview = document.getElementById(kind === "buy" ? "buyPreview" : "transPreview");
  9089. const reader = new FileReader();
  9090. reader.onload = () => {
  9091. preview.src = reader.result;
  9092. preview.classList.remove("hidden");
  9093. certFiles[kind + "Url"] = reader.result;
  9094. };
  9095. reader.readAsDataURL(file);
  9096. setOcrStatus(kind, "正在识别…", false);
  9097. setTimeout(() => {
  9098. if (kind === "buy") {
  9099. fillFields(document.getElementById("buyFormCol"), ocrBuySample);
  9100. ensureWarnQtyInputs(document.getElementById("buyFormCol"));
  9101. } else {
  9102. fillFields(document.getElementById("transForm0"), ocrTransSample);
  9103. ensureWarnQtyInputs(document.getElementById("transForm0"));
  9104. }
  9105. setOcrStatus(kind, "识别完成,请核对后保存", true);
  9106. toast(kind === "buy" ? "购买证已识别" : "运输证已识别");
  9107. }, 600);
  9108. }
  9109. function recognizeExtraTrans(input) {
  9110. const file = input.files && input.files[0];
  9111. const box = input.closest(".extra-trans");
  9112. if (!file || !box) return;
  9113. const preview = box.querySelector(".extra-preview");
  9114. const reader = new FileReader();
  9115. reader.onload = () => {
  9116. preview.src = reader.result;
  9117. preview.classList.remove("hidden");
  9118. box.dataset.file = file.name;
  9119. box.dataset.url = reader.result;
  9120. };
  9121. reader.readAsDataURL(file);
  9122. setTimeout(() => {
  9123. fillFields(box, ocrTransSamples[extraTransN % ocrTransSamples.length]);
  9124. ensureWarnQtyInputs(box);
  9125. toast("运输证已识别");
  9126. }, 500);
  9127. }
  9128. function setBuyFieldsRequired(on) {
  9129. ["buyNo", "buyer", "product", "buyQty"].forEach((name) => {
  9130. const el = document.querySelector(`#buyFormCol [name="${name}"]`);
  9131. if (el) el.required = on;
  9132. });
  9133. }
  9134. function openCertDlg(demo, buyId) {
  9135. const form = document.getElementById("certForm");
  9136. form.reset();
  9137. form.querySelector("[name=unit]").value = "吨";
  9138. ensureWarnQtyInputs(form);
  9139. attachBuyId = buyId || null;
  9140. extraTransN = 0;
  9141. document.getElementById("extraTransSlots").innerHTML = "";
  9142. certFiles.buy = certFiles.trans = null;
  9143. certFiles.buyUrl = certFiles.transUrl = "";
  9144. ["buyPreview", "transPreview"].forEach((id) => {
  9145. document.getElementById(id).classList.add("hidden");
  9146. document.getElementById(id).removeAttribute("src");
  9147. });
  9148. document.getElementById("buyFile").value = "";
  9149. document.getElementById("transFile").value = "";
  9150. const appending = !!attachBuyId;
  9151. document.getElementById("buyOcrBox").classList.toggle("hidden", appending);
  9152. document.getElementById("buyFormCol").classList.toggle("hidden", appending);
  9153. setBuyFieldsRequired(!appending);
  9154. document.getElementById("certDlgTitle").textContent = appending ? "追加运输证" : "上传购买证及运输证";
  9155. document.getElementById("certDlgHint").textContent = appending
  9156. ? "在已有购买证下继续上传运输证,车号将自动进入分派。"
  9157. : "先上传购买证和运输证并核对数量、车号;保存后再人工勾选对应运输计划。";
  9158. document.getElementById("certSaveBtn").textContent = appending ? "追加运输证" : "保存证件";
  9159. if (demo) {
  9160. fillFields(document.getElementById("buyFormCol"), ocrBuySample);
  9161. fillFields(document.getElementById("transForm0"), ocrTransSample);
  9162. certFiles.buy = "购买备案证明样例.png";
  9163. certFiles.trans = "运输备案证明样例.png";
  9164. certFiles.buyUrl = "购买备案证明样例.png";
  9165. certFiles.transUrl = "运输备案证明样例.png";
  9166. const bp = document.getElementById("buyPreview");
  9167. const tp = document.getElementById("transPreview");
  9168. bp.src = certFiles.buyUrl; bp.classList.remove("hidden");
  9169. tp.src = certFiles.transUrl; tp.classList.remove("hidden");
  9170. setOcrStatus("buy", "已填入购买备案证明样例", true);
  9171. setOcrStatus("trans", "已填入运输备案证明样例", true);
  9172. } else {
  9173. setOcrStatus("buy", appending ? "沿用已有购买证" : "待上传", appending);
  9174. setOcrStatus("trans", "待上传", false);
  9175. }
  9176. document.getElementById("certDialog").showModal();
  9177. }
  9178. function closeCertDlg() {
  9179. document.getElementById("certDialog").close();
  9180. attachBuyId = null;
  9181. setBuyFieldsRequired(true);
  9182. }
  9183. function transDraftFromFields(f, file, url) {
  9184. return {
  9185. id: ++transSeq, created: today, shipped: 0,
  9186. transNo: f.transNo, transDate: f.transDate, owner: f.owner, ownerLegal: f.ownerLegal,
  9187. ownerTel: f.ownerTel, ownerAddr: f.ownerAddr, pack: f.pack, permitNo: f.permitNo,
  9188. carrier: f.carrier, carrierAddr: f.carrierAddr, mode: f.mode, plates: f.plates,
  9189. route: f.route, consignee: f.consignee, transQty: f.transQty === "" ? 0 : +f.transQty,
  9190. transTimes: f.transTimes, transFrom: f.transFrom, transDue: f.transDue,
  9191. applicantUnit: f.applicantUnit, transPolice: f.transPolice, transHandler: f.transHandler,
  9192. warnQty: warnQtyOf(f.warnQty, f.transQty),
  9193. transFile: file || "运输证.jpg", transUrl: url || "", dispatches: []
  9194. };
  9195. }
  9196. function collectTransDrafts() {
  9197. const list = [];
  9198. const first = fieldsOf(document.getElementById("transForm0"));
  9199. if ((first.transNo || "").trim()) {
  9200. list.push(transDraftFromFields(first, certFiles.trans, certFiles.transUrl));
  9201. }
  9202. document.querySelectorAll("#extraTransSlots .extra-trans").forEach((box) => {
  9203. const f = fieldsOf(box);
  9204. if (!(f.transNo || "").trim()) return;
  9205. list.push(transDraftFromFields(f, box.dataset.file, box.dataset.url));
  9206. });
  9207. return list;
  9208. }
  9209. function saveCert() {
  9210. const form = document.getElementById("certForm");
  9211. if (!form.reportValidity()) return;
  9212. const transDrafts = collectTransDrafts();
  9213. if (!transDrafts.length) {
  9214. toast("请至少填写一张运输证编号");
  9215. return;
  9216. }
  9217. const hasTransFile = !!(certFiles.trans || certFiles.transUrl) ||
  9218. [...document.querySelectorAll("#extraTransSlots .extra-trans")].some((b) => b.dataset.file || b.dataset.url);
  9219. if (!attachBuyId && !(certFiles.buy || certFiles.buyUrl)) {
  9220. toast("请先上传购买证");
  9221. return;
  9222. }
  9223. if (!hasTransFile) {
  9224. toast("请先上传运输证");
  9225. return;
  9226. }
  9227. transDrafts.forEach(seedDispatchesOnTrans);
  9228. if (attachBuyId) {
  9229. const c = certs.find((x) => x.id === attachBuyId);
  9230. if (!c) return;
  9231. (c.transList ||= []).push(...transDrafts);
  9232. closeCertDlg();
  9233. toast(`已追加 ${transDrafts.length} 张运输证,可在本页设置预警量并分派车辆`);
  9234. showMod("cert");
  9235. showCertView("detail", c.id);
  9236. return;
  9237. }
  9238. const buy = fieldsOf(document.getElementById("buyFormCol"));
  9239. const rec = {
  9240. ...buy, id: ++certSeq, created: today, shipped: 0,
  9241. buyQty: +buy.buyQty, unit: buy.unit || "吨",
  9242. warnQty: warnQtyOf(buy.warnQty, buy.buyQty),
  9243. ctrl: /硝|铵|炸药/.test(buy.product || "") ? "易制爆" : "易制毒",
  9244. buyFile: certFiles.buy || "购买证.jpg",
  9245. buyUrl: certFiles.buyUrl || "",
  9246. transList: transDrafts
  9247. };
  9248. certs.unshift(rec);
  9249. closeCertDlg();
  9250. showMod("cert");
  9251. showCertView("detail", rec.id);
  9252. const matchIds = waitLinkPlans().filter((p) => planMatchesCert(p, rec)).map((p) => p.id);
  9253. toast(matchIds.length
  9254. ? `已保存。可在本页设置预警量、分派车辆;有 ${matchIds.length} 条计划可关联`
  9255. : "已保存。可在本页设置购买证 / 运输证预警量并分派车辆");
  9256. }
  9257. function seedDispatchesOnTrans(t) {
  9258. const raw = String(t.plates || "").split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean);
  9259. const company = t.carrier || "";
  9260. const seen = new Set();
  9261. t.dispatches = [];
  9262. raw.forEach((plate) => {
  9263. if (seen.has(plate)) return;
  9264. seen.add(plate);
  9265. let meta = carrierFleet.find((v) => v.plate === plate);
  9266. if (!meta) {
  9267. meta = { company: company || "未登记运输公司", plate, driver: "", cap: 32 };
  9268. if (company) carrierFleet.push(meta);
  9269. }
  9270. if (company && meta.company && meta.company !== company) return;
  9271. t.dispatches.push({ plate, driver: meta.driver || "", qty: 0, shipped: 0 });
  9272. });
  9273. if (t.dispatches.length) splitOneTrans(t, true);
  9274. }
  9275. function activeTrans() {
  9276. const c = certs.find((x) => x.id === currentCertId);
  9277. return c ? findTrans(c, currentTransId) : null;
  9278. }
  9279. function openDispatchDlg(transId) {
  9280. const c = certs.find((x) => x.id === currentCertId);
  9281. if (!c) return;
  9282. currentTransId = transId;
  9283. const t = findTrans(c, transId);
  9284. if (!t) return;
  9285. const form = document.getElementById("dispatchForm");
  9286. form.reset();
  9287. form.querySelector("[name=company]").value = t.carrier || "";
  9288. const used = new Set((t.dispatches || []).map((d) => d.plate));
  9289. let pool = sameCompanyCars(t.carrier).filter((v) => !used.has(v.plate));
  9290. if (!pool.length) pool = carrierFleet.filter((v) => !used.has(v.plate) && (!t.carrier || v.company === t.carrier));
  9291. const sel = form.querySelector("[name=plate]");
  9292. sel.innerHTML = pool.map((v) => `<option value="${v.plate}">${v.plate} · ${v.driver}(核定 ${v.cap} 吨)</option>`).join("")
  9293. + `<option value="__new__">录入同公司其他车辆…</option>`;
  9294. if (!pool.length) sel.value = "__new__";
  9295. form.querySelector("[name=qty]").value = unassignedTrans(t) || "";
  9296. document.getElementById("dispatchDlgHint").textContent =
  9297. `「${t.carrier || "运输公司"}」车辆共用运输证 ${t.transNo}。证载 ${t.transQty || 0}${c.unit},未分派 ${unassignedTrans(t)}${c.unit},计划占用 ${transItemHold(t)}${c.unit}。`;
  9298. syncDispatchDriver();
  9299. document.getElementById("dispatchDialog").showModal();
  9300. }
  9301. function closeDispatchDlg() { document.getElementById("dispatchDialog").close(); }
  9302. function syncDispatchDriver() {
  9303. const plate = document.querySelector("#dispatchForm [name=plate]").value;
  9304. const isNew = plate === "__new__";
  9305. document.getElementById("dispatchNewPlateRow").classList.toggle("hidden", !isNew);
  9306. document.querySelector("#dispatchForm [name=newPlate]").required = isNew;
  9307. const v = carrierFleet.find((x) => x.plate === plate);
  9308. if (v) document.querySelector("#dispatchForm [name=driver]").value = v.driver;
  9309. }
  9310. function saveDispatch() {
  9311. const c = certs.find((x) => x.id === currentCertId);
  9312. const t = activeTrans();
  9313. const form = document.getElementById("dispatchForm");
  9314. if (!c || !t || !form.reportValidity()) return;
  9315. let plate = form.querySelector("[name=plate]").value;
  9316. if (plate === "__new__") {
  9317. plate = (form.querySelector("[name=newPlate]").value || "").trim();
  9318. if (!plate) {
  9319. toast("请填写同公司车牌");
  9320. return;
  9321. }
  9322. if (!carrierFleet.some((x) => x.plate === plate)) {
  9323. carrierFleet.push({ company: t.carrier || "", plate, driver: form.querySelector("[name=driver]").value, cap: 32 });
  9324. }
  9325. }
  9326. const driver = form.querySelector("[name=driver]").value;
  9327. const qty = +form.querySelector("[name=qty]").value;
  9328. const v = carrierFleet.find((x) => x.plate === plate);
  9329. if (v && t.carrier && v.company !== t.carrier) {
  9330. toast("只能分派同一运输公司的车辆,以便共用一张运输证");
  9331. return;
  9332. }
  9333. if ((t.dispatches || []).some((d) => d.plate === plate)) {
  9334. toast("该车已在本运输证下");
  9335. return;
  9336. }
  9337. if (qty - unassignedTrans(t) > 1e-6) {
  9338. toast(`分派量超过运输证未分派量 ${unassignedTrans(t)} ${c.unit}`);
  9339. return;
  9340. }
  9341. (t.dispatches ||= []).push({ plate, driver, qty, shipped: 0 });
  9342. t.plates = t.dispatches.map((d) => d.plate).join("、");
  9343. closeDispatchDlg();
  9344. toast(`已将 ${qty}${c.unit} 分派给 ${plate}`);
  9345. renderCertDetail();
  9346. }
  9347. function splitOneTrans(t, silent) {
  9348. if (!t || !(t.dispatches || []).length) {
  9349. if (!silent) toast("请先分派车辆");
  9350. return false;
  9351. }
  9352. const n = t.dispatches.length;
  9353. const total = Number(t.transQty || 0);
  9354. const base = Math.floor((total / n) * 100) / 100;
  9355. let acc = 0;
  9356. t.dispatches.forEach((d, i) => {
  9357. d.qty = i === n - 1 ? Math.round((total - acc) * 100) / 100 : base;
  9358. acc += d.qty;
  9359. if (Number(d.shipped) > Number(d.qty)) d.shipped = d.qty;
  9360. });
  9361. return true;
  9362. }
  9363. function splitTransToVehicles(certId, transId) {
  9364. const c = certs.find((x) => x.id === (certId || currentCertId));
  9365. const t = c ? findTrans(c, transId) : null;
  9366. if (!c || !t) return;
  9367. if (!splitOneTrans(t, false)) return;
  9368. toast(`已按运输证 ${t.transQty}${c.unit} 均分到 ${t.dispatches.length} 辆同公司车辆`);
  9369. renderCertDetail();
  9370. }
  9371. function updateDispatchQty(transId, index, value) {
  9372. const c = certs.find((x) => x.id === currentCertId);
  9373. const t = c ? findTrans(c, transId) : null;
  9374. if (!c || !t || !t.dispatches[index]) return;
  9375. const d = t.dispatches[index];
  9376. const qty = Math.max(Number(d.shipped || 0), +value || 0);
  9377. const others = assignedSum(t) - Number(d.qty);
  9378. if (others + qty - Number(t.transQty || 0) > 1e-6) {
  9379. toast(`分派合计不能超过运输证 ${t.transQty} ${c.unit}`);
  9380. renderCertDetail();
  9381. return;
  9382. }
  9383. d.qty = qty;
  9384. toast(`${d.plate} 分派运量已改为 ${qty} ${c.unit}`);
  9385. renderCertDetail();
  9386. }
  9387. function pickVehicleForTrans(t, needQty, plate) {
  9388. const list = (t.dispatches || []).slice();
  9389. if (plate) {
  9390. const hit = list.find((d) => d.plate === plate);
  9391. if (hit) return hit;
  9392. }
  9393. list.sort((a, b) =>
  9394. (Number(b.qty) - vehicleHold(b.plate, t.id)) - (Number(a.qty) - vehicleHold(a.plate, t.id)));
  9395. return list.find((d) => Number(d.qty) - vehicleHold(d.plate, t.id) + 1e-9 >= needQty) || list[0] || null;
  9396. }
  9397. function pickVehicleForPlan(cert, needQty, transId, plate) {
  9398. const t = transId ? findTrans(cert, transId) : transOf(cert)[0];
  9399. if (!t) return null;
  9400. return pickVehicleForTrans(t, needQty, plate);
  9401. }
  9402. function applyShipToTrans(t, plate, qty) {
  9403. if (!t || !(t.dispatches || []).length) return;
  9404. let left = qty;
  9405. const hit = t.dispatches.find((d) => d.plate === plate);
  9406. const order = hit ? [hit, ...t.dispatches.filter((d) => d !== hit)] : t.dispatches;
  9407. order.forEach((d) => {
  9408. if (left <= 0) return;
  9409. const room = Math.max(0, Number(d.qty) - Number(d.shipped || 0));
  9410. const take = Math.min(room, left);
  9411. d.shipped = Number(d.shipped || 0) + take;
  9412. left -= take;
  9413. });
  9414. }
  9415. function applyShipToDispatch(cert, plate, qty, transId) {
  9416. const t = transId ? findTrans(cert, transId) : transOf(cert)[0];
  9417. applyShipToTrans(t, plate, qty);
  9418. }
  9419. function planCrew(p) {
  9420. const names = [p.driver, p.escort].filter(Boolean);
  9421. return names.length ? names.join(" / ") : "—";
  9422. }
  9423. function planDeliverAt(p) {
  9424. const d = p.deliverDate || p.date;
  9425. if (!d) return "—";
  9426. return p.deliverTime ? `${d} ${p.deliverTime}` : d;
  9427. }
  9428. function planContractNo(p) {
  9429. return p.contractNo || "—";
  9430. }
  9431. function planStHtml(p) {
  9432. const s = typeof p === "string" ? p : p.status;
  9433. if (typeof p === "object" && p.waitNotify && s === "待安排") return `<span class="st-pill wait">等客户通知</span>`;
  9434. if (typeof p === "object" && p.archived) return `<span class="st-pill arch">已归档</span>`;
  9435. if (s === "已完成") return `<span class="st-pill fin">已完成</span>`;
  9436. if (s === "已安排") return `<span class="st-pill ok">已安排</span>`;
  9437. if (s === "已作废") return `<span class="st-pill void">已作废</span>`;
  9438. if (s === "草稿") return `<span class="st-pill">草稿</span>`;
  9439. if (s === "待安排") return `<span class="st-pill wait">待安排</span>`;
  9440. return `<span class="st-pill wait">${s || "待安排"}</span>`;
  9441. }
  9442. function planTabMatch(p, tab) {
  9443. if (tab === "待安排") return p.status === "待安排";
  9444. if (tab === "已安排") return p.status === "已安排";
  9445. if (tab === "草稿中") return p.status === "草稿";
  9446. if (tab === "入库单") return p.type === "入直单" || p.type === "入库单";
  9447. if (tab === "出库单") return p.type === "出库单";
  9448. if (tab === "直销单") return p.type === "直销单";
  9449. return true;
  9450. }
  9451. function planTabStatus(tab) {
  9452. if (tab === "待处理" || tab === "待安排") return "待安排";
  9453. if (tab === "审核中" || tab === "已安排") return "已安排";
  9454. if (tab === "草稿中") return "草稿";
  9455. if (tab === "已完成") return "已完成";
  9456. if (tab === "已作废") return "已作废";
  9457. return "";
  9458. }
  9459. function productCell(p) {
  9460. const tags = [];
  9461. if (p.shipMode === "槽车运输" && p.product !== "槽车运输") tags.push(`<span class="prod-tag orange">槽车运输</span>`);
  9462. if (p.ctrl === "易制毒") tags.push(`<span class="prod-tag red">易制毒</span>`);
  9463. if (p.ctrl === "易制爆") tags.push(`<span class="prod-tag orange">易制爆</span>`);
  9464. const name = p.product === "槽车运输" ? `<span class="prod-tag orange">槽车运输</span>` : (p.product || "—");
  9465. return `${name}${tags.join("")}`;
  9466. }
  9467. function matchCell(p) {
  9468. if (p.status === "已作废") return "—";
  9469. if (p.certId) return `<span class="match-tag yes">有匹配 [3]</span> <span class="match-tag ok">已匹配</span>`;
  9470. if (p.ctrl) return `<span class="match-tag yes">有匹配 [3]</span>`;
  9471. if (p.status === "已完成") return `<span class="match-tag yes">有匹配 [3]</span> <span class="match-tag ok">已匹配</span>`;
  9472. return "—";
  9473. }
  9474. function sourceCell(p) {
  9475. const cert = certs.find((c) => c.id === p.certId);
  9476. return `<div class="src-cell">
  9477. <span class="ico"><span class="src-dot">${(p.source || "手").slice(0, 1)}</span>${p.sourceNo || p.no}</span>
  9478. <span>${cert ? cert.buyNo : p.no}</span>
  9479. </div>`;
  9480. }
  9481. function planOps(p) {
  9482. if (p.status === "已作废") return "";
  9483. const taskBtn = `<button class="op-link" type="button" onclick="openPlanTaskDlg(${p.id})">${p.taskNo ? "查看任务单" : "下发"}</button>`;
  9484. if (p.status === "待安排") {
  9485. const extra = (p.ctrl && !p.certId) ? `<button class="op-link" type="button" onclick="openLinkCertDlg(${p.id})">关联</button>` : "";
  9486. return `${extra}<button class="op-link" type="button" onclick="arrangePlan(${p.id})">审核</button>${taskBtn}`;
  9487. }
  9488. if (p.status === "已安排") {
  9489. return `<button class="op-link" type="button" onclick="unapprovePlan(${p.id})">反审核</button><button class="op-link" type="button" onclick="completePlan(${p.id})">完成</button>${taskBtn}`;
  9490. }
  9491. if (p.status === "已完成" && !p.archived) {
  9492. return `<button class="op-link" type="button" onclick="showMod('archive')">去归档审核</button>${taskBtn}`;
  9493. }
  9494. return `<button class="op-link" type="button" onclick="showMod('archive')">查看归档</button>${taskBtn}`;
  9495. }
  9496. function selectedPlans() {
  9497. return plans.filter((p) => selectedPlanIds.has(p.id));
  9498. }
  9499. function togglePlanSel(id, on) {
  9500. if (on) selectedPlanIds.add(id);
  9501. else selectedPlanIds.delete(id);
  9502. const row = document.querySelector(`#planTable tbody input[data-id="${id}"]`)?.closest("tr");
  9503. if (row) row.classList.toggle("sel", on);
  9504. }
  9505. function togglePlanSelAll(on) {
  9506. document.querySelectorAll("#planTable tbody input[type=checkbox]").forEach((el) => {
  9507. el.checked = on;
  9508. togglePlanSel(+el.dataset.id, on);
  9509. });
  9510. }
  9511. function isWaitPrecursorPlan(p) {
  9512. return p.ctrl === "易制毒" && !p.certId && p.status !== "已完成" && p.status !== "已作废";
  9513. }
  9514. function isTomorrowWaitPlan(p) {
  9515. if (!p || p.status !== "待安排") return false;
  9516. const tom = dayShift(today, 1);
  9517. return p.date === tom || p.deliverDate === tom;
  9518. }
  9519. function isTodayPlan(p) {
  9520. if (!p || p.status === "已作废") return false;
  9521. return p.date === today || p.deliverDate === today;
  9522. }
  9523. function isTodayDonePlan(p) {
  9524. return isTodayPlan(p) && p.status === "已完成";
  9525. }
  9526. function isTodayOpenPlan(p) {
  9527. return isTodayPlan(p) && p.status !== "已完成";
  9528. }
  9529. function isWaitReviewPrecursorPlan(p) {
  9530. return !!(p && p.ctrl === "易制毒" && p.status === "待安排");
  9531. }
  9532. function isWaitArchivePlan(p) {
  9533. return !!(p && p.status === "已完成" && !p.archived);
  9534. }
  9535. function planCardMatch(p, key) {
  9536. if (key === "tomorrowWait") return isTomorrowWaitPlan(p);
  9537. if (key === "todayAll") return isTodayPlan(p);
  9538. if (key === "todayDone") return isTodayDonePlan(p);
  9539. if (key === "todayOpen") return isTodayOpenPlan(p);
  9540. if (key === "precursorReview") return isWaitReviewPrecursorPlan(p);
  9541. if (key === "waitArchive") return isWaitArchivePlan(p);
  9542. return true;
  9543. }
  9544. function resetPlanListTabs() {
  9545. planTab = "全部";
  9546. planDateTab = "全部";
  9547. document.querySelectorAll("#planTabs button").forEach((x) => x.classList.toggle("on", x.dataset.tab === "全部"));
  9548. document.querySelectorAll("#planDateTabs button").forEach((x) => x.classList.toggle("on", x.dataset.tab === "全部"));
  9549. }
  9550. function setPlanCardFilter(key) {
  9551. planCardFilter = planCardFilter === key ? "" : key;
  9552. if (planCardFilter) resetPlanListTabs();
  9553. renderPlans();
  9554. if (planCardFilter) document.getElementById("planTable")?.scrollIntoView({ behavior: "smooth", block: "start" });
  9555. }
  9556. function toggleUnlinkedOnly() {
  9557. setPlanCardFilter("precursorReview");
  9558. }
  9559. function toggleWaitPrecursorPlans() {
  9560. setPlanCardFilter("precursorReview");
  9561. }
  9562. function batchAudit() {
  9563. const list = selectedPlans();
  9564. if (!list.length) { toast("请先勾选计划"); return; }
  9565. const wait = list.find((p) => p.status === "待安排");
  9566. if (wait) { arrangePlan(wait.id); return; }
  9567. const ready = list.find((p) => p.status === "已安排");
  9568. if (ready) { unapprovePlan(ready.id); return; }
  9569. toast("所选计划无需审核或反审核");
  9570. }
  9571. function unapprovePlan(id) {
  9572. const p = plans.find((x) => x.id === id);
  9573. if (!p || p.status !== "已安排") return;
  9574. p.status = "待安排";
  9575. toast(`${p.no} 已反审核,回到待安排`);
  9576. renderPlans();
  9577. }
  9578. function batchLinkCert() {
  9579. goLinkBuyTransCert();
  9580. }
  9581. function goLinkBuyTransCert() {
  9582. showMod("cert");
  9583. if (!certs.length) {
  9584. toast("请先上传购买证及运输证");
  9585. openCertDlg(false);
  9586. return;
  9587. }
  9588. showCertView("list");
  9589. if (!waitLinkPlans().length) {
  9590. toast("暂无待关联计划,可在证件上勾选");
  9591. return;
  9592. }
  9593. toast("请人工核对运输证和车辆后点击「确认关联」");
  9594. setTimeout(focusCertWait, 40);
  9595. }
  9596. function suggestProcFromPlan(p) {
  9597. const text = `${p.product || ""} ${p.spec || ""}`;
  9598. const qty = Number(p.realQty != null ? p.realQty : p.planQty) || 0;
  9599. const conc = parseConc(text) || parseConc(p.spec);
  9600. const grade = acidGradeOf(text);
  9601. const isAcid = /硫酸|试剂酸|硫磺|槽车运输/.test(text) || p.spec === "CP";
  9602. const isHcl = /盐酸/.test(text);
  9603. const isAlkali = /烧碱|液碱/.test(text);
  9604. let draft;
  9605. if (isAcid && conc && conc < 90) {
  9606. const rawName = grade === "reagent" ? "试剂硫酸" : "98%工业硫酸";
  9607. const rawWh = grade === "reagent" ? "h2so4-rg" : "h2so4-98";
  9608. const rawQty = +(qty * conc / 98).toFixed(3);
  9609. const product = /槽车/.test(p.product || "")
  9610. ? (grade === "reagent" ? conc + "%试剂酸" : conc + "%工业硫酸")
  9611. : p.product;
  9612. const round = conc < 40 ? 30 : conc < 70 ? 50 : 50;
  9613. const toWh = suggestWarehouse(product, p.spec) || acidTargetWh(round);
  9614. draft = { mode: "dilute", rawName, rawWh, rawQty, water: Math.max(0, +(qty - rawQty).toFixed(3)), product, toWh, outQty: qty };
  9615. } else if (isHcl) {
  9616. const toWh = suggestWarehouse(p.product, p.spec) || "hcl-31-ind";
  9617. draft = { mode: "convert", rawName: p.product, rawWh: toWh, rawQty: qty, water: 0, product: p.product, toWh, outQty: qty };
  9618. } else if (isAlkali) {
  9619. const toWh = suggestWarehouse(p.product, p.spec) || "naoh-32";
  9620. draft = { mode: "convert", rawName: p.product, rawWh: toWh, rawQty: qty, water: 0, product: p.product, toWh, outQty: qty };
  9621. } else {
  9622. const toWh = suggestWarehouse(p.product, p.spec) || warehouseForPlan(p) || "h2so4-50";
  9623. draft = { mode: "convert", rawName: p.product, rawWh: toWh || "h2so4-50", rawQty: qty, water: 0, product: p.product, toWh: toWh || "h2so4-50", outQty: qty };
  9624. }
  9625. return draft;
  9626. }
  9627. function suggestTaskKind(p) {
  9628. const text = `${p.product || ""} ${p.spec || ""} ${p.pack || ""}`;
  9629. if (/分装|袋装|PL|桶装|吨桶|吨袋|IBC/.test(text)) return "分装";
  9630. if (/精制|试剂/.test(text)) return "精制";
  9631. return "稀释";
  9632. }
  9633. function suggestTaskContent(list, kind) {
  9634. return list.map((p) => {
  9635. const qty = Number(p.realQty) > 0 ? p.realQty : p.planQty;
  9636. return `请按运输计划 ${p.no} 对 ${p.product}${p.spec ? "(" + p.spec + ")" : ""} ${qty}${p.unit} 执行${kind}作业。`;
  9637. }).join("\n");
  9638. }
  9639. function defaultPlanTime() {
  9640. return today + "T08:00";
  9641. }
  9642. function staffOptionsHtml(selected) {
  9643. return SITE_STAFF.map((n) => `<option ${n === selected ? "selected" : ""}>${n}</option>`).join("");
  9644. }
  9645. function suggestAssignee(p) {
  9646. const text = `${p.product || ""} ${p.spec || ""}`;
  9647. if (/盐酸/.test(text)) return "张伟(盐酸库)";
  9648. if (/烧碱|液碱/.test(text)) return "王磊(液碱库)";
  9649. if (/分装|袋装/.test(text)) return "陈刚(分装库)";
  9650. if (/精制|试剂/.test(text)) return "赵敏(精制岗)";
  9651. return "刘洋(硫酸库)";
  9652. }
  9653. function previewTaskNo(kind) {
  9654. const code = { 分装: "FZ", 稀释: "XS", 精制: "JZ" }[kind] || "FZ";
  9655. return "SX" + code + today.replace(/-/g, "") + "-" + String(taskSeq + 1).padStart(3, "0");
  9656. }
  9657. function nextTaskNo(kind) {
  9658. const code = { 分装: "FZ", 稀释: "XS", 精制: "JZ" }[kind] || "FZ";
  9659. return "SX" + code + today.replace(/-/g, "") + "-" + String(++taskSeq).padStart(3, "0");
  9660. }
  9661. function planTaskLinesOf(list) {
  9662. return list.map((p) => ({
  9663. planId: p.id,
  9664. planNo: p.no,
  9665. product: p.product || "—",
  9666. model: p.spec || "",
  9667. pack: p.pack || "",
  9668. unit: p.unit || "吨",
  9669. qty: Number(p.realQty) > 0 ? p.realQty : p.planQty,
  9670. done: false
  9671. }));
  9672. }
  9673. function taskLinesOf(t) {
  9674. if (t.lines && t.lines.length) return t.lines;
  9675. return [{ product: t.product || "—", model: t.spec || "", pack: t.pack || "", unit: t.unit || "吨", qty: t.qty, done: t.status === "已完成" }];
  9676. }
  9677. function renderPlanTaskLines(list) {
  9678. const lines = planTaskLinesOf(list);
  9679. const pad = lines.concat(Array.from({ length: Math.max(0, 5 - lines.length) }, () => null));
  9680. document.getElementById("planTaskLines").innerHTML = `<table class="task-line-table"><thead><tr>
  9681. <th>序号</th><th>产品名称</th><th>产品型号</th><th>包装规格</th><th>单位</th><th>数量</th><th>是否完成</th>
  9682. </tr></thead><tbody>${pad.map((ln, i) => ln ? `<tr>
  9683. <td>${i + 1}</td><td>${ln.product}</td><td>${ln.model || "—"}</td><td>${ln.pack || "—"}</td>
  9684. <td>${ln.unit}</td><td class="num">${ln.qty}</td><td>□ 完成</td>
  9685. </tr>` : `<tr><td>${i + 1}</td><td></td><td></td><td></td><td></td><td></td><td></td></tr>`).join("")}</tbody></table>`;
  9686. }
  9687. function onPlanTaskKindChange() {
  9688. const form = document.getElementById("planTaskForm");
  9689. if (form) form.no.value = previewTaskNo(form.kind.value);
  9690. }
  9691. function refreshTaskContent() {
  9692. onPlanTaskKindChange();
  9693. }
  9694. function taskStHtml(s) {
  9695. const cls = s === "已完成" ? "st-run" : s === "执行中" ? "st-fix" : "st-stop";
  9696. return `<span class="st ${cls}">${s}</span>`;
  9697. }
  9698. function openPlanTaskDlg(id) {
  9699. let list = [];
  9700. if (id != null && typeof id !== "object") {
  9701. const one = plans.find((x) => x.id === id);
  9702. if (one) list = [one];
  9703. } else {
  9704. list = selectedPlans().filter((x) => x.status !== "已作废");
  9705. }
  9706. if (!list.length) { toast("请先勾选运输计划,再点「下发任务单」"); return; }
  9707. if (list.some((x) => x.status === "已作废")) { toast("已作废计划不能下发任务单"); return; }
  9708. const linked = list.filter((x) => x.taskNo);
  9709. if (linked.length === list.length) {
  9710. const nos = [...new Set(linked.map((x) => x.taskNo))];
  9711. const t = nos.length === 1 ? TASKS.find((x) => x.no === nos[0]) : null;
  9712. if (t) { openTaskSheetDlg(t.id); return; }
  9713. toast("所选计划均已下发任务单");
  9714. showMod("task");
  9715. return;
  9716. }
  9717. list = list.filter((x) => !x.taskNo);
  9718. linkingPlanTaskIds = list.map((x) => x.id);
  9719. const form = document.getElementById("planTaskForm");
  9720. const kind = suggestTaskKind(list[0]);
  9721. const orderBy = String(list[0].salesman || "调度员").split("/")[0].trim() || "调度员";
  9722. form.kind.value = kind;
  9723. form.no.value = previewTaskNo(kind);
  9724. form.orderDate.value = today;
  9725. form.deliverDate.value = list[0].deliverDate || list[0].date || today;
  9726. form.orderBy.value = orderBy;
  9727. form.handler.innerHTML = staffOptionsHtml(suggestAssignee(list[0]));
  9728. form.maker.value = "郑丽娜";
  9729. form.auditor.value = "";
  9730. form.supervisor.value = "";
  9731. form.bucket.value = "桶循环";
  9732. form.remark.value = list.map((x) => x.customer || x.settler || "").filter(Boolean)[0] || "";
  9733. renderPlanTaskLines(list);
  9734. document.getElementById("planTaskHint").textContent =
  9735. list.length === 1
  9736. ? `来源计划 ${list[0].no} · ${planTypeName(list[0])}。明细按附件任务单字段带出,确认后下发。`
  9737. : `已勾选 ${list.length} 条计划,将合并为 1 张${kind}任务单(产品明细多行)。`;
  9738. document.getElementById("planTaskDialog").showModal();
  9739. }
  9740. function closePlanTaskDlg() {
  9741. document.getElementById("planTaskDialog").close();
  9742. linkingPlanTaskIds = [];
  9743. }
  9744. function savePlanTask() {
  9745. const list = plans.filter((x) => linkingPlanTaskIds.includes(x.id));
  9746. if (!list.length) { toast("未找到来源计划"); return; }
  9747. const form = document.getElementById("planTaskForm");
  9748. if (!form.reportValidity()) return;
  9749. const f = Object.fromEntries(new FormData(form));
  9750. const lines = planTaskLinesOf(list);
  9751. const no = nextTaskNo(f.kind);
  9752. const first = lines[0];
  9753. TASKS.unshift({
  9754. id: taskSeq, no, kind: f.kind, status: "待执行",
  9755. date: f.orderDate || today,
  9756. orderDate: f.orderDate, deliverDate: f.deliverDate,
  9757. orderBy: f.orderBy, handler: f.handler, maker: f.maker || "郑丽娜",
  9758. auditor: f.auditor || "", supervisor: f.supervisor || "",
  9759. bucket: f.bucket || "桶循环", remark: f.remark || "",
  9760. planTime: (f.deliverDate || today) + "T08:00",
  9761. planId: list[0].id, planNo: list.map((x) => x.no).join("、"),
  9762. product: first.product, spec: first.model, pack: first.pack,
  9763. qty: lines.reduce((s, ln) => s + Number(ln.qty || 0), 0), unit: first.unit,
  9764. assignee: f.handler, lines
  9765. });
  9766. list.forEach((p) => { p.taskNo = no; });
  9767. closePlanTaskDlg();
  9768. toast(`已下发任务单 ${no},含 ${lines.length} 行明细`);
  9769. selectedPlanIds.clear();
  9770. renderPlans();
  9771. showMod("task");
  9772. openTaskSheetDlg(taskSeq);
  9773. }
  9774. function taskSheetBox(on) {
  9775. return `<span class="ts-box ${on ? "on" : ""}"></span>`;
  9776. }
  9777. function closeTaskSheetDlg() {
  9778. document.getElementById("taskSheetDialog").close();
  9779. }
  9780. function openTaskSheetDlg(id) {
  9781. const t = TASKS.find((x) => x.id === id);
  9782. if (!t) { toast("未找到任务单"); return; }
  9783. const lines = taskLinesOf(t);
  9784. const pad = lines.concat(Array.from({ length: Math.max(0, 5 - lines.length) }, () => ({})));
  9785. const b = t.bucket || "";
  9786. const blank = (v) => v || "";
  9787. document.querySelector("#taskSheetDialog h3").textContent = `山峡${t.kind}任务单`;
  9788. document.getElementById("taskSheetBody").innerHTML = `<div class="task-sheet">
  9789. <h3>山峡${t.kind}任务单</h3>
  9790. <div class="ts-no">NO: ${t.no}</div>
  9791. <table>
  9792. <colgroup>
  9793. <col style="width:9%"><col style="width:16%"><col style="width:14%"><col style="width:16%">
  9794. <col style="width:12%"><col style="width:12%"><col style="width:21%">
  9795. </colgroup>
  9796. <tr>
  9797. <td colspan="3">下单日期:${blank(t.orderDate || t.date)}</td>
  9798. <td colspan="4">交货日期:${blank(t.deliverDate)}</td>
  9799. </tr>
  9800. <tr>
  9801. <td colspan="3">下单人:${blank(t.orderBy)}</td>
  9802. <td colspan="4">经手人:${blank(t.handler || t.assignee)}</td>
  9803. </tr>
  9804. <tr>
  9805. <th>序号</th><th>产品名称</th><th>产品型号</th><th>包装规格</th><th>单位</th><th>数量</th><th>是否完成</th>
  9806. </tr>
  9807. ${pad.map((ln, i) => `<tr>
  9808. <td class="idx">${i + 1}</td>
  9809. <td class="center">${blank(ln.product)}</td>
  9810. <td class="center">${blank(ln.model || ln.spec)}</td>
  9811. <td class="center">${blank(ln.pack)}</td>
  9812. <td class="center">${blank(ln.unit)}</td>
  9813. <td class="center">${ln.qty != null && ln.qty !== "" ? ln.qty : ""}</td>
  9814. <td class="center">${ln.product ? (ln.done ? "☑完成" : "□完成") : ""}</td>
  9815. </tr>`).join("")}
  9816. <tr>
  9817. <td class="center">备注</td>
  9818. <td colspan="6">${blank(t.remark)}
  9819. <span class="ts-checks">
  9820. ${taskSheetBox(/全新/.test(b))}全新桶
  9821. ${taskSheetBox(/回收/.test(b))}是否回收桶
  9822. ${taskSheetBox(/循环/.test(b))}桶循环
  9823. </span>
  9824. </td>
  9825. </tr>
  9826. <tr>
  9827. <td colspan="7" class="ts-foot">制单人:${blank(t.maker)}  审核:${blank(t.auditor) || "________"}  生产主管:${blank(t.supervisor) || "________"}</td>
  9828. </tr>
  9829. </table>
  9830. </div>`;
  9831. document.getElementById("taskSheetDialog").showModal();
  9832. }
  9833. function nowLocal() {
  9834. const d = new Date();
  9835. const p = (n) => String(n).padStart(2, "0");
  9836. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;
  9837. }
  9838. function selectedTasks() {
  9839. return TASKS.filter((t) => selectedTaskIds.has(t.id));
  9840. }
  9841. function toggleTaskSel(id, on) {
  9842. if (on) selectedTaskIds.add(id);
  9843. else selectedTaskIds.delete(id);
  9844. }
  9845. function toggleTaskSelAll(on) {
  9846. document.querySelectorAll("#taskTable tbody input[type=checkbox]").forEach((el) => {
  9847. el.checked = on;
  9848. toggleTaskSel(+el.dataset.id, on);
  9849. });
  9850. }
  9851. function renderTasks() {
  9852. const q = (document.getElementById("taskQ")?.value || "").trim();
  9853. const st = document.getElementById("taskSt")?.value || "";
  9854. const rows = TASKS.filter((t) => {
  9855. if (taskTab === "待执行" && t.status !== "待执行") return false;
  9856. if (taskTab === "已完成" && t.status !== "已完成") return false;
  9857. if (["稀释", "精制", "分装"].includes(taskTab) && t.kind !== taskTab) return false;
  9858. if (st && t.status !== st) return false;
  9859. const blob = [t.no, t.planNo, t.product, t.spec, t.pack, t.orderBy, t.handler, t.assignee, t.remark, t.maker, t.report].join(" ");
  9860. if (q && !blob.includes(q)) return false;
  9861. return true;
  9862. });
  9863. const kindN = (status) => ["稀释", "精制", "分装"].map((kind) =>
  9864. TASKS.filter((t) => t.kind === kind && (!status || t.status === status)).length
  9865. );
  9866. const slashTriple = (parts) => parts.map((p) => `<span>${p}</span>`).join("<i>/</i>");
  9867. const kindsTitle = `<span class="kinds">${slashTriple(["稀释", "精制", "分装"])}</span>`;
  9868. document.getElementById("taskStats").innerHTML = [
  9869. ["待执行", slashTriple(kindN("待执行")), "wait"],
  9870. ["执行中", slashTriple(kindN("执行中")), "run"],
  9871. ["已完成", slashTriple(kindN("已完成")), "done"],
  9872. ["全部", slashTriple(kindN("")), "all"]
  9873. ].map(([k, v, cls]) => `<div class="stat"><div class="k"><span class="stat-name ${cls}">${k}</span>${kindsTitle}</div><div class="v">${v}</div></div>`).join("");
  9874. document.getElementById("taskHint").innerHTML = `当前 <b>${rows.length}</b> 条`;
  9875. document.getElementById("taskCount").textContent = `共 ${rows.length} 条`;
  9876. const allOn = rows.length && rows.every((t) => selectedTaskIds.has(t.id));
  9877. document.getElementById("taskTable").innerHTML = `<table class="erp-table task-ops"><thead><tr>
  9878. <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="toggleTaskSelAll(this.checked)" /></th>
  9879. <th>任务单号</th><th>类型</th><th>状态</th><th>下单日期</th><th>交货日期</th><th>下单人</th><th>经手人</th>
  9880. <th>产品名称</th><th>产品型号</th><th>包装规格</th><th>单位</th><th>数量</th><th>备注</th><th>制单人</th>
  9881. </tr></thead><tbody>${rows.length ? rows.map((t) => {
  9882. const lines = taskLinesOf(t);
  9883. const first = lines[0] || {};
  9884. const more = lines.length > 1 ? ` 等${lines.length}行` : "";
  9885. return `<tr class="${selectedTaskIds.has(t.id) ? "sel" : ""}">
  9886. <td class="chk"><input type="checkbox" data-id="${t.id}" ${selectedTaskIds.has(t.id) ? "checked" : ""} onchange="toggleTaskSel(${t.id}, this.checked)" /></td>
  9887. <td class="cell-no"><button class="op-link" type="button" onclick="openTaskSheetDlg(${t.id})">${t.no}</button></td>
  9888. <td>${t.kind}任务单</td>
  9889. <td>${taskStHtml(t.status)}</td>
  9890. <td>${t.orderDate || t.date || "—"}</td>
  9891. <td>${t.deliverDate || "—"}</td>
  9892. <td>${t.orderBy || "—"}</td>
  9893. <td>${t.handler || t.assignee || "—"}</td>
  9894. <td>${(first.product || t.product || "—") + more}</td>
  9895. <td>${first.model || t.spec || "—"}</td>
  9896. <td>${first.pack || t.pack || "—"}</td>
  9897. <td>${first.unit || t.unit || "—"}</td>
  9898. <td class="num">${first.qty != null ? first.qty : t.qty}</td>
  9899. <td class="clip">${[t.remark, t.bucket].filter(Boolean).join(" · ") || "—"}</td>
  9900. <td>${t.maker || "—"}</td>
  9901. </tr>`;
  9902. }).join("") : `<tr><td colspan="15" class="muted">暂无任务单。请在运输计划勾选后点「下发任务单」。</td></tr>`}</tbody></table>`;
  9903. updateBadges();
  9904. }
  9905. function taskOps(t) {
  9906. const sheet = `<button class="op-link" type="button" onclick="openTaskSheetDlg(${t.id})">查看单据</button>`;
  9907. if (t.status === "待执行") {
  9908. return `${sheet}<button class="op-link" type="button" onclick="receiveTask(${t.id})">接收任务</button><button class="op-link" type="button" onclick="openTaskReportDlg(${t.id})">回传完成</button>`;
  9909. }
  9910. if (t.status === "执行中") {
  9911. return `${sheet}<button class="op-link" type="button" onclick="openTaskReportDlg(${t.id})">回传完成</button>`;
  9912. }
  9913. return `${sheet}<button class="op-link" type="button" onclick="openTaskReportDlg(${t.id})">查看回传</button>`;
  9914. }
  9915. function receiveTask(id) {
  9916. const t = TASKS.find((x) => x.id === id);
  9917. if (!t || t.status !== "待执行") { toast("仅待执行任务可接收"); return; }
  9918. t.status = "执行中";
  9919. t.receivedAt = nowLocal();
  9920. toast(`${t.no} 现场已接收,可回传完成`);
  9921. renderTasks();
  9922. }
  9923. function batchReceiveTask() {
  9924. const list = selectedTasks().filter((t) => t.status === "待执行");
  9925. if (!list.length) { toast("请勾选待执行的任务单后再点「接收任务」"); return; }
  9926. list.forEach((t) => {
  9927. t.status = "执行中";
  9928. t.receivedAt = nowLocal();
  9929. });
  9930. toast(`已接收 ${list.length} 张任务单`);
  9931. selectedTaskIds.clear();
  9932. renderTasks();
  9933. }
  9934. function batchReportTask() {
  9935. const list = selectedTasks().filter((t) => t.status !== "已完成");
  9936. if (!list.length) { toast("请勾选未完成的任务单后再点「回传完成」"); return; }
  9937. openTaskReportDlg(list[0].id);
  9938. }
  9939. function renderTaskSheetPhotoList(photos, clickable) {
  9940. const thumbs = document.getElementById("taskSheetPhotoThumbs");
  9941. const chips = document.getElementById("taskSheetPhotoChips");
  9942. const list = photos || [];
  9943. thumbs.innerHTML = list.map((a, i) => {
  9944. if (a.url && (a.type || "").startsWith("image/")) {
  9945. const btn = clickable
  9946. ? `<button type="button" onclick="openAttach('task',${reportingTaskId},${i})"><img alt="${a.name}" src="${a.url}" /></button>`
  9947. : `<img alt="${a.name}" src="${a.url}" />`;
  9948. return btn;
  9949. }
  9950. return "";
  9951. }).join("");
  9952. chips.innerHTML = list.map((a, i) =>
  9953. clickable && a.url
  9954. ? `<button class="chip" type="button" onclick="openAttach('task',${reportingTaskId},${i})">${a.name}</button>`
  9955. : `<span class="chip ghost">${a.name}</span>`
  9956. ).join("");
  9957. }
  9958. function previewTaskSheetPhotos(input) {
  9959. const files = [...input.files];
  9960. renderTaskSheetPhotoList(files.map((f) => ({
  9961. name: f.name,
  9962. type: f.type,
  9963. url: f.type.startsWith("image/") ? URL.createObjectURL(f) : ""
  9964. })), false);
  9965. }
  9966. function openTaskReportDlg(id) {
  9967. const t = TASKS.find((x) => x.id === id);
  9968. if (!t) return;
  9969. reportingTaskId = id;
  9970. const form = document.getElementById("taskReportForm");
  9971. const view = t.status === "已完成";
  9972. document.getElementById("taskReportTitle").textContent = view ? "查看回传" : "回传完成任务";
  9973. document.getElementById("taskReportHint").textContent = view
  9974. ? `${t.no} · ${t.assignee} 已回传完成。`
  9975. : `${t.no} · ${t.kind}任务单 · ${t.product} ${t.qty}${t.unit}。现场完成后填写回传说明,并拍照上传纸质分装单。`;
  9976. form.no.value = t.no;
  9977. form.assignee.value = t.assignee;
  9978. form.doneTime.value = t.doneTime || nowLocal();
  9979. form.doneQty.value = t.doneQty != null ? t.doneQty : t.qty;
  9980. form.report.value = t.report || "";
  9981. ["doneTime", "doneQty", "report"].forEach((n) => { form[n].readOnly = view; form[n].disabled = view; });
  9982. const photoInput = form.sheetPhotos;
  9983. photoInput.value = "";
  9984. photoInput.required = !view;
  9985. document.getElementById("taskSheetPhotoBtn").classList.toggle("hidden", view);
  9986. document.getElementById("taskSheetPhotoHint").textContent = view
  9987. ? "已上传的纸质分装单照片,点击可查看。"
  9988. : "须拍摄已签字完成的纸质分装任务单,可多张。";
  9989. renderTaskSheetPhotoList(t.sheetPhotos || [], view);
  9990. document.getElementById("taskReportSave").classList.toggle("hidden", view);
  9991. document.getElementById("taskReportDialog").showModal();
  9992. }
  9993. function closeTaskReportDlg() {
  9994. document.getElementById("taskReportDialog").close();
  9995. reportingTaskId = null;
  9996. }
  9997. function saveTaskReport() {
  9998. const t = TASKS.find((x) => x.id === reportingTaskId);
  9999. if (!t) { toast("未找到任务单"); return; }
  10000. const form = document.getElementById("taskReportForm");
  10001. const photoInput = form.sheetPhotos;
  10002. if (!photoInput.files.length) {
  10003. toast("请拍照上传已完成的纸质分装单");
  10004. photoInput.focus();
  10005. return;
  10006. }
  10007. if (!form.reportValidity()) return;
  10008. const f = Object.fromEntries(new FormData(form));
  10009. readFiles(photoInput.files).then((photos) => {
  10010. if (t.status === "待执行") {
  10011. t.receivedAt = t.receivedAt || nowLocal();
  10012. }
  10013. t.status = "已完成";
  10014. t.doneTime = f.doneTime;
  10015. t.doneQty = +f.doneQty;
  10016. t.report = f.report;
  10017. t.sheetPhotos = photos;
  10018. if (t.lines && t.lines.length) t.lines.forEach((ln) => { ln.done = true; });
  10019. closeTaskReportDlg();
  10020. toast(`${t.no} 已回传完成`);
  10021. selectedTaskIds.delete(t.id);
  10022. renderTasks();
  10023. });
  10024. }
  10025. function voidPlan(id) {
  10026. const p = plans.find((x) => x.id === id);
  10027. if (!p || p.status === "已完成") return;
  10028. p.status = "已作废";
  10029. toast(`${p.no} 已作废`);
  10030. renderPlans();
  10031. }
  10032. function copyPlan() {
  10033. const rows = selectedPlans();
  10034. if (rows.length !== 1) { toast("请勾选一条计划后再复制"); return; }
  10035. const src = rows[0];
  10036. const no = "PO" + today.replace(/-/g, "") + String(++planSeq).padStart(3, "0");
  10037. plans.unshift({
  10038. ...src, id: planSeq, no, status: "草稿", realQty: null, tractor: "", driver: "", escort: "",
  10039. certId: null, transId: null, warehouseId: null, archived: false, waitNotify: false,
  10040. source: "复制", sourceNo: src.no, taskNo: "", contractNo: src.contractNo || ""
  10041. });
  10042. selectedPlanIds.clear();
  10043. toast("已复制为草稿 " + no);
  10044. renderPlans();
  10045. }
  10046. function arrangeSelectedPlans() {
  10047. const list = selectedPlans().filter((p) => p.status === "待安排" || p.status === "草稿");
  10048. if (!list.length) { toast("请勾选待安排或草稿计划"); return; }
  10049. list.forEach((p) => arrangePlan(p.id));
  10050. }
  10051. function waitCustomerNotify() {
  10052. const list = selectedPlans().filter((p) => p.status === "待安排" || p.status === "草稿");
  10053. if (!list.length) { toast("请勾选待安排计划"); return; }
  10054. list.forEach((p) => { p.waitNotify = true; p.status = "待安排"; });
  10055. toast(`已标记 ${list.length} 条为等客户通知`);
  10056. renderPlans();
  10057. }
  10058. function printPlanDocs() {
  10059. const list = selectedPlans();
  10060. if (!list.length) { toast("请先勾选计划"); return; }
  10061. toast("演示:已打印 " + list.map((p) => p.no).join("、"));
  10062. }
  10063. function voidSelectedPlans() {
  10064. const list = selectedPlans().filter((p) => p.status !== "已完成" && p.status !== "已作废");
  10065. if (!list.length) { toast("请勾选可作废的计划"); return; }
  10066. list.forEach((p) => { p.status = "已作废"; p.waitNotify = false; });
  10067. selectedPlanIds.clear();
  10068. toast("已作废所选计划");
  10069. renderPlans();
  10070. }
  10071. function createTriParty() {
  10072. toast("演示:三方人车快速创建");
  10073. }
  10074. function renderPlanKpis() {
  10075. const box = document.getElementById("planKpis");
  10076. if (!box) return;
  10077. const todayAllN = plans.filter(isTodayPlan).length;
  10078. const todayDoneN = plans.filter(isTodayDonePlan).length;
  10079. const todayOpenN = plans.filter(isTodayOpenPlan).length;
  10080. const todayOn = ["todayAll", "todayDone", "todayOpen"].includes(planCardFilter);
  10081. const bit = (key, cls, label, n) =>
  10082. `<button class="${cls}${planCardFilter === key ? " on" : ""}" type="button" onclick="setPlanCardFilter('${key}')">${label ? `${label} ` : ""}<b>${n}</b></button>`;
  10083. box.innerHTML = [
  10084. `<div class="stat tone-wait ${planCardFilter === "tomorrowWait" ? "on" : ""}">
  10085. <div class="k">明日待安排计划</div>
  10086. <button class="v" type="button" onclick="setPlanCardFilter('tomorrowWait')">${plans.filter(isTomorrowWaitPlan).length}</button>
  10087. </div>`,
  10088. `<div class="stat tone-today ${todayOn ? "on" : ""}">
  10089. <div class="k">今日计划</div>
  10090. <div class="today-bits">
  10091. ${bit("todayAll", "", "共", todayAllN)}
  10092. ${bit("todayDone", "", "已完成", todayDoneN)}
  10093. ${bit("todayOpen", "open", "未完成", todayOpenN)}
  10094. </div>
  10095. </div>`,
  10096. `<div class="stat tone-ctrl ${planCardFilter === "precursorReview" ? "on" : ""}">
  10097. <div class="k">待审核易制毒计划</div>
  10098. <button class="v" type="button" onclick="setPlanCardFilter('precursorReview')">${plans.filter(isWaitReviewPrecursorPlan).length}</button>
  10099. </div>`,
  10100. `<div class="stat tone-arch ${planCardFilter === "waitArchive" ? "on" : ""}">
  10101. <div class="k">待归档计划</div>
  10102. <button class="v" type="button" onclick="setPlanCardFilter('waitArchive')">${plans.filter(isWaitArchivePlan).length}</button>
  10103. </div>`
  10104. ].join("");
  10105. }
  10106. function renderPlans() {
  10107. const q = (document.getElementById("planQ")?.value || "").trim();
  10108. const alerts = document.getElementById("planAlerts");
  10109. if (alerts) alerts.innerHTML = "";
  10110. const rows = plans.filter((p) => {
  10111. if (!planTabMatch(p, planTab)) return false;
  10112. const d = p.date || "";
  10113. if (planDateTab === "昨日" && d !== dayShift(today, -1)) return false;
  10114. if (planDateTab === "今日" && d !== today) return false;
  10115. if (planDateTab === "明日" && d !== dayShift(today, 1)) return false;
  10116. if (planDateTab === "本周") {
  10117. const [a, b] = quoteWeekRange();
  10118. if (d < a || d > b) return false;
  10119. }
  10120. if (planCardFilter && !planCardMatch(p, planCardFilter)) return false;
  10121. const blob = [p.no, p.product, p.customer, p.settler, p.tractor, p.driver, p.escort, p.salesman, p.sourceNo, p.loadFrom, p.contractNo, planTypeName(p)].join(" ");
  10122. if (q && !blob.includes(q)) return false;
  10123. return true;
  10124. });
  10125. const size = parseInt(document.getElementById("planPageSize").value, 10) || 20;
  10126. const pageRows = rows.slice(0, size);
  10127. renderPlanKpis();
  10128. document.getElementById("planCount").textContent = "状态全选";
  10129. const allOn = pageRows.length && pageRows.every((p) => selectedPlanIds.has(p.id));
  10130. document.getElementById("planTable").innerHTML = `<table class="erp-table plan-table"><thead><tr>
  10131. <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="togglePlanSelAll(this.checked)" /></th>
  10132. <th class="idx">序号</th>
  10133. <th class="col-no">计划编号</th>
  10134. <th>跟进状态</th>
  10135. <th>计划日期</th>
  10136. <th>送货时间</th>
  10137. <th>单据类型</th>
  10138. <th>车牌号</th>
  10139. <th>驾押人员</th>
  10140. <th>装货客户</th>
  10141. <th>送货客户</th>
  10142. <th>产品名称</th>
  10143. <th>产品规格</th>
  10144. <th>包装规格</th>
  10145. <th>计划数量</th>
  10146. <th>实际数量</th>
  10147. <th>合同</th>
  10148. <th>任务单</th>
  10149. </tr></thead><tbody>${pageRows.map((p, i) => {
  10150. const out = p.status === "已作废" ? "—" : (p.realQty == null ? 0 : p.realQty);
  10151. return `<tr class="${p.ctrl && !p.certId && p.status === "待安排" ? "row-wait" : ""}${selectedPlanIds.has(p.id) ? " sel" : ""}">
  10152. <td class="chk"><input type="checkbox" data-id="${p.id}" ${selectedPlanIds.has(p.id) ? "checked" : ""} onchange="togglePlanSel(${p.id}, this.checked)" /></td>
  10153. <td class="idx">${i + 1}</td>
  10154. <td class="cell-no col-no">${p.no}</td>
  10155. <td>${planStHtml(p)}</td>
  10156. <td>${p.date}</td>
  10157. <td>${planDeliverAt(p)}</td>
  10158. <td>${planTypeName(p)}</td>
  10159. <td>${p.tractor || "—"}</td>
  10160. <td>${planCrew(p)}</td>
  10161. <td class="clip">${p.loadFrom || "—"}</td>
  10162. <td class="clip">${p.customer || p.settler || "—"}</td>
  10163. <td>${p.product || "—"}</td>
  10164. <td>${p.spec || "—"}</td>
  10165. <td>${p.pack || "—"}</td>
  10166. <td class="num qty">${p.planQty}${p.unit ? " " + p.unit : ""}</td>
  10167. <td class="num out-qty">${out}${out !== "—" && p.unit ? " " + p.unit : ""}</td>
  10168. <td class="clip">${planContractNo(p)}</td>
  10169. <td>${p.taskNo ? `<button class="op-link" type="button" onclick="openPlanTaskDlg(${p.id})">${p.taskNo}</button>` : "—"}</td>
  10170. </tr>`;
  10171. }).join("")}</tbody></table>`;
  10172. updateBadges();
  10173. }
  10174. function openPlanDlg() {
  10175. const form = document.getElementById("planForm");
  10176. form.reset();
  10177. form.querySelector("[name=unit]").value = "吨";
  10178. form.querySelector("[name=date]").value = today;
  10179. document.getElementById("planDialog").showModal();
  10180. }
  10181. function closePlanDlg() { document.getElementById("planDialog").close(); }
  10182. function savePlan() {
  10183. const form = document.getElementById("planForm");
  10184. if (!form.reportValidity()) return;
  10185. const f = Object.fromEntries(new FormData(form));
  10186. plans.unshift({
  10187. ...f, id: ++planSeq, no: "PO" + today.replace(/-/g, "") + String(planSeq).padStart(3, "0"),
  10188. status: "待安排", planQty: +f.planQty, realQty: null, tractor: "", driver: "", certId: null, warehouseId: null,
  10189. deliverDate: f.deliverDate || f.date,
  10190. salesman: f.salesman || "调度员",
  10191. dept: f.dept || "运输调度组",
  10192. shipMode: f.shipMode || "槽车运输",
  10193. spec: f.spec || "",
  10194. pack: f.pack || "槽罐车",
  10195. settler: f.settler || f.customer || "",
  10196. docKind: f.type === "直销单" ? "开单类" : "选单类",
  10197. source: "手工录入",
  10198. sourceNo: "PO" + today.replace(/-/g, "") + String(planSeq).padStart(3, "0"),
  10199. deliverTime: "",
  10200. escort: "",
  10201. contractNo: f.contractNo || ""
  10202. });
  10203. closePlanDlg();
  10204. toast("计划已保存为待安排");
  10205. renderPlans();
  10206. }
  10207. function arrangePlan(id) {
  10208. const p = plans.find((x) => x.id === id);
  10209. if (!p) return;
  10210. if (p.status === "已作废" || p.status === "已完成") {
  10211. toast("该计划不能安排");
  10212. return;
  10213. }
  10214. if (p.ctrl && !p.certId) {
  10215. toast("请先在购买证管理中人工勾选关联计划");
  10216. const c = suggestCertForPlan(p);
  10217. showMod("cert");
  10218. if (c) openCertLinkPlanDlg(c.id, [p.id]);
  10219. else openCertDlg(false);
  10220. return;
  10221. }
  10222. const cert = certs.find((c) => c.id === p.certId);
  10223. if (cert) {
  10224. const d = pickVehicleForPlan(cert, p.planQty, p.transId, p.tractor);
  10225. if (d) {
  10226. p.tractor = d.plate;
  10227. p.driver = d.driver || p.driver;
  10228. }
  10229. }
  10230. p.status = "已安排";
  10231. toast(p.tractor ? `已安排 ${p.tractor},分派运量来自同一运输公司运输证` : "已安排发运");
  10232. renderPlans();
  10233. }
  10234. function refreshCertViews() {
  10235. if (currentMod !== "cert") return;
  10236. if (document.getElementById("view-cert-detail")?.classList.contains("hidden")) renderCertList();
  10237. else renderCertDetail();
  10238. updateBadges();
  10239. }
  10240. function completePlan(id) {
  10241. const p = plans.find((x) => x.id === id);
  10242. if (!p) return;
  10243. if (p.status !== "已安排") {
  10244. toast("仅已安排的计划可标记完成");
  10245. return;
  10246. }
  10247. if (p.ctrl && !p.certId) {
  10248. toast("易制毒 / 易制爆计划请先关联购买证和运输证");
  10249. openLinkCertDlg(id);
  10250. return;
  10251. }
  10252. p.status = "已完成";
  10253. if (p.realQty == null) p.realQty = p.planQty;
  10254. p.archived = false;
  10255. p.warehouseId = isDirectSale(p) ? null : suggestWarehouse(p.product, p.spec);
  10256. let msg = `${p.no} 已完成,请到运输归档人工审核`;
  10257. if (p.certId) {
  10258. const s = settleCertOnComplete(p);
  10259. if (s) {
  10260. msg = `${p.no} 已完成,扣减 ${fmtTon(s.n)}${p.unit};购买证余 ${fmtTon(s.buyLeft)}${s.cert.unit},运输证余 ${fmtTon(s.transLeft)}${s.cert.unit}`;
  10261. if (lowRemain(s.buyLeft, buyWarnQty(s.cert)) || lowRemain(s.transLeft, transWarnQty(s.trans)) || lowRemain(s.availBuy, buyWarnQty(s.cert)) || lowRemain(s.availTrans, transWarnQty(s.trans))) {
  10262. msg += "(余量已达自设预警量)";
  10263. }
  10264. }
  10265. }
  10266. toast(msg);
  10267. renderPlans();
  10268. refreshCertViews();
  10269. }
  10270. function openArchiveDlg(id) {
  10271. const p = plans.find((x) => x.id === id);
  10272. if (!p) return;
  10273. if (p.status === "已作废") {
  10274. toast("已作废计划不能归档");
  10275. return;
  10276. }
  10277. if (p.status !== "已完成") {
  10278. toast("请先在运输计划中将单据标记为已完成");
  10279. return;
  10280. }
  10281. if (p.archived) {
  10282. toast("该单据已归档");
  10283. return;
  10284. }
  10285. if (p.ctrl && !p.certId) {
  10286. toast("请先关联购买证 / 运输证");
  10287. openLinkCertDlg(id);
  10288. return;
  10289. }
  10290. const qty = Number(p.realQty || p.planQty);
  10291. const cert = certs.find((c) => c.id === p.certId);
  10292. if (cert && remain(cert, p.id) + 1e-9 < qty) {
  10293. toast(`购买证余量不足:剩余 ${remain(cert, p.id)} ${cert.unit},本计划 ${qty} ${p.unit}`);
  10294. return;
  10295. }
  10296. if (cert && transRemain(cert, p.id) + 1e-9 < qty) {
  10297. toast(`运输证余量不足:剩余 ${transRemain(cert, p.id)} ${cert.unit},本计划 ${qty} ${p.unit}`);
  10298. return;
  10299. }
  10300. archivingPlanId = id;
  10301. const form = document.getElementById("archiveForm");
  10302. const wh = warehouseForPlan(p);
  10303. const whRow = document.getElementById("archiveWhRow");
  10304. if (isDirectSale(p)) {
  10305. document.getElementById("archiveHint").textContent = `${p.no} 为直销单,无中转仓库,审核通过后仅归档,不记入库存。`;
  10306. document.getElementById("archiveWhHint").textContent = "直销单不经过硫酸库 / 液碱库 / 分装库 / 固体库。";
  10307. whRow.style.display = "grid";
  10308. document.getElementById("archiveWhName").value = "无中转仓库";
  10309. form.warehouseId.value = "";
  10310. } else {
  10311. const action = isInboundPlan(p) ? "入库计入" : "出库扣减";
  10312. document.getElementById("archiveHint").textContent =
  10313. `${p.no} 为${planTypeName(p)},按品名「${p.product}」自动对应仓库,审核通过后${action}库存。`;
  10314. document.getElementById("archiveWhHint").textContent = isInboundPlan(p)
  10315. ? "入库单:按品名进入对应仓库(进货入库)。"
  10316. : "出库单:按品名从对应仓库发出(销货出库)。";
  10317. whRow.style.display = "grid";
  10318. document.getElementById("archiveWhName").value = wh ? whNameOf(wh) : "未匹配到仓库,请核对品名";
  10319. form.warehouseId.value = wh || "";
  10320. }
  10321. document.getElementById("archiveNo").value = p.no;
  10322. document.getElementById("archiveProd").value = `${planTypeName(p)} · ${p.product}${p.spec ? " / " + p.spec : ""}`;
  10323. document.getElementById("archiveUnit").value = p.unit;
  10324. form.realQty.value = qty;
  10325. document.getElementById("archiveDialog").showModal();
  10326. }
  10327. function closeArchiveDlg() {
  10328. document.getElementById("archiveDialog").close();
  10329. archivingPlanId = null;
  10330. }
  10331. function recordWhFromArchive(p) {
  10332. if (isDirectSale(p) || !p.warehouseId) return;
  10333. const qty = Number(p.realQty || p.planQty);
  10334. const inbound = isInboundPlan(p);
  10335. const stock = matchStock(p.warehouseId, p.product);
  10336. const price = stock ? stock.cost : 0;
  10337. if (inbound) {
  10338. if (stock) stock.qty += qty;
  10339. WH_LEDGER.unshift({
  10340. date: today, doc: "进货单", no: p.no, wh: p.warehouseId, product: p.product,
  10341. inQty: qty, inCost: qty * price, outQty: 0, outCost: 0,
  10342. balQty: stock ? stock.qty : qty, price, balCost: stock ? stock.qty * price : qty * price,
  10343. note: "运输归档审核入库"
  10344. });
  10345. } else {
  10346. if (stock) stock.qty = Math.max(0, +(stock.qty - qty).toFixed(3));
  10347. WH_LEDGER.unshift({
  10348. date: today, doc: "销货单", no: p.no, wh: p.warehouseId, product: p.product,
  10349. inQty: 0, inCost: 0, outQty: qty, outCost: qty * price,
  10350. balQty: stock ? stock.qty : 0, price, balCost: stock ? stock.qty * price : 0,
  10351. note: "运输归档审核出库"
  10352. });
  10353. }
  10354. }
  10355. function applyArchive(p, qty) {
  10356. if (!p || p.archived) return "skip";
  10357. if (p.status !== "已完成") return "未完成";
  10358. if (p.ctrl && !p.certId) return "待关联证件";
  10359. const n = Number(qty != null ? qty : (p.realQty || p.planQty));
  10360. const cert = certs.find((c) => c.id === p.certId);
  10361. if (cert && remain(cert, p.id) + 1e-9 < n) return `购买证余量不足(剩 ${remain(cert, p.id)})`;
  10362. if (cert && transRemain(cert, p.id) + 1e-9 < n) return `运输证余量不足(剩 ${transRemain(cert, p.id)})`;
  10363. p.realQty = n;
  10364. p.status = "已完成";
  10365. if (isDirectSale(p)) {
  10366. p.warehouseId = null;
  10367. p.archived = true;
  10368. p.archiveDate = today;
  10369. if (cert) settleCertOnComplete(p);
  10370. return "直销";
  10371. }
  10372. const wh = suggestWarehouse(p.product, p.spec) || p.warehouseId;
  10373. if (!wh) return "未匹配仓库";
  10374. p.warehouseId = wh;
  10375. p.archived = true;
  10376. p.archiveDate = today;
  10377. recordWhFromArchive(p);
  10378. if (cert) settleCertOnComplete(p);
  10379. return isInboundPlan(p) ? "入库" : "出库";
  10380. }
  10381. function confirmArchive() {
  10382. const p = plans.find((x) => x.id === archivingPlanId);
  10383. if (!p) return;
  10384. const form = document.getElementById("archiveForm");
  10385. if (!form.reportValidity()) return;
  10386. const qty = Number(form.realQty.value);
  10387. const result = applyArchive(p, qty);
  10388. if (result === "未匹配仓库") {
  10389. toast("未能按品名匹配仓库,请核对品名后重试");
  10390. return;
  10391. }
  10392. if (result === "待关联证件") {
  10393. toast("请先关联购买证 / 运输证");
  10394. closeArchiveDlg();
  10395. openLinkCertDlg(p.id);
  10396. return;
  10397. }
  10398. if (result !== "入库" && result !== "出库" && result !== "直销" && result !== "skip") {
  10399. toast(result);
  10400. return;
  10401. }
  10402. if (result === "直销") toast(`${p.no} 已审核归档,直销单无中转仓库`);
  10403. else if (result === "skip") toast("该单据已归档");
  10404. else toast(`${p.no} 已审核归档,${result} ${whNameOf(p.warehouseId)}`);
  10405. closeArchiveDlg();
  10406. renderPlans();
  10407. showMod("archive");
  10408. }
  10409. function archiveOps(p) {
  10410. if (p.archived) return `<button class="op-link" type="button" onclick="showMod('warehouse')">看仓储</button>`;
  10411. return `<button class="op-link" type="button" onclick="openArchiveDlg(${p.id})">审核归档</button>`;
  10412. }
  10413. function toggleArchiveSel(id, on) {
  10414. if (on) selectedArchiveIds.add(id);
  10415. else selectedArchiveIds.delete(id);
  10416. }
  10417. function toggleArchiveSelAll(on) {
  10418. document.querySelectorAll("#archiveTable tbody input[type=checkbox]").forEach((el) => {
  10419. el.checked = on;
  10420. toggleArchiveSel(+el.dataset.id, on);
  10421. });
  10422. }
  10423. function batchReviewArchive() {
  10424. const pending = plans.filter((p) => p.status === "已完成" && !p.archived);
  10425. const picked = pending.filter((p) => selectedArchiveIds.has(p.id));
  10426. const list = picked.length ? picked : pending.filter((p) => archiveTab === "待审核" || archiveTab === "全部");
  10427. if (!list.length) { toast("没有待审核的已完成单据"); return; }
  10428. if (list.length === 1) {
  10429. openArchiveDlg(list[0].id);
  10430. return;
  10431. }
  10432. let ok = 0;
  10433. const fail = [];
  10434. list.forEach((p) => {
  10435. const r = applyArchive(p, p.realQty || p.planQty);
  10436. if (r === "入库" || r === "出库" || r === "直销") ok += 1;
  10437. else if (r !== "skip") fail.push(`${p.no} ${r}`);
  10438. });
  10439. selectedArchiveIds.clear();
  10440. toast(fail.length ? `已归档 ${ok} 条;${fail[0]}` : `已审核归档 ${ok} 条`);
  10441. renderPlans();
  10442. renderArchive();
  10443. }
  10444. function renderArchive() {
  10445. const sel = document.getElementById("archiveWh");
  10446. const keep = sel.value;
  10447. sel.innerHTML = `<option value="">全部仓库</option><option value="none">无中转仓库</option>` + warehouseSelectHtml(keep);
  10448. if (keep && ![...sel.options].some((o) => o.value === keep)) sel.value = "";
  10449. else sel.value = keep;
  10450. const q = (document.getElementById("archiveQ").value || "").trim();
  10451. const type = document.getElementById("archiveType").value;
  10452. const wh = sel.value;
  10453. const done = plans.filter((p) => p.status === "已完成");
  10454. const rows = done.filter((p) => {
  10455. if (archiveTab === "待审核" && p.archived) return false;
  10456. if (archiveTab === "已归档" && !p.archived) return false;
  10457. if (type && p.type !== type) return false;
  10458. if (wh === "none") { if (!isDirectSale(p)) return false; }
  10459. else if (wh && warehouseForPlan(p) !== wh) return false;
  10460. const blob = [p.no, p.product, p.customer, p.settler, p.tractor, p.driver, warehouseForPlan(p) ? whNameOf(warehouseForPlan(p)) : "无中转"].join(" ");
  10461. if (q && !blob.includes(q)) return false;
  10462. return true;
  10463. });
  10464. const ym = today.slice(0, 7);
  10465. const yr = today.slice(0, 4);
  10466. const inMonth = (d) => (d || "").slice(0, 7) === ym;
  10467. const inYear = (d) => (d || "").slice(0, 4) === yr;
  10468. const waitN = done.filter((p) => !p.archived && inMonth(p.date || p.deliverDate)).length;
  10469. const monthArchN = done.filter((p) => p.archived && inMonth(p.archiveDate || p.date)).length;
  10470. const unArchN = done.filter((p) => !p.archived).length;
  10471. const yearArchN = done.filter((p) => p.archived && inYear(p.archiveDate || p.date)).length;
  10472. document.getElementById("archiveStats").innerHTML = [
  10473. ["当月待审核", waitN, "待人工审核"],
  10474. ["当月已归档", monthArchN, "审核通过"],
  10475. ["未归档", unArchN, "已完成尚未归档"],
  10476. ["本年度归档数", yearArchN, "本年审核通过"]
  10477. ].map(([k, v, s]) => `<div class="stat"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");
  10478. const hint = document.getElementById("archiveHintBar");
  10479. if (hint) hint.innerHTML = `当前 <b>${archiveTab}</b> 共 <b>${rows.length}</b> 条`;
  10480. document.getElementById("archiveCount").textContent = archiveTab === "待审核" ? `待审核 ${rows.length} 条` : `共 ${rows.length} 条`;
  10481. const vis = rows.filter((p) => !p.archived);
  10482. const allOn = vis.length && vis.every((p) => selectedArchiveIds.has(p.id));
  10483. document.getElementById("archiveTable").innerHTML = rows.length ? `<table class="erp-table" style="min-width:1480px"><thead><tr>
  10484. <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="toggleArchiveSelAll(this.checked)" /></th>
  10485. <th class="idx">序号</th>
  10486. <th>单据编号</th><th>审核状态</th><th>单据类型</th><th>品名</th><th>规格</th>
  10487. <th>实发量</th><th>对应仓库</th><th>库存动作</th><th>客户</th><th>车牌</th><th>驾驶员</th><th>操作</th>
  10488. </tr></thead><tbody>${rows.map((p, i) => `<tr>
  10489. <td class="chk">${p.archived ? "" : `<input type="checkbox" data-id="${p.id}" ${selectedArchiveIds.has(p.id) ? "checked" : ""} onchange="toggleArchiveSel(${p.id}, this.checked)" />`}</td>
  10490. <td class="idx">${i + 1}</td>
  10491. <td class="cell-no">${p.no}</td>
  10492. <td>${p.archived ? `<span class="st-pill arch">已归档</span>` : `<span class="st-pill wait">待审核</span>`}</td>
  10493. <td>${planTypeName(p)}</td>
  10494. <td>${productCell(p)}</td>
  10495. <td>${p.spec || "—"}</td>
  10496. <td class="num qty">${p.realQty != null ? p.realQty : p.planQty} ${p.unit}</td>
  10497. <td>${warehouseCell(p)}</td>
  10498. <td>${stockActCell(p)}</td>
  10499. <td>${p.settler || p.customer || "—"}</td>
  10500. <td>${p.tractor || "—"}</td>
  10501. <td>${p.driver || "—"}</td>
  10502. <td>${archiveOps(p)}</td>
  10503. </tr>`).join("")}</tbody></table>` : `<div class="empty">${archiveTab === "待审核" ? "暂无待审核单据。请在运输计划中将已安排单据点「完成」。" : "没有符合条件的归档记录。"}</div>`;
  10504. updateBadges();
  10505. }
  10506. function openLinkCertDlg(planId) {
  10507. linkingPlanId = planId;
  10508. const p = plans.find((x) => x.id === planId);
  10509. if (!p) return;
  10510. document.getElementById("linkCertHint").textContent =
  10511. `${p.no} · ${p.product} ${p.planQty}${p.unit} · ${p.ctrl || "普通"}。请选择购买证下的一张运输证;关联后按该证车辆计划量预警。`;
  10512. const pairs = certs.flatMap((c) => transOf(c).map((t) => ({ c, t })));
  10513. document.getElementById("linkCertTable").innerHTML = pairs.length ? `<table><thead><tr>
  10514. <th>购买证</th><th>运输证 / 公司</th><th>品名</th><th>购买证余量</th><th>运输证余量</th><th>分派车辆</th><th></th>
  10515. </tr></thead><tbody>${pairs.map(({ c, t }) => {
  10516. const mismatch = c.product && p.product && !String(p.product).includes(c.product) && !String(c.product).includes(String(p.product).replace(/\d+%/g, "").trim());
  10517. const short = remainGap(c, p.id) < p.planQty || transItemGap(t, p.id) < p.planQty;
  10518. return `<tr>
  10519. <td class="cell-no">${c.buyNo}</td>
  10520. <td>${t.transNo}<div class="muted">${t.carrier || "—"}</div></td>
  10521. <td>${c.product}${mismatch ? `<div class="muted">与计划品名不同</div>` : ""}</td>
  10522. <td>${remainHtml(c)}</td>
  10523. <td>${transItemRemain(t)} ${c.unit}${transItemWarn(t) ? `<div class="muted">≤${fmtTon(transWarnQty(t))}${c.unit}预警</div>` : ""}</td>
  10524. <td>${(t.dispatches || []).map((d) => d.plate).join("、") || "未分派"}</td>
  10525. <td><button class="btn btn-sm btn-primary" type="button" onclick="linkCert(${c.id}, ${t.id})">关联此证</button>${short ? `<div class="muted">余量低于本计划</div>` : ""}</td>
  10526. </tr>`;
  10527. }).join("")}</tbody></table>` : `<div class="empty">没有可用证件,请先上传购买证及运输证。</div>`;
  10528. document.getElementById("linkCertDialog").showModal();
  10529. }
  10530. function closeLinkCertDlg() { document.getElementById("linkCertDialog").close(); linkingPlanId = null; }
  10531. function attachPlanToCert(p, c, t, allowOver, plate) {
  10532. const qty = occupyQty(p) || Number(p.planQty || 0);
  10533. if (!allowOver && remainGap(c, p.id) + 1e-9 < qty) return `购买证余量 ${remain(c, p.id)}${c.unit} 不足`;
  10534. if (!allowOver && transItemGap(t, p.id) + 1e-9 < qty) return `运输证 ${t.transNo} 余量 ${transItemRemain(t, p.id)}${c.unit} 不足`;
  10535. p.certId = c.id;
  10536. p.transId = t.id;
  10537. const want = plate || p.tractor || "";
  10538. const d = want
  10539. ? ((t.dispatches || []).find((x) => x.plate === want) || { plate: want, driver: p.driver })
  10540. : pickVehicleForTrans(t, qty, p.tractor);
  10541. if (d && d.plate) {
  10542. p.tractor = d.plate;
  10543. p.driver = d.driver || p.driver;
  10544. }
  10545. const calc = calcVehPlanQty(c, t, p.tractor, p);
  10546. p.vehPlanQty = calc.qty;
  10547. p.linkBound = calc.room;
  10548. return "";
  10549. }
  10550. function linkCert(certId, transId) {
  10551. const p = plans.find((x) => x.id === linkingPlanId);
  10552. const c = certs.find((x) => x.id === certId);
  10553. const t = c ? findTrans(c, transId) : null;
  10554. if (!p || !c || !t) return;
  10555. const err = attachPlanToCert(p, c, t, true);
  10556. if (err) { toast(err); return; }
  10557. closeLinkCertDlg();
  10558. const over = p.tractor && t.dispatches && vehicleOver({ plate: p.tractor, qty: ((t.dispatches || []).find((d) => d.plate === p.tractor) || {}).qty || 0 }, t);
  10559. toast(`已关联 ${c.buyNo} / ${t.transNo}${p.tractor ? " · " + p.tractor : ""}${over ? "(该车计划量已超分派,已预警)" : ""}`);
  10560. renderPlans();
  10561. if (currentMod === "cert") renderCertDetail();
  10562. }
  10563. function unlinkCertPlan(planId) {
  10564. const p = plans.find((x) => x.id === planId);
  10565. if (!p) return;
  10566. p.certId = null;
  10567. p.transId = null;
  10568. p.shipCertId = null;
  10569. p.procureCertId = null;
  10570. toast(`${p.no} 已取消关联`);
  10571. if (currentMod === "cert") {
  10572. if (document.getElementById("view-cert-detail")?.classList.contains("hidden")) renderCertList();
  10573. else renderCertDetail();
  10574. } else renderPlans();
  10575. updateBadges();
  10576. }
  10577. function certAllVehOptions(c, plate) {
  10578. const ds = c ? allDispatches(c) : [];
  10579. const plates = [...new Set(ds.map((d) => d.plate).filter(Boolean))];
  10580. if (plate && !plates.includes(plate)) plates.unshift(plate);
  10581. const opts = [`<option value="">未指定车辆</option>`];
  10582. plates.forEach((pl) => {
  10583. const d = ds.find((x) => x.plate === pl);
  10584. const cap = d ? Number(d.qty || 0) : 0;
  10585. const tno = d && d.transNo ? d.transNo : "";
  10586. opts.push(`<option value="${pl}" ${pl === plate ? "selected" : ""}>${pl}${cap ? " · 分派 " + cap + "吨" : ""}${tno ? " · " + tno : ""}</option>`);
  10587. });
  10588. return opts.join("");
  10589. }
  10590. function certLinkVehOptions(t, plate) {
  10591. const fromPlates = t ? String(t.plates || "").split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean) : [];
  10592. const plates = t ? [...new Set([...(t.dispatches || []).map((d) => d.plate), ...fromPlates, plate].filter(Boolean))] : (plate ? [plate] : []);
  10593. const opts = [`<option value="">未指定车辆</option>`];
  10594. plates.forEach((pl) => {
  10595. const d = t && (t.dispatches || []).find((x) => x.plate === pl);
  10596. const cap = d ? Number(d.qty || 0) : 0;
  10597. opts.push(`<option value="${pl}" ${pl === plate ? "selected" : ""}>${pl}${cap ? " · 分派 " + cap + "吨" : ""}</option>`);
  10598. });
  10599. return opts.join("");
  10600. }
  10601. function openCertLinkPlanDlg(certId, focusPlanIds) {
  10602. const c = certs.find((x) => x.id === certId);
  10603. if (!c) return;
  10604. linkingCertId = certId;
  10605. currentCertId = certId;
  10606. linkingCertFocusIds = (focusPlanIds || []).map(Number);
  10607. const list = transOf(c);
  10608. if (!list.length) {
  10609. toast("请先上传运输证");
  10610. return;
  10611. }
  10612. document.getElementById("certLinkPlanHint").textContent =
  10613. `购买证 ${c.buyNo} · ${c.product} · 证载 ${c.buyQty}${c.unit}。勾选计划并指定运输证、车辆;余量 = 证载 − Σ关联车辆计划量,≤ 自设预警量时预警。`;
  10614. const qEl = document.getElementById("certLinkPlanQ");
  10615. if (qEl) qEl.value = "";
  10616. const cands = plans.filter((p) =>
  10617. p.status !== "已作废" &&
  10618. (p.certId === c.id || !p.certId) &&
  10619. (p.ctrl || /硫酸|硝酸|铵/.test(p.product || ""))
  10620. ).sort((a, b) => {
  10621. const fa = linkingCertFocusIds.includes(a.id) ? 1 : 0;
  10622. const fb = linkingCertFocusIds.includes(b.id) ? 1 : 0;
  10623. if (fa !== fb) return fb - fa;
  10624. const ma = planMatchesCert(a, c) ? 1 : 0;
  10625. const mb = planMatchesCert(b, c) ? 1 : 0;
  10626. if (ma !== mb) return mb - ma;
  10627. const la = a.certId === c.id ? 1 : 0;
  10628. const lb = b.certId === c.id ? 1 : 0;
  10629. return lb - la;
  10630. });
  10631. document.getElementById("certLinkPlanTable").innerHTML = cands.length ? `<table class="erp-table"><thead><tr>
  10632. <th class="chk"></th><th>计划编号</th><th>状态</th><th>品名</th><th>客户</th><th>计划量</th><th>两证可运</th><th>占用运输证</th><th>关联车辆</th>
  10633. </tr></thead><tbody>${cands.map((p) => {
  10634. const on = p.certId === c.id || linkingCertFocusIds.includes(p.id);
  10635. const match = planMatchesCert(p, c);
  10636. const tid = (suggestTransForPlan(c, p) || list[0]).id;
  10637. const t = findTrans(c, tid);
  10638. const blob = [p.no, p.product, p.customer, p.settler, p.tractor, p.ctrl].join(" ");
  10639. return `<tr data-blob="${blob}">
  10640. <td class="chk"><input type="checkbox" data-plan="${p.id}" ${on ? "checked" : ""} onchange="previewCertLinkPlans()" /></td>
  10641. <td class="cell-no">${p.no}${linkingCertFocusIds.includes(p.id) ? `<span class="match-rec">待关联</span>` : ""}</td>
  10642. <td>${planStHtml(p)}</td>
  10643. <td>${p.product}${match ? `<span class="match-rec">匹配</span>` : ""}<div class="muted">${p.ctrl || ""}</div></td>
  10644. <td class="clip">${p.customer || p.settler || "—"}</td>
  10645. <td class="num">${p.planQty} ${p.unit}</td>
  10646. <td class="num" data-bound-for="${p.id}">${fmtTon(calcVehPlanQty(c, t, p.tractor, p).qty)} ${c.unit}</td>
  10647. <td><select data-trans-for="${p.id}" onchange="syncCertLinkRow(${p.id})">${list.map((x) =>
  10648. `<option value="${x.id}" ${x.id === tid ? "selected" : ""}>${x.transNo} · 余 ${fmtTon(transItemRemain(x))}${c.unit}</option>`
  10649. ).join("")}</select></td>
  10650. <td><select data-veh-for="${p.id}" onchange="previewCertLinkPlans()">${certLinkVehOptions(t, p.tractor)}</select></td>
  10651. </tr>`;
  10652. }).join("")}</tbody></table>` : `<div class="empty">没有可关联的易制毒 / 易制爆计划。</div>`;
  10653. document.getElementById("certLinkPlanDialog").showModal();
  10654. previewCertLinkPlans();
  10655. }
  10656. function filterCertLinkPlanRows() {
  10657. const q = (document.getElementById("certLinkPlanQ")?.value || "").trim();
  10658. document.querySelectorAll("#certLinkPlanTable tbody tr").forEach((tr) => {
  10659. const blob = tr.getAttribute("data-blob") || "";
  10660. tr.style.display = !q || blob.includes(q) ? "" : "none";
  10661. });
  10662. }
  10663. function syncCertLinkRow(planId) {
  10664. const c = certs.find((x) => x.id === linkingCertId);
  10665. if (!c) return;
  10666. const transSel = document.querySelector(`#certLinkPlanTable [data-trans-for="${planId}"]`);
  10667. const vehSel = document.querySelector(`#certLinkPlanTable [data-veh-for="${planId}"]`);
  10668. const p = plans.find((x) => x.id === planId);
  10669. const t = findTrans(c, +(transSel && transSel.value));
  10670. if (vehSel) vehSel.innerHTML = certLinkVehOptions(t, (vehSel.value || (p && p.tractor) || ""));
  10671. previewCertLinkPlans();
  10672. }
  10673. function certLinkDrafts() {
  10674. const c = certs.find((x) => x.id === linkingCertId);
  10675. if (!c) return [];
  10676. return [...document.querySelectorAll("#certLinkPlanTable [data-plan]:checked")].map((el) => {
  10677. const p = plans.find((x) => x.id === +el.dataset.plan);
  10678. if (!p) return null;
  10679. const transSel = document.querySelector(`#certLinkPlanTable [data-trans-for="${p.id}"]`);
  10680. const vehSel = document.querySelector(`#certLinkPlanTable [data-veh-for="${p.id}"]`);
  10681. const t = findTrans(c, +(transSel && transSel.value));
  10682. const plate = (vehSel && vehSel.value) || p.tractor || "";
  10683. return { p, t, plate, qty: occupyQty(p) };
  10684. }).filter((x) => x && x.t);
  10685. }
  10686. function previewCertLinkPlans() {
  10687. const box = document.getElementById("certLinkPreview");
  10688. const c = certs.find((x) => x.id === linkingCertId);
  10689. if (!box || !c) return;
  10690. const drafts = certLinkDrafts();
  10691. drafts.forEach(({ p, t, plate }) => {
  10692. const cell = document.querySelector(`[data-bound-for="${p.id}"]`);
  10693. if (cell) cell.textContent = `${fmtTon(calcVehPlanQty(c, t, plate, p).qty)} ${c.unit}`;
  10694. });
  10695. const shown = new Set([...document.querySelectorAll("#certLinkPlanTable [data-plan]")].map((el) => +el.dataset.plan));
  10696. const keep = linkedPlansOf(c).filter((p) => !shown.has(p.id));
  10697. const used = drafts.reduce((s, x) => s + x.qty, 0) + keep.reduce((s, p) => s + occupyQty(p), 0);
  10698. const buyLeft = Number(c.buyQty || 0) - used;
  10699. const byTrans = {};
  10700. const veh = {};
  10701. drafts.forEach((x) => {
  10702. byTrans[x.t.id] = (byTrans[x.t.id] || 0) + x.qty;
  10703. if (x.plate) veh[x.t.id + "|" + x.plate] = (veh[x.t.id + "|" + x.plate] || 0) + x.qty;
  10704. });
  10705. keep.forEach((p) => {
  10706. if (p.transId) byTrans[p.transId] = (byTrans[p.transId] || 0) + occupyQty(p);
  10707. if (p.transId && p.tractor) veh[p.transId + "|" + p.tractor] = (veh[p.transId + "|" + p.tractor] || 0) + occupyQty(p);
  10708. });
  10709. const transTips = Object.entries(byTrans).map(([tid, hold]) => {
  10710. const t = findTrans(c, +tid);
  10711. if (!t) return "";
  10712. const left = Number(t.transQty || 0) - hold;
  10713. if (left < -1e-6) return `${t.transNo} 超 ${(-left).toFixed(2)}${c.unit}`;
  10714. if (lowRemain(left, transWarnQty(t))) return `${t.transNo} 余 ${left}${c.unit}(≤${fmtTon(transWarnQty(t))})`;
  10715. return `${t.transNo} 余 ${left}${c.unit}`;
  10716. });
  10717. const vehTips = [];
  10718. Object.entries(veh).forEach(([key, hold]) => {
  10719. const [tid, plate] = key.split("|");
  10720. const t = findTrans(c, +tid);
  10721. const d = t && (t.dispatches || []).find((x) => x.plate === plate);
  10722. const cap = d ? Number(d.qty || 0) : 0;
  10723. if (cap > 0 && hold - cap > 1e-6) vehTips.push(`${plate} 计划 ${hold}${c.unit} > 分派 ${cap}${c.unit}`);
  10724. else if (cap <= 0 && hold > 1e-6) vehTips.push(`${plate} 计划 ${hold}${c.unit},运输证尚未分派该车`);
  10725. });
  10726. let cls = "";
  10727. const bits = [
  10728. `已勾选 ${drafts.length} 条,各车运输计划量 = min(计划量, 购买证余量, 运输证余量, 车辆分派)`,
  10729. `关联合计 ${fmtTon(used)}${c.unit}`,
  10730. `购买证 ${fmtTon(c.buyQty)} − 已完成/在途 = 余 ${fmtTon(buyLeft)}${c.unit}`,
  10731. ...transTips
  10732. ];
  10733. const transOver = Object.entries(byTrans).some(([tid, hold]) => {
  10734. const t = findTrans(c, +tid);
  10735. return t && Number(t.transQty || 0) - hold < -1e-6;
  10736. });
  10737. if (buyLeft < -1e-6 || transOver || vehTips.length) {
  10738. cls = "over";
  10739. if (buyLeft < -1e-6) bits.push(`购买证超 ${(-buyLeft).toFixed(2)}${c.unit}`);
  10740. bits.push(...vehTips);
  10741. } else if ((buyLeft > 0 && buyLeft <= buyWarnQty(c)) || transTips.some((s) => s.includes("≤"))) {
  10742. cls = "warn";
  10743. bits.push(`余量 ≤ 预警量 ${fmtTon(buyWarnQty(c))}${c.unit},保存后预警`);
  10744. } else if (vehTips.length === 0) {
  10745. bits.push("车辆计划量未超分派");
  10746. }
  10747. box.className = "link-preview" + (cls ? " " + cls : "");
  10748. box.textContent = bits.filter(Boolean).join(" · ");
  10749. }
  10750. function closeCertLinkPlanDlg() {
  10751. document.getElementById("certLinkPlanDialog").close();
  10752. linkingCertId = null;
  10753. linkingCertFocusIds = [];
  10754. }
  10755. function saveCertLinkPlans() {
  10756. const c = certs.find((x) => x.id === linkingCertId);
  10757. if (!c) return;
  10758. const drafts = certLinkDrafts();
  10759. const missing = [...document.querySelectorAll("#certLinkPlanTable [data-plan]:checked")].length - drafts.length;
  10760. if (missing > 0) { toast("请为勾选的计划选择运输证"); return; }
  10761. const picked = drafts.map((x) => x.p.id);
  10762. const shown = [...document.querySelectorAll("#certLinkPlanTable [data-plan]")].map((el) => +el.dataset.plan);
  10763. let n = 0;
  10764. let warn = 0;
  10765. plans.forEach((p) => {
  10766. if (p.certId === c.id && shown.includes(p.id) && !picked.includes(p.id)) {
  10767. p.certId = null;
  10768. p.transId = null;
  10769. }
  10770. });
  10771. drafts.forEach(({ p, t, plate }) => {
  10772. attachPlanToCert(p, c, t, true, plate);
  10773. n += 1;
  10774. const d = (t.dispatches || []).find((x) => x.plate === p.tractor) || { plate: p.tractor, qty: 0 };
  10775. if (p.tractor && vehicleOver(d, t)) warn += 1;
  10776. });
  10777. closeCertLinkPlanDlg();
  10778. toast(warn ? `已关联 ${n} 条计划,已按两证计算各车运输计划量,其中 ${warn} 车超分派预警` : `已人工关联 ${n} 条计划,已按购买证 / 运输证计算各车运输计划量`);
  10779. showMod("cert");
  10780. showCertView("detail", c.id);
  10781. updateBadges();
  10782. }
  10783. document.getElementById("subnav").addEventListener("click", (e) => {
  10784. const b = e.target.closest("button");
  10785. if (b) showView(b.dataset.view);
  10786. });
  10787. document.getElementById("certSubnav").addEventListener("click", (e) => {
  10788. const b = e.target.closest("button");
  10789. if (!b || b.disabled) return;
  10790. showCertView(b.dataset.view);
  10791. });
  10792. document.getElementById("expTabs").addEventListener("click", (e) => {
  10793. const b = e.target.closest("button");
  10794. if (!b) return;
  10795. setExpTab(b.dataset.tab);
  10796. });
  10797. document.getElementById("planTabs").addEventListener("click", (e) => {
  10798. const b = e.target.closest("button");
  10799. if (!b) return;
  10800. planCardFilter = "";
  10801. planTab = b.dataset.tab;
  10802. document.querySelectorAll("#planTabs button").forEach((x) => x.classList.toggle("on", x === b));
  10803. renderPlans();
  10804. });
  10805. document.getElementById("planDateTabs").addEventListener("click", (e) => {
  10806. const b = e.target.closest("button");
  10807. if (!b) return;
  10808. planCardFilter = "";
  10809. planDateTab = b.dataset.tab;
  10810. document.querySelectorAll("#planDateTabs button").forEach((x) => x.classList.toggle("on", x === b));
  10811. renderPlans();
  10812. });
  10813. document.getElementById("quoteTabs").addEventListener("click", (e) => {
  10814. const b = e.target.closest("button");
  10815. if (!b) return;
  10816. quoteTab = b.dataset.tab;
  10817. quotePage = 1;
  10818. document.querySelectorAll("#quoteTabs button").forEach((x) => x.classList.toggle("on", x === b));
  10819. renderQuotes();
  10820. });
  10821. document.getElementById("ctTabs").addEventListener("click", (e) => {
  10822. const b = e.target.closest("button");
  10823. if (!b) return;
  10824. ctTab = b.dataset.tab;
  10825. ctPage = 1;
  10826. document.querySelectorAll("#ctTabs button").forEach((x) => x.classList.toggle("on", x === b));
  10827. renderContracts();
  10828. });
  10829. document.getElementById("archiveTabs").addEventListener("click", (e) => {
  10830. const b = e.target.closest("button");
  10831. if (!b) return;
  10832. archiveTab = b.dataset.tab;
  10833. document.querySelectorAll("#archiveTabs button").forEach((x) => x.classList.toggle("on", x === b));
  10834. renderArchive();
  10835. });
  10836. document.getElementById("taskTabs").addEventListener("click", (e) => {
  10837. const b = e.target.closest("button");
  10838. if (!b) return;
  10839. taskTab = b.dataset.tab;
  10840. document.querySelectorAll("#taskTabs button").forEach((x) => x.classList.toggle("on", x === b));
  10841. renderTasks();
  10842. });
  10843. document.querySelector(".sidebar").addEventListener("click", (e) => {
  10844. const tog = e.target.closest("[data-toggle]");
  10845. if (tog) {
  10846. const group = tog.closest(".nav-group");
  10847. const willOpen = !group.classList.contains("open");
  10848. group.classList.toggle("open");
  10849. if (willOpen && !group.querySelector(".side-sub.active")) {
  10850. const first = group.querySelector("[data-mod], [data-page]");
  10851. if (first?.dataset.mod) {
  10852. if (first.dataset.whView) currentWhView = first.dataset.whView;
  10853. if (first.dataset.fleetView) currentFleetView = first.dataset.fleetView;
  10854. showMod(first.dataset.mod);
  10855. } else if (first?.dataset.page) showPage(first.dataset.page);
  10856. }
  10857. return;
  10858. }
  10859. const leaf = e.target.closest("[data-mod], [data-page]");
  10860. if (!leaf) return;
  10861. if (leaf.dataset.mod) {
  10862. if (leaf.dataset.whView) currentWhView = leaf.dataset.whView;
  10863. if (leaf.dataset.fleetView) currentFleetView = leaf.dataset.fleetView;
  10864. showMod(leaf.dataset.mod);
  10865. } else showPage(leaf.dataset.page);
  10866. });
  10867. document.getElementById("globalSearch").addEventListener("keydown", (e) => {
  10868. if (e.key !== "Enter") return;
  10869. const q = e.target.value;
  10870. if (currentMod === "cert") {
  10871. document.getElementById("certQ").value = q;
  10872. showCertView("list");
  10873. } else if (currentMod === "plan") {
  10874. document.getElementById("planQ").value = q;
  10875. renderPlans();
  10876. } else if (currentMod === "product") {
  10877. document.getElementById("prodQ").value = q;
  10878. prodPage = 1;
  10879. renderProducts();
  10880. } else if (currentMod === "quote") {
  10881. document.getElementById("quoteQ").value = q;
  10882. quotePage = 1;
  10883. renderQuotes();
  10884. } else if (currentMod === "contract") {
  10885. document.getElementById("ctQ").value = q;
  10886. ctPage = 1;
  10887. renderContracts();
  10888. } else if (currentMod === "archive") {
  10889. document.getElementById("archiveQ").value = q;
  10890. renderArchive();
  10891. } else if (currentMod === "fleet") {
  10892. document.getElementById("fleetQ").value = q;
  10893. showView("fleet");
  10894. } else if (currentMod === "expense") {
  10895. setExpTab("全部");
  10896. const box = document.getElementById("expQ");
  10897. if (box) box.value = q;
  10898. if (typeof renderExpenseList === "function") renderExpenseList();
  10899. } else if (currentMod === "staff") {
  10900. const box = document.getElementById("staffQ");
  10901. if (box) box.value = q;
  10902. staffPage = 1;
  10903. renderStaff();
  10904. }
  10905. });
  10906. fillPlates();
  10907. updateBadges();
  10908. bindCertDrop(document.getElementById("buyOcrBox"), "buy");
  10909. bindCertDrop(document.getElementById("transOcrBox"), "trans");
  10910. document.getElementById("certForm")?.addEventListener("input", (e) => {
  10911. const n = e.target && e.target.name;
  10912. if (n !== "buyQty" && n !== "transQty") return;
  10913. const box = e.target.closest(".form") || e.target.closest(".extra-trans") || e.target.closest("#buyFormCol");
  10914. const warnEl = box && box.querySelector("[name=warnQty]");
  10915. if (warnEl) warnEl.value = defaultWarnQty(e.target.value);
  10916. });
  10917. showMod("cert");
  10918. document.addEventListener("click", () => {
  10919. document.getElementById("ctExportDrop")?.classList.remove("open");
  10920. document.getElementById("prodExportDrop")?.classList.remove("open");
  10921. });
  10922. </script>
  10923. </body>
  10924. </html>