| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675106761067710678106791068010681106821068310684106851068610687106881068910690106911069210693106941069510696106971069810699107001070110702107031070410705107061070710708107091071010711107121071310714107151071610717107181071910720107211072210723107241072510726107271072810729107301073110732107331073410735107361073710738107391074010741107421074310744107451074610747107481074910750107511075210753107541075510756107571075810759107601076110762107631076410765107661076710768107691077010771107721077310774107751077610777107781077910780107811078210783107841078510786107871078810789107901079110792107931079410795107961079710798107991080010801108021080310804108051080610807108081080910810108111081210813108141081510816108171081810819108201082110822108231082410825108261082710828108291083010831108321083310834108351083610837108381083910840108411084210843108441084510846108471084810849108501085110852108531085410855108561085710858108591086010861108621086310864108651086610867108681086910870108711087210873108741087510876108771087810879108801088110882108831088410885108861088710888108891089010891108921089310894108951089610897108981089910900109011090210903109041090510906109071090810909109101091110912109131091410915109161091710918109191092010921109221092310924109251092610927109281092910930109311093210933109341093510936109371093810939109401094110942109431094410945109461094710948109491095010951109521095310954109551095610957109581095910960109611096210963109641096510966109671096810969109701097110972109731097410975109761097710978109791098010981109821098310984109851098610987109881098910990109911099210993109941099510996109971099810999110001100111002110031100411005110061100711008110091101011011110121101311014110151101611017 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>山峡代工一体化平台</title>
- <link rel="preconnect" href="https://fonts.googleapis.com" />
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
- <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet" />
- <style>
- :root {
- --green: #167c4a;
- --green-dark: #0f5e38;
- --green-soft: #e8f6ee;
- --green-line: #b7dfc8;
- --blue: #2a6bb3;
- --blue-soft: #e8f1fb;
- --blue-line: #b7cce8;
- --ink: #1c2430;
- --muted: #5b6675;
- --line: #e6ebe7;
- --paper: #f4f7f5;
- --white: #ffffff;
- --warn: #d94841;
- --warn-soft: #fdecea;
- --amber: #c47d11;
- --amber-soft: #fff6e5;
- --sidebar: 232px;
- --header: 56px;
- --radius: 12px;
- --shadow: 0 1px 2px rgba(28, 36, 48, 0.04), 0 8px 24px rgba(28, 36, 48, 0.04);
- }
- * { box-sizing: border-box; margin: 0; padding: 0; }
- html, body { height: 100%; }
- body {
- font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", "Microsoft YaHei", sans-serif;
- color: var(--ink); background: var(--paper); font-size: 13px; line-height: 1.5;
- }
- button, input, select, textarea { font-family: inherit; font-size: 13px; }
- button { cursor: pointer; border: 0; background: none; }
- .app {
- display: grid;
- grid-template-columns: var(--sidebar) 1fr;
- min-height: 100vh;
- }
- .sidebar {
- background: var(--white);
- border-right: 1px solid var(--line);
- display: flex;
- flex-direction: column;
- position: sticky;
- top: 0;
- height: 100vh;
- }
- .brand {
- height: var(--header);
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 0 18px;
- border-bottom: 1px solid var(--line);
- min-width: 0;
- }
- .logo {
- width: 28px; height: 28px; border-radius: 8px; flex: none;
- background: linear-gradient(160deg, #22a05d, var(--green-dark));
- display: grid; place-items: center; color: #fff; font-weight: 700; font-size: 13px;
- }
- .brand strong { font-size: 13px; letter-spacing: 0.02em; font-weight: 700; }
- .brand .sub { display: block; font-size: 11px; color: var(--muted); font-weight: 400; margin-top: 1px; }
- .nav { padding: 12px 10px; overflow: auto; flex: 1; }
- .nav-label {
- font-size: 11px; color: #8a94a0; letter-spacing: 0.08em;
- padding: 10px 12px 6px; text-transform: uppercase;
- }
- .side-item {
- display: flex; align-items: center; gap: 10px;
- padding: 9px 12px; margin-bottom: 2px;
- border-radius: 8px; color: #3d4754; width: 100%; text-align: left; font-size: 13.5px;
- }
- .side-item svg { width: 16px; height: 16px; flex: none; stroke: currentColor; fill: none; stroke-width: 1.8; }
- .side-item:hover { background: #f3f6f4; }
- .side-item.active { background: var(--green-soft); color: var(--green-dark); font-weight: 600; }
- .side-dot { display: none; }
- .badge-n {
- margin-left: auto; min-width: 18px; height: 18px; padding: 0 5px;
- border-radius: 9px; background: var(--warn); color: #fff;
- font-size: 10px; display: grid; place-items: center; font-weight: 600;
- }
- .tag {
- margin-left: auto; font-size: 10px; color: var(--green); background: var(--green-soft);
- border-radius: 4px; padding: 1px 5px; font-weight: 600;
- }
- .nav-group { margin-bottom: 2px; }
- .nav-group > .side-item .chev {
- margin-left: auto; width: 14px; height: 14px; opacity: .45;
- transition: transform .15s; flex: none;
- }
- .nav-group.open > .side-item .chev { transform: rotate(90deg); }
- .nav-sub { display: none; padding: 0 0 6px; }
- .nav-group.open > .nav-sub { display: block; }
- .side-sub { padding: 8px 12px 8px 38px; font-size: 13px; margin-bottom: 1px; }
- .coming {
- background: var(--white); border: 1px solid var(--line); border-radius: var(--radius);
- box-shadow: var(--shadow); padding: 56px 24px; text-align: center;
- }
- .coming h2 { font-size: 16px; margin-bottom: 8px; }
- .coming p { color: var(--muted); font-size: 13px; }
- .main { min-width: 0; overflow: visible; }
- .topbar {
- height: var(--header);
- background: var(--white);
- border-bottom: 1px solid var(--line);
- display: flex; align-items: center; gap: 16px;
- padding: 0 28px; position: sticky; top: 0; z-index: 5;
- }
- .crumb { font-size: 13px; color: var(--muted); }
- .crumb b { color: var(--ink); font-weight: 600; }
- .search {
- margin-left: auto; display: flex; align-items: center; gap: 8px;
- background: var(--paper); border: 1px solid var(--line);
- border-radius: 8px; padding: 6px 10px; width: 260px;
- }
- .search input, .top-search {
- border: 0; outline: 0; background: transparent; width: 100%; font-size: 13px; color: var(--ink);
- }
- .search input::placeholder, .top-search::placeholder { color: #8a94a0; }
- .user {
- display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); flex-shrink: 0;
- }
- .avatar {
- width: 28px; height: 28px; border-radius: 50%;
- background: #d7eee0; color: var(--green-dark);
- display: grid; place-items: center; font-weight: 700; font-size: 12px;
- }
- .page { padding: 24px 28px 40px; overflow: visible; }
- .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; }
- #mod-fleet > .subnav { display: none; }
- .subnav button { height: 32px; padding: 0 12px; border-radius: 8px; color: #4b5563; font-weight: 500; white-space: nowrap; }
- .subnav button.on { background: var(--green-soft); color: var(--green-dark); font-weight: 650; }
- .page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; }
- .page-head h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
- .page-head p { color: var(--muted); margin-top: 6px; font-size: 13px; }
- .actions { display: flex; gap: 8px; flex-wrap: wrap; }
- .btn { height: 32px; padding: 0 12px; border-radius: 8px; font-weight: 500; border: 1px solid var(--line); background: var(--white); }
- .btn:hover { border-color: var(--green-line); }
- .btn-primary { background: var(--green); color: #fff; border-color: var(--green); }
- .btn-primary:hover { background: var(--green-dark); }
- .btn-sm { height: 26px; padding: 0 8px; font-size: 12px; }
- .alerts { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
- .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; }
- .alert.red { background: var(--warn-soft); color: var(--warn); border-color: #f5c5c2; }
- .alert.ok { background: var(--green-soft); color: var(--green-dark); border-color: var(--green-line); }
- .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 22px; }
- .stats.five { grid-template-columns: repeat(5, 1fr); }
- .stats.three { grid-template-columns: repeat(3, 1fr); }
- .stat { background: var(--white); border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; box-shadow: var(--shadow); }
- .stat .k { color: var(--muted); font-size: 12px; }
- .stat .v { font-size: 26px; font-weight: 700; margin: 4px 0 2px; letter-spacing: -.03em; }
- #taskStats .stat .k {
- display: flex;
- align-items: baseline;
- gap: 10px;
- white-space: nowrap;
- }
- #taskStats .stat .k .stat-name {
- font-weight: 650;
- font-size: 13px;
- }
- #taskStats .stat .k .stat-name.wait { color: var(--amber); }
- #taskStats .stat .k .stat-name.run { color: var(--blue); }
- #taskStats .stat .k .stat-name.done { color: var(--green); }
- #taskStats .stat .k .stat-name.all { color: var(--ink); }
- #taskStats .stat .k .kinds { font-weight: 400; color: var(--muted); }
- #taskStats .stat .v { font-size: 22px; letter-spacing: 0; white-space: nowrap; margin-bottom: 0; }
- #taskStats .stat .v i,
- #taskStats .stat .k .kinds i {
- font-style: normal;
- font-weight: 400;
- color: #b4bcc6;
- margin: 0 .42em;
- vertical-align: .12em;
- }
- #taskStats .stat .v i { font-size: 12px; }
- #taskStats .stat .k .kinds i { font-size: 10px; }
- .stat .s { font-size: 12px; color: var(--muted); margin-top: 0; }
- .plan-n-link {
- display: inline-flex; align-items: center; height: 22px; padding: 0 8px;
- border-radius: 6px; border: 1px solid var(--green-line); background: var(--green-soft);
- color: var(--green-dark); font-weight: 650; font-size: 12px; cursor: pointer;
- }
- .plan-n-link:hover { background: #d7eee0; }
- .plan-n-link.off { background: #f3f4f6; border-color: #e5e7eb; color: #6b7280; }
- #vehPlanBody tfoot td {
- font-weight: 700; background: #f7faf8; border-top: 1px solid var(--line);
- }
- .grid { display: grid; grid-template-columns: 1.35fr .9fr; gap: 12px; align-items: start; }
- .stack { display: flex; flex-direction: column; gap: 12px; }
- .card { background: var(--white); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
- .card-h { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border-bottom: 1px solid var(--line); }
- .card-h h2 { font-size: 14px; font-weight: 650; }
- .card-b { padding: 12px 14px; }
- .quick { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
- .qbtn {
- text-align: left; border: 1px solid var(--line); border-radius: var(--radius);
- padding: 14px; background: var(--white); min-height: 88px;
- box-shadow: var(--shadow); transition: border-color .15s, transform .15s;
- }
- .qbtn:hover { border-color: var(--green-line); transform: translateY(-1px); background: var(--white); }
- .qbtn .ico {
- width: 40px; height: 40px; border-radius: 10px; display: grid; place-items: center;
- margin-bottom: 8px; font-weight: 700; font-size: 12px;
- background: var(--green-soft); color: var(--green-dark);
- }
- .qbtn h3 { font-size: 13.5px; font-weight: 650; }
- .qbtn p { color: var(--muted); font-size: 12px; margin-top: 4px; line-height: 1.45; }
- table { width: 100%; border-collapse: collapse; }
- th, td { text-align: left; padding: 8px 6px; border-bottom: 1px solid #eef1f4; font-size: 12px; }
- th { color: var(--muted); font-weight: 500; }
- tr:hover td { background: #f8faf9; }
- .st { display: inline-flex; align-items: center; height: 20px; padding: 0 7px; border-radius: 99px; font-size: 11px; font-weight: 600; }
- .st-run { background: var(--green-soft); color: var(--green-dark); }
- .st-fix { background: #fff7ed; color: var(--amber); }
- .st-stop { background: #f3f4f6; color: #4b5563; }
- .st-due { background: #fef2f2; color: #b91c1c; }
- .timeline { display: flex; flex-direction: column; gap: 0; }
- .tl { display: grid; grid-template-columns: 72px 1fr; gap: 8px; padding: 8px 0; border-bottom: 1px solid #eef1f4; }
- .tl:last-child { border: 0; }
- .tl .t { color: var(--muted); font-size: 11px; }
- .tl b { font-weight: 600; }
- .tl .d { color: var(--muted); }
- .bars { display: flex; flex-direction: column; gap: 8px; }
- .bar-row { display: grid; grid-template-columns: 88px 1fr 64px; gap: 8px; align-items: center; }
- .bar-track { height: 8px; background: #eef1f4; border-radius: 99px; overflow: visible; position: relative; }
- .bar-fill { height: 100%; background: linear-gradient(90deg, #167c4a, #22a05d); border-radius: 99px; }
- .bar-fill.warn { background: linear-gradient(90deg, #e24b4a, #f97316); }
- .bar-mark { position: absolute; top: -3px; height: 14px; width: 2px; background: var(--warn); border-radius: 1px; }
- .bar-row .n { color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
- .rate-over { color: #b91c1c; font-weight: 650; }
- .filters { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
- .filters input, .filters select, .form input, .form select, .form textarea {
- height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; background: #fff; outline: none;
- }
- .form textarea { height: 64px; padding: 8px 10px; resize: vertical; }
- .form { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
- .form label { display: flex; flex-direction: column; gap: 4px; color: var(--muted); font-size: 12px; }
- .form .full { grid-column: 1 / -1; }
- .form .span2 { grid-column: span 2; }
- .fuel-plate-grid {
- display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px;
- }
- .fuel-plate-card {
- background: var(--white); border: 1px solid var(--line); border-radius: var(--radius);
- padding: 14px 16px; box-shadow: var(--shadow); min-width: 0; cursor: pointer;
- }
- .fuel-plate-card:hover { border-color: var(--blue); }
- .fuel-plate-card .plate { font-size: 15px; font-weight: 700; color: var(--ink); letter-spacing: .01em; white-space: nowrap; }
- .fuel-plate-card .meta { color: var(--muted); font-size: 12px; margin: 6px 0 10px; }
- .fuel-plate-card .nums { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
- .fuel-plate-card .nums.four { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
- .fuel-plate-card .nums.four b { font-size: 16px; }
- .fuel-plate-card .nums b { display: block; font-size: 18px; font-weight: 700; letter-spacing: -.03em; }
- .fuel-plate-card .nums span { color: var(--muted); font-size: 11px; }
- .fuel-plate-card .mom { display: block; font-size: 11px; font-style: normal; font-weight: 650; margin-top: 2px; }
- .fuel-plate-card .mom.up { color: #c2410c; }
- .fuel-plate-card .mom.down { color: var(--green-dark); }
- .fuel-plate-card .mom.flat { color: var(--muted); font-weight: 500; }
- @media (max-width: 1200px) { .fuel-plate-grid { grid-template-columns: repeat(2, 1fr); } }
- .file-box {
- border: 1px dashed var(--green-line); border-radius: 8px; background: #f7fbf8;
- padding: 8px 10px; min-height: 64px; display: flex; flex-direction: column; gap: 6px;
- }
- .file-box input[type=file] { height: auto; padding: 0; border: 0; background: transparent; }
- .file-hint { color: var(--amber); font-size: 11px; }
- .task-photo-row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 10px; }
- .task-photo-up { width: 132px; height: 96px; flex-shrink: 0; }
- .task-photo-thumbs { display: flex; flex-wrap: wrap; gap: 8px; }
- .task-photo-thumbs button { padding: 0; border: 0; background: transparent; cursor: pointer; }
- .task-photo-thumbs img {
- width: 88px; height: 88px; object-fit: cover; border-radius: 8px;
- border: 1px solid var(--line); display: block;
- }
- .chips { display: flex; flex-wrap: wrap; gap: 6px; }
- .chip {
- display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 99px;
- background: var(--green-soft); color: var(--green-dark); font-size: 11px; font-weight: 600; border: 0;
- }
- .chip.ghost { background: #f3f4f6; color: #4b5563; cursor: default; }
- .chip:disabled { opacity: .65; cursor: default; }
- dialog {
- border: 1px solid var(--line); border-radius: 12px; padding: 0; box-shadow: var(--shadow);
- max-width: min(720px, 92vw); width: 720px;
- }
- dialog.apply-dlg { max-width: min(760px, 94vw); width: 760px; }
- dialog.apply-dlg.wide-dlg { max-width: min(920px, 96vw); width: 920px; }
- .task-line-wrap { overflow: auto; border: 1px solid var(--line); border-radius: 8px; }
- .task-line-table { width: 100%; border-collapse: collapse; font-size: 12px; }
- .task-line-table th, .task-line-table td { border-bottom: 1px solid #eef1f4; padding: 7px 8px; text-align: left; }
- .task-line-table thead th { background: #f7faf8; color: #5b6675; font-weight: 600; }
- .task-sheet {
- background: #fff; border: 1px solid #222; padding: 16px 18px 12px; font-size: 13px; color: #111;
- }
- .task-sheet h3 { text-align: center; font-size: 20px; letter-spacing: .18em; margin: 0 0 6px; font-weight: 700; }
- .task-sheet .ts-no { text-align: right; font-size: 12px; margin: 0 2px 6px; }
- .task-sheet table { width: 100%; border-collapse: collapse; table-layout: fixed; }
- .task-sheet td, .task-sheet th { border: 1px solid #333; padding: 7px 8px; vertical-align: middle; }
- .task-sheet th { text-align: center; font-weight: 650; background: #fff; }
- .task-sheet td.idx, .task-sheet td.center { text-align: center; }
- .task-sheet .ts-foot { text-align: center; letter-spacing: .08em; }
- .task-sheet .ts-checks { display: inline-flex; align-items: center; gap: 14px; margin-left: 16px; }
- .task-sheet .ts-box {
- display: inline-block; width: 12px; height: 12px; border: 1px solid #333;
- margin-right: 4px; vertical-align: -1px; background: #fff;
- }
- .task-sheet .ts-box.on { background: #111; }
- dialog::backdrop { background: rgba(28, 36, 48, .35); }
- .dlg-h { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border-bottom: 1px solid var(--line); }
- .dlg-h.green { background: var(--green-soft); }
- .dlg-h h3 { font-size: 14px; }
- .dlg-x { width: 28px; height: 28px; border-radius: 6px; color: #5b6572; font-size: 18px; line-height: 1; }
- .dlg-x:hover { background: #e4eee7; }
- .dlg-b { padding: 14px; max-height: 70vh; overflow: auto; }
- .dlg-b img { max-width: 100%; border-radius: 8px; display: block; }
- .dlg-f { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--line); }
- .apply { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 28px; }
- .apply .row { display: grid; grid-template-columns: 88px 1fr; gap: 10px; align-items: start; }
- .apply .full { grid-column: 1 / -1; }
- .apply .lab { color: var(--ink); padding-top: 7px; text-align: right; }
- .apply input, .apply select, .apply textarea {
- width: 100%; height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; background: #fff; outline: none;
- }
- .apply textarea { height: 88px; padding: 8px 10px; resize: vertical; }
- .star { color: #e24b4a; margin-right: 2px; }
- .stepper { display: flex; height: 32px; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: #fff; }
- .stepper button { width: 36px; background: #f7f8fa; color: #6a7380; }
- .stepper button:hover { background: #eef1f4; }
- .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; }
- .upload {
- width: 132px; height: 108px; border: 1px dashed var(--green-line); border-radius: 10px;
- background: #f4faf6; display: flex; flex-direction: column; align-items: center; justify-content: center;
- color: var(--green); gap: 4px; cursor: pointer; font-weight: 500;
- }
- .ocr-box .upload { width: 100%; height: 72px; }
- .upload:hover { background: #ebf7ef; }
- .upload input { display: none; }
- .upload .plus { font-size: 22px; line-height: 1; font-weight: 400; }
- .hint { color: var(--muted); font-size: 11px; margin-top: 8px; }
- .hidden { display: none !important; }
- .toast {
- position: fixed; right: 20px; bottom: 20px; background: #1c2430; color: #fff;
- padding: 10px 14px; border-radius: 8px; display: none; z-index: 50;
- }
- .toast.show { display: block; }
- .pie-wrap { display: flex; align-items: center; gap: 16px; }
- .legend { display: flex; flex-direction: column; gap: 6px; }
- .legend span { display: flex; align-items: center; gap: 6px; }
- .sw { width: 8px; height: 8px; border-radius: 2px; }
- .cell-no { color: var(--green-dark); font-weight: 600; }
- .muted { color: var(--muted); }
- .row-link { cursor: pointer; }
- .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; }
- .need-tag.bao { background: #fff7ed; color: var(--amber); }
- .remain { font-variant-numeric: tabular-nums; font-weight: 650; }
- .remain.warn { color: var(--amber); }
- .remain.bad { color: #b91c1c; }
- .ocr-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
- .ocr-box {
- border: 1px dashed var(--green-line); border-radius: 12px; background: #f7fbf8; padding: 12px;
- min-height: 148px; display: flex; flex-direction: column; gap: 8px;
- }
- .ocr-box.buy { border-color: var(--green-line); background: #f4faf6; }
- .ocr-box.trans { border-color: #b7cce8; background: #f3f7fc; }
- .ocr-box.drag { outline: 2px dashed var(--green); outline-offset: 2px; }
- .ocr-box.trans.drag { outline-color: var(--blue); }
- .ocr-box.buy h4 { color: var(--green-dark); }
- .ocr-box.trans h4 { color: var(--blue); }
- .ocr-box.trans .upload { border-color: #b7cce8; background: #eef4fb; color: var(--blue); }
- .ocr-box.trans .upload:hover { background: #e4eef8; }
- .split-h .col-buy h4 { color: var(--green-dark); border-bottom-color: var(--green-line); }
- .split-h .col-trans h4 { color: var(--blue); border-bottom-color: #c5d6ee; }
- dialog:not([open]) { display: none; }
- .ocr-box h4 { font-size: 13px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
- .ocr-preview { width: 100%; max-height: 120px; object-fit: contain; border-radius: 8px; background: #fff; border: 1px solid var(--line); }
- .ocr-status { font-size: 12px; color: var(--amber); min-height: 18px; }
- .ocr-status.ok { color: var(--green-dark); }
- .split-h { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 4px; }
- .split-h h4 { font-size: 13px; margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1px solid var(--line); }
- .kv { display: grid; grid-template-columns: 108px 1fr; gap: 6px 10px; font-size: 12px; }
- .kv .lab { color: var(--muted); text-align: right; }
- .kv .val { font-weight: 500; }
- .sheet { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
- .sheet .full-card { grid-column: 1 / -1; }
- .cert-duo {
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(0, 3fr);
- gap: 16px;
- align-items: stretch;
- width: 100%;
- margin-top: 24px;
- }
- .cert-pane.buy .kv { grid-template-columns: 72px 1fr; }
- .cert-thumb {
- display: block; width: 100%; margin-top: 12px; padding: 6px;
- border: 1px solid var(--line); border-radius: 8px; background: #fff;
- cursor: zoom-in; text-align: center;
- }
- .cert-thumb:hover { border-color: var(--green-line); }
- .cert-pane.trans .cert-thumb:hover { border-color: var(--blue-line); }
- .cert-thumb img {
- display: block; width: 100%; max-height: 140px; object-fit: contain; margin: 0 auto;
- }
- dialog.cert-img-dlg {
- max-width: min(1100px, 96vw); width: auto; background: transparent;
- border: 0; box-shadow: none; padding: 0;
- }
- dialog.cert-img-dlg img {
- display: block; max-width: 94vw; max-height: 88vh; object-fit: contain;
- border-radius: 10px; background: #111; box-shadow: 0 16px 48px rgba(0,0,0,.35);
- }
- dialog.cert-img-dlg::backdrop { background: rgba(28, 36, 48, .72); }
- .cert-pane {
- border-radius: var(--radius);
- padding: 10px 10px 12px;
- min-width: 0;
- display: flex;
- flex-direction: column;
- }
- .cert-pane > .card { flex: 1; }
- .cert-pane.buy {
- background: linear-gradient(180deg, #e8f6ee 0%, #f4faf6 48px, #f7fbf8 100%);
- border: 1px solid var(--green-line);
- }
- .cert-pane.trans {
- background: linear-gradient(180deg, var(--blue-soft) 0%, #f3f7fc 48px, #f6f9fd 100%);
- border: 1px solid var(--blue-line);
- }
- .trans-card-row {
- display: flex;
- gap: 12px;
- align-items: stretch;
- min-width: 0;
- }
- .trans-card-row > .card {
- flex: 1 1 0;
- min-width: 0;
- margin: 0;
- }
- .trans-card-row .kv { grid-template-columns: 76px 1fr; }
- .cert-pane-h {
- display: flex; align-items: center; gap: 8px;
- padding: 4px 6px 10px;
- }
- .cert-pane-h h2 { font-size: 14px; font-weight: 650; }
- .cert-pane-h .muted { margin-left: auto; font-size: 12px; }
- .cert-tag {
- display: inline-flex; align-items: center; height: 22px; padding: 0 8px;
- border-radius: 6px; font-size: 11px; font-weight: 700; letter-spacing: 0.04em;
- }
- .cert-tag.buy { background: var(--green); color: #fff; }
- .cert-tag.trans { background: var(--blue); color: #fff; }
- .cert-pane.buy .card { border-color: var(--green-line); }
- .cert-pane.trans .card { border-color: var(--blue-line); }
- .cert-pane .card-h { background: #fff; }
- .cert-pane.buy .card-h { border-bottom-color: var(--green-line); }
- .cert-pane.trans .card-h { border-bottom-color: var(--blue-line); }
- .stat.tone-buy { border-color: var(--green-line); box-shadow: none; }
- .stat.tone-buy .k { color: var(--green-dark); }
- .stat.tone-trans { border-color: var(--blue-line); box-shadow: none; }
- .stat.tone-trans .k { color: var(--blue); }
- .qty-bar { height: 8px; background: #eef1f4; border-radius: 99px; overflow: hidden; margin-top: 8px; }
- .qty-fill { height: 100%; background: linear-gradient(90deg, #167c4a, #22a05d); }
- .qty-fill.warn { background: linear-gradient(90deg, #e24b4a, #f97316); }
- .warn-qty-table { width: 100%; border-collapse: collapse; }
- .warn-qty-table th, .warn-qty-table td {
- padding: 8px 10px; font-size: 12px; text-align: left; border-bottom: 1px solid #eef1f4; vertical-align: middle;
- }
- .warn-qty-table thead th { color: #5b6675; font-weight: 600; background: #f7faf8; }
- .warn-qty-edit { display: inline-flex; align-items: center; gap: 6px; }
- .warn-qty-edit input {
- width: 88px; height: 28px; border: 1px solid var(--line); border-radius: 6px; padding: 0 8px;
- }
- .erp-page { margin-top: -6px; }
- .rel-flow {
- display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center;
- font-size: 12.5px; color: var(--muted); margin: 0 0 12px; padding: 8px 12px;
- background: #fff; border: 1px dashed var(--green-line); border-radius: 10px;
- }
- .rel-flow b { color: var(--ink); font-weight: 650; }
- .rel-flow em { font-style: normal; color: var(--green); font-weight: 650; }
- .cert-steps {
- display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px;
- }
- .cert-step {
- background: #fff; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
- display: flex; gap: 10px; align-items: flex-start;
- }
- .cert-step .n {
- width: 22px; height: 22px; border-radius: 99px; flex: none;
- background: var(--green-soft); color: var(--green-dark);
- display: grid; place-items: center; font-weight: 750; font-size: 12px;
- }
- .cert-step { cursor: pointer; }
- .cert-step:hover { border-color: var(--green-line); }
- .cert-step.on { border-color: var(--green); background: #f4faf6; }
- .cert-step.warn { border-color: #fdba74; background: #fff7ed; }
- .cert-step.warn .n { background: #fff7ed; color: #9a3412; }
- .cert-step b { display: block; font-size: 13px; }
- .cert-step p { color: var(--muted); font-size: 12px; margin-top: 2px; line-height: 1.45; }
- .cert-step .s { margin-top: 4px; font-size: 12px; color: var(--ink); font-weight: 650; }
- @media (max-width: 980px) { .cert-steps { grid-template-columns: 1fr; } }
- .extra-trans {
- margin-top: 12px; padding: 12px; border: 1px dashed #b7cce8; border-radius: 10px; background: #f6f9fd;
- }
- .extra-trans.drag { outline: 2px dashed var(--blue); outline-offset: 2px; }
- .extra-trans h4 { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
- .veh-over td { background: #fff5f5; }
- .warn-dot { color: #b91c1c; font-weight: 700; }
- .veh-board { overflow: auto; }
- .link-preview {
- margin: 0 0 12px; padding: 10px 12px; border-radius: 10px;
- background: #f4faf6; border: 1px solid var(--green-line); font-size: 13px; line-height: 1.55;
- }
- .link-preview.warn { background: #fff7ed; border-color: #fdba74; }
- .link-preview.over { background: #fef2f2; border-color: #fecaca; color: #9f1239; }
- .stat.stat-click { cursor: pointer; }
- .stat.stat-click:hover { border-color: var(--green-line); }
- .wait-link-box { margin-bottom: 12px; }
- .wait-link-box .card-h { align-items: flex-start; flex-wrap: wrap; }
- .wait-link-box .card-h h2 { flex: none; white-space: nowrap; }
- .wait-link-box .card-h .muted { flex: 1; min-width: 180px; }
- .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; }
- .match-rec.veh { background: #eef4fb; color: var(--blue); }
- .match-rec.warn { background: #fff7ed; color: var(--amber); }
- .match-rec.in { background: #eef4fb; color: var(--blue); }
- .match-rec.out { background: var(--green-soft); color: var(--green-dark); }
- .match-rec.direct { background: #f5f0ff; color: #6d28d9; }
- #certWaitCard { overflow: visible; }
- .wait-scene-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
- .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; }
- .wait-table, table.erp-table.wait-table { width: 100%; min-width: 100%; border-collapse: collapse; }
- .wait-table th, .wait-table td {
- padding: 8px 10px; font-size: 12px; vertical-align: middle;
- white-space: nowrap; border-bottom: 1px solid #eef1f4; background: #fff;
- }
- .wait-table thead th {
- position: sticky; top: 0; z-index: 1; background: #f7faf8;
- color: #5b6675; font-weight: 600;
- }
- .wait-table tbody tr:hover td { background: #f8faf9; }
- .wait-table tr.warn-row td { background: #fffdf8; }
- .wait-table tr.over-row td { background: #fff8f8; }
- .wait-table .cell-name, .wait-table td.wrap {
- white-space: normal; max-width: 140px; line-height: 1.4;
- }
- .wait-table .cell-remain, .wait-table td.remain-cell {
- white-space: normal; min-width: 140px; max-width: 200px; line-height: 1.4;
- }
- .wait-table select {
- display: block; height: 28px; min-width: 148px; max-width: 210px;
- border: 1px solid var(--line); border-radius: 6px; padding: 0 6px; background: #fff;
- }
- .wait-table .hint { margin-top: 4px; }
- .wait-table .op, .wait-table td.ops { white-space: nowrap; }
- .erp-table.wait-table .idx,
- .erp-table.wait-table .col-no {
- position: static; left: auto; box-shadow: none;
- }
- .erp-table.wait-table .col-op-buy,
- .erp-table.wait-table .col-op-trans {
- position: sticky; right: auto; min-width: 88px; z-index: 5; text-align: center;
- }
- .erp-table.wait-table .col-op-buy { left: 0; }
- .erp-table.wait-table .col-op-trans { left: 88px; box-shadow: 6px 0 8px -6px rgba(28,36,48,.12); }
- .erp-table.wait-table thead .col-op-buy,
- .erp-table.wait-table thead .col-op-trans { z-index: 6; background: #f7faf8; }
- .erp-table.wait-table tbody .col-op-buy,
- .erp-table.wait-table tbody .col-op-trans { background: #fff; }
- .wait-table .col-op-buy .btn,
- .wait-table .col-op-trans .btn { margin: 0; width: 100%; white-space: nowrap; }
- .wait-table .clip { max-width: none; overflow: visible; text-overflow: unset; }
- .wait-table th.col-ship,
- .wait-table td.col-ship {
- white-space: normal; max-width: none; min-width: 196px; width: 18%; line-height: 1.4; overflow: visible;
- }
- .wait-op-no { font-size: 11px; color: var(--muted); line-height: 1.3; word-break: break-all; }
- .btn.wait-btn-buy {
- background: #e8f6ee; border-color: var(--green-line); color: var(--green-dark);
- }
- .btn.wait-btn-buy:hover { background: #d7eee0; }
- .btn.wait-btn-trans {
- background: #eef4fb; border-color: var(--blue-line); color: var(--blue);
- }
- .btn.wait-btn-trans:hover { background: #e0ebf7; }
- .btn.wait-btn-buy.wait-on,
- .btn.wait-btn-trans.wait-on {
- background: #f3f4f6; border-color: #e5e7eb; color: #6b7280; font-weight: 500;
- }
- .btn.wait-btn-buy.wait-on:hover,
- .btn.wait-btn-trans.wait-on:hover { background: #eaecef; border-color: #d1d5db; color: #4b5563; }
- dialog.wait-link-dlg { max-width: min(1180px, 96vw); width: 1180px; }
- .wait-link-dlg .dlg-b { overflow: auto; }
- .wait-link-dlg .cert-pane {
- margin-bottom: 0; overflow: visible; min-width: 0; height: auto;
- box-sizing: border-box;
- }
- .wait-link-dlg .cert-pane > .card,
- .wait-link-dlg .wait-trans-sheet {
- margin: 0; box-shadow: none; min-width: 0; flex: none;
- overflow: visible; border-radius: 10px; box-sizing: border-box;
- }
- .wait-link-dlg .wait-trans-sheet + .wait-trans-sheet { margin-top: 12px; }
- .wait-link-dlg .wait-trans-sheet.hit { border-color: var(--blue-line); }
- .wait-link-dlg .qty-bar { margin-bottom: 2px; }
- .wait-link-dlg .wait-cert-pick {
- display: flex; align-items: center; gap: 8px; cursor: pointer; min-width: 0;
- }
- .wait-link-dlg .wait-cert-pick input { margin: 0; }
- .wait-link-dlg .card-h .btn { margin-left: auto; }
- .wait-link-dlg .kv .val { overflow-wrap: anywhere; word-break: break-word; }
- .wait-plan-bar {
- display: flex; flex-wrap: wrap; gap: 6px 16px; align-items: center;
- padding: 10px 12px; margin-bottom: 12px;
- background: #f7faf8; border: 1px solid var(--green-line); border-radius: 10px; font-size: 12px;
- }
- .wait-plan-bar b { font-weight: 650; }
- .wait-dlg-split { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; align-items: stretch; }
- .wait-dlg-split > * { min-width: 0; }
- @media (max-width: 900px) { .wait-dlg-split { grid-template-columns: 1fr; } }
- .wait-info-card {
- border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: #fff; margin-bottom: 10px;
- }
- .wait-info-card.buy { border-color: var(--green-line); background: #f7fbf8; }
- .wait-info-card.trans { border-color: #c5d6ee; background: #f6f9fd; }
- .wait-info-card.hit { box-shadow: inset 0 0 0 1px var(--green); }
- .wait-info-card h4 {
- display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 13px;
- }
- .wait-info-card .kv { grid-template-columns: 78px 1fr 78px 1fr; gap: 4px 8px; }
- .wait-info-stack { max-height: 56vh; overflow: auto; padding: 8px 6px; min-width: 0; }
- .wait-scene-tabs button {
- height: 26px; padding: 0 10px; border-radius: 6px; border: 1px solid var(--line); background: #fff; color: #4b5563;
- }
- .wait-scene-tabs button.on { background: var(--green-soft); color: var(--green-dark); border-color: var(--green-line); font-weight: 650; }
- .qty-formula {
- display: flex; flex-wrap: wrap; gap: 8px 18px; margin-bottom: 12px;
- padding: 10px 12px; background: #f7faf8; border: 1px dashed var(--green-line); border-radius: 10px; font-size: 13px;
- }
- .qty-formula b { font-variant-numeric: tabular-nums; }
- .qty-formula .bad { color: var(--warn); }
- .qty-formula .warn { color: var(--amber); }
- .link-sec h4 { font-size: 13px; margin: 4px 0 8px; }
- .link-sec + .link-sec { margin-top: 14px; }
- .ct-mini { font-size: 11px; color: var(--muted); line-height: 1.45; }
- .erp-tabs-row {
- display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
- border-bottom: 1px solid var(--line); margin-bottom: 8px;
- }
- .plan-tabs { display: flex; gap: 2px; flex-wrap: wrap; }
- .plan-tabs button {
- height: 36px; padding: 0 14px; border: 0; background: transparent;
- color: #4b5563; font-weight: 500; border-bottom: 2px solid transparent; margin-bottom: -1px;
- }
- .plan-tabs button.on { color: var(--green); font-weight: 650; border-bottom-color: var(--green); }
- .plan-table tr.row-wait td { background: #fff8f8; }
- .plan-foot {
- display: flex; justify-content: space-between; align-items: center;
- padding: 8px 14px; border-top: 1px solid var(--green-line); background: #eaf6ef;
- }
- .erp-toolbar {
- display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
- padding: 4px 0 8px;
- }
- .erp-toolbar .btn { display: inline-flex; align-items: center; gap: 4px; }
- .erp-toolbar .split { width: 1px; height: 16px; background: var(--line); margin: 0 2px; }
- .erp-toolbar .right { margin-left: auto; display: flex; align-items: center; gap: 6px; }
- .erp-toolbar input[type=search], .erp-toolbar input[type=text] {
- height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; width: 168px;
- }
- .erp-toolbar .btn.on { background: var(--green-soft); border-color: var(--green-line); color: var(--green-dark); }
- .btn-danger { background: var(--warn); color: #fff; border-color: var(--warn); }
- .btn-danger:hover { background: #c13b35; border-color: #c13b35; }
- .plan-action-row { align-items: center; border-bottom: 0; margin-bottom: 0; gap: 10px; }
- .plan-action-row .plan-tabs button { height: 32px; padding: 0 10px; }
- .quote-search.plan-search { width: 280px; margin-left: 0; }
- .plan-toolbar { padding: 6px 0 8px; }
- #planKpis { margin-bottom: 12px; grid-template-columns: repeat(4, 1fr); }
- #planKpis .stat {
- width: 100%; text-align: left;
- display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 10px;
- min-height: 96px; padding: 18px 18px;
- transition: border-color .15s, background .15s;
- }
- #planKpis .stat .k { color: var(--ink); font-size: 13px; font-weight: 650; white-space: nowrap; }
- #planKpis .stat .v {
- margin: 0; padding: 0; border: 0; background: none;
- font-size: 28px; line-height: 1; font-weight: 700; letter-spacing: -.03em;
- cursor: pointer; color: var(--amber);
- }
- #planKpis .stat .v:hover { opacity: .85; }
- #planKpis .today-bits {
- display: flex; flex-wrap: nowrap; align-items: baseline; gap: 14px;
- white-space: nowrap;
- }
- #planKpis .today-bits button {
- margin: 0; padding: 0; border: 0; background: none; cursor: pointer;
- font-size: 13px; font-weight: 500; color: var(--muted); line-height: 1;
- }
- #planKpis .today-bits button b { font-size: 22px; font-weight: 700; color: var(--green); margin: 0 3px; }
- #planKpis .today-bits button.open b { color: var(--amber); }
- #planKpis .today-bits button:hover { color: var(--ink); }
- #planKpis .today-bits button.on { color: var(--ink); }
- #planKpis .stat.tone-ctrl .v { color: var(--warn); }
- #planKpis .stat.tone-arch .v { color: var(--blue); }
- #planKpis .stat.on.tone-wait { border-color: #e8c56b; background: var(--amber-soft); }
- #planKpis .stat.on.tone-today { border-color: var(--green-line); background: var(--green-soft); }
- #planKpis .stat.on.tone-ctrl { border-color: #f5c5c2; background: var(--warn-soft); }
- #planKpis .stat.on.tone-arch { border-color: var(--blue-line); background: var(--blue-soft); }
- .erp-hint { font-size: 12px; color: var(--muted); margin-left: auto; }
- .erp-hint b { color: var(--ink); font-weight: 600; }
- .erp-card { overflow: hidden; }
- .erp-table-wrap { overflow: auto; max-height: calc(100vh - 228px); }
- #quoteStats, #ctStats { display: block; margin-bottom: 12px; }
- .wait-banner {
- width: 100%; display: flex; align-items: center; gap: 10px;
- padding: 12px 16px; text-align: left; cursor: pointer;
- background: #fff; border: 1px solid #f3ddb0; border-radius: var(--radius);
- box-shadow: var(--shadow);
- transition: border-color .15s, background .15s, box-shadow .15s;
- }
- .wait-banner:hover { border-color: #e8c56b; background: var(--amber-soft); }
- .wait-banner.on { border-color: #e8c56b; background: var(--amber-soft); }
- .wait-banner.empty { border-color: var(--green-line); }
- .wait-banner.empty:hover, .wait-banner.empty.on { background: var(--green-soft); border-color: var(--green); }
- .wb-ico {
- width: 36px; height: 36px; border-radius: 10px; flex: none;
- display: grid; place-items: center; background: var(--amber-soft); color: var(--amber);
- }
- .wait-banner.empty .wb-ico { background: var(--green-soft); color: var(--green-dark); }
- .wb-ico svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.8; }
- .wb-n { font-size: 28px; font-weight: 700; letter-spacing: -.04em; color: var(--amber); line-height: 1; }
- .wait-banner.empty .wb-n { color: var(--green-dark); }
- .wb-k { font-size: 14px; font-weight: 700; color: var(--ink); line-height: 1.2; }
- .wb-go {
- margin-left: auto; flex: none; height: 28px; padding: 0 12px; border-radius: 999px;
- font-size: 12px; font-weight: 650; display: grid; place-items: center;
- background: var(--amber); color: #fff;
- }
- .wait-banner.empty .wb-go { background: var(--green); }
- #mod-quote .erp-table-wrap, #mod-contract .erp-table-wrap { max-height: calc(100vh - 280px); }
- table.erp-table { min-width: 1680px; border-collapse: separate; border-spacing: 0; }
- .erp-table th, .erp-table td {
- padding: 7px 8px; font-size: 12px; white-space: nowrap; vertical-align: middle;
- border-bottom: 1px solid #eef1f4; background: #fff;
- }
- .erp-table thead th {
- position: sticky; top: 0; z-index: 2; background: #f7faf8;
- color: #5b6675; font-weight: 600; border-bottom: 1px solid var(--line);
- }
- .task-tag {
- display: inline-flex; align-items: center; height: 20px; padding: 0 7px;
- border-radius: 4px; font-size: 11px; font-weight: 650;
- }
- .task-tag.dilute { background: #e8f1fb; color: var(--blue); }
- .task-tag.refine { background: #f3eefb; color: #7c5cbf; }
- .task-tag.pack { background: #fff1e0; color: #c47d11; }
- .erp-table .num { font-variant-numeric: tabular-nums; }
- .erp-table .qty { color: var(--green); font-weight: 650; }
- .erp-table .out-qty { color: var(--blue); }
- .erp-table .chk, .erp-table .idx { width: 36px; text-align: center; color: var(--muted); padding-left: 8px; padding-right: 4px; }
- .erp-table .chk { position: sticky; left: 0; z-index: 3; }
- .erp-table .idx { position: sticky; left: 36px; z-index: 3; }
- .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); }
- .erp-table thead .chk, .erp-table thead .idx, .erp-table thead .col-no { z-index: 4; background: #f7faf8; }
- .erp-table tbody .chk, .erp-table tbody .idx, .erp-table tbody .col-no { background: inherit; }
- .erp-table .col-op {
- position: sticky; right: 0; z-index: 3; min-width: 88px;
- box-shadow: -6px 0 8px -6px rgba(28,36,48,.12);
- white-space: nowrap;
- }
- .erp-table.task-ops .col-op { min-width: 168px; }
- .erp-table thead .col-op { z-index: 4; background: #f7faf8; }
- .erp-table tbody .col-op { background: inherit; }
- .erp-table .clip { max-width: 148px; overflow: hidden; text-overflow: ellipsis; }
- .erp-table.wait-table .idx,
- .erp-table.wait-table .col-no {
- position: static !important; left: auto !important; box-shadow: none;
- }
- .erp-table.wait-table .col-op-buy,
- .erp-table.wait-table .col-op-trans {
- position: sticky; right: auto; min-width: 88px; z-index: 8; text-align: center;
- pointer-events: auto;
- }
- .erp-table.wait-table .col-op-buy { left: 0; }
- .erp-table.wait-table .col-op-trans { left: 88px; box-shadow: 6px 0 8px -6px rgba(28,36,48,.12); }
- .erp-table.wait-table thead .col-op-buy,
- .erp-table.wait-table thead .col-op-trans { z-index: 9; background: #f7faf8; }
- .erp-table.wait-table tbody .col-op-buy,
- .erp-table.wait-table tbody .col-op-trans { background: #fff; }
- .wait-table .col-op-buy .btn,
- .wait-table .col-op-trans .btn {
- position: relative; z-index: 9; pointer-events: auto; cursor: pointer;
- }
- .kind-tag {
- display: inline-flex; align-items: center; height: 22px; padding: 0 8px;
- border-radius: 4px; font-size: 12px; font-weight: 650;
- }
- .kind-tag.dilute { background: #eaf2fb; color: var(--blue); }
- .kind-tag.refine { background: var(--green-soft); color: var(--green-dark); }
- .kind-tag.pack { background: var(--amber-soft); color: var(--amber); }
- .st-pill {
- display: inline-flex; align-items: center; height: 22px; padding: 0 8px;
- border-radius: 4px; font-size: 12px; font-weight: 600; color: #fff;
- }
- .st-pill.wait { background: #f59e0b; }
- .st-pill.ok { background: #22a05d; }
- .st-pill.done { background: #22a05d; }
- .st-pill.fin { background: #2a6bb3; }
- .st-pill.arch { background: #64748b; }
- .wh-act { font-weight: 650; }
- .wh-act.in { color: var(--green); }
- .wh-act.out { color: var(--blue); }
- .wh-act.none { color: var(--muted); font-weight: 500; }
- .st-pill.void { background: #ef4444; }
- .prod-tag {
- display: inline-flex; align-items: center; height: 18px; padding: 0 6px; margin-left: 4px;
- border-radius: 3px; font-size: 11px; font-weight: 650;
- }
- .prod-tag.orange { background: #fff1e0; color: #c47d11; }
- .prod-tag.green { background: #e8f6ee; color: var(--green-dark); }
- .prod-tag.red { background: #fdecea; color: #d94841; }
- .match-tag { font-weight: 650; }
- .match-tag.yes { color: #c47d11; }
- .match-tag.ok { color: var(--green); }
- .match-tag.no { color: #9aa3ad; }
- .src-cell { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: #5b6675; line-height: 1.35; }
- .src-cell .ico { display: inline-flex; align-items: center; gap: 4px; }
- .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; }
- .op-link { color: var(--green); font-weight: 600; background: none; border: 0; padding: 0 8px 0 0; }
- .op-link:hover { text-decoration: underline; }
- .op-link.muted { color: #9aa3ad; cursor: default; text-decoration: none; }
- .page-size { height: 26px; border: 1px solid var(--line); border-radius: 6px; padding: 0 6px; background: #fff; }
- .query-row { display: none; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
- .query-row.show { display: flex; }
- .query-row select, .query-row input {
- height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; background: #fff;
- }
- .plan-foot .sum { color: var(--green); font-weight: 650; }
- .amt-red { color: #e24b4a; font-weight: 700; font-variant-numeric: tabular-nums; }
- .sale-tag {
- display: inline-flex; align-items: center; height: 22px; padding: 0 8px;
- border-radius: 4px; font-size: 12px; font-weight: 650;
- background: var(--green-soft); color: var(--green-dark);
- }
- .quote-table { min-width: 1280px !important; }
- .quote-table .quote-expand,
- .quote-table .chk,
- .quote-table .col-no {
- position: sticky;
- z-index: 3;
- background: #fff;
- }
- .quote-table .quote-expand {
- left: 0;
- width: 32px; min-width: 32px; max-width: 32px;
- padding-left: 0; padding-right: 0;
- }
- .quote-table .chk {
- left: 32px;
- width: 40px; min-width: 40px; max-width: 40px;
- padding-left: 0; padding-right: 0;
- }
- .quote-table .col-no {
- left: 72px;
- width: 176px; min-width: 176px; max-width: 176px;
- }
- .quote-table thead .quote-expand,
- .quote-table thead .chk,
- .quote-table thead .col-no {
- z-index: 5;
- background: #f7faf8;
- }
- .quote-table tbody .quote-expand,
- .quote-table tbody .chk,
- .quote-table tbody .col-no { background: #fff; }
- .quote-table tr.row-alert .quote-expand,
- .quote-table tr.row-alert .chk,
- .quote-table tr.row-alert .col-no { background: #fdecea; }
- .quote-table .col-title {
- min-width: 168px;
- max-width: 240px;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .quote-table .col-title .quote-link {
- display: inline-block;
- max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: bottom;
- }
- .erp-table tr.row-alert td { background: #fdecea; }
- .erp-table tr.row-alert .chk, .erp-table tr.row-alert .idx, .erp-table tr.row-alert .col-no,
- .erp-table tr.row-alert .quote-expand { background: #fdecea; }
- .quote-expand { color: #9aa3ad; text-align: center; cursor: pointer; }
- .quote-expand.on { color: var(--green-dark); }
- .quote-sub td { background: #f8faf9 !important; font-size: 12px; color: var(--muted); white-space: normal; }
- .quote-link { color: var(--green); font-weight: 650; background: none; border: 0; padding: 0; text-align: left; }
- .quote-link:hover { text-decoration: underline; }
- .th-sort { display: inline-flex; align-items: center; gap: 4px; }
- .th-sort::after { content: ""; width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid #c5ccd4; }
- .quote-search.wide { width: 280px; }
- .erp-drop { position: relative; }
- .erp-drop-menu {
- display: none; position: absolute; top: calc(100% + 4px); left: 0; z-index: 8;
- min-width: 132px; background: #fff; border: 1px solid var(--line); border-radius: 8px;
- box-shadow: var(--shadow); padding: 4px;
- }
- .erp-drop.open .erp-drop-menu { display: block; }
- .erp-drop-menu button {
- display: block; width: 100%; text-align: left; height: 32px; padding: 0 10px;
- border-radius: 6px; color: var(--ink);
- }
- .erp-drop-menu button:hover { background: var(--green-soft); color: var(--green-dark); }
- .ct-table { min-width: 1780px !important; }
- .ct-table .quote-expand,
- .ct-table .chk,
- .ct-table .col-no {
- position: sticky; z-index: 3; background: #fff;
- }
- .ct-table .quote-expand { left: 0; width: 32px; min-width: 32px; max-width: 32px; padding: 7px 0; }
- .ct-table .chk { left: 32px; width: 40px; min-width: 40px; max-width: 40px; padding: 7px 0; }
- .ct-table .col-no { left: 72px; width: 168px; min-width: 168px; max-width: 168px; }
- .ct-table thead .quote-expand,
- .ct-table thead .chk,
- .ct-table thead .col-no { z-index: 5; background: #f7faf8; }
- .ct-table tbody .quote-expand,
- .ct-table tbody .chk,
- .ct-table tbody .col-no { background: #fff; }
- .ct-table tr.row-alert .quote-expand,
- .ct-table tr.row-alert .chk,
- .ct-table tr.row-alert .col-no { background: #fdecea; }
- .ct-name { white-space: normal !important; min-width: 148px; max-width: 186px; line-height: 1.35; }
- .ct-name .quote-link { display: block; }
- .ct-name .sub { display: block; color: var(--green); font-size: 11px; font-weight: 600; }
- .ct-cust { white-space: normal !important; min-width: 132px; max-width: 168px; line-height: 1.35; }
- .ct-prod { white-space: normal !important; min-width: 96px; max-width: 118px; line-height: 1.35; }
- .ct-att { display: flex; align-items: center; gap: 8px; min-width: 92px; }
- .pdf-chip { display: flex; flex-direction: column; align-items: center; gap: 2px; color: #d94841; min-width: 48px; }
- .pdf-box {
- width: 28px; height: 32px; border-radius: 3px; background: #e24b4a; color: #fff;
- font-size: 8px; font-weight: 800; letter-spacing: .2px; display: grid; place-items: center;
- box-shadow: 0 1px 2px rgba(226,75,74,.25);
- }
- .pdf-chip em { font-style: normal; font-size: 10px; font-weight: 650; color: #5b6675; white-space: nowrap; }
- .up-chip {
- width: 28px; height: 28px; border-radius: 6px; background: var(--green-soft); color: var(--green);
- display: grid; place-items: center; flex: none;
- }
- .up-chip:hover { background: var(--green); color: #fff; }
- .up-chip svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 2; }
- .ct-st { color: #3d4754; }
- .quote-search {
- margin-left: auto; display: flex; align-items: center; gap: 6px;
- height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; background: #fff; width: 240px;
- }
- .quote-search svg { width: 14px; height: 14px; stroke: #8a94a0; fill: none; stroke-width: 2; flex: none; }
- .quote-search input { border: 0; outline: 0; flex: 1; min-width: 0; background: transparent; }
- .btn-ico { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.8; }
- .btn .btn-ico { margin-right: 4px; vertical-align: -2px; }
- .quote-pager { display: flex; align-items: center; gap: 6px; }
- .quote-pager button {
- min-width: 28px; height: 28px; padding: 0 8px; border: 1px solid transparent;
- border-radius: 6px; color: #4b5563;
- }
- .quote-pager button.on { background: var(--green); color: #fff; font-weight: 650; }
- #mod-product .erp-toolbar { padding-top: 0; }
- #mod-product .prod-search-row {
- display: flex;
- align-items: center;
- flex-wrap: nowrap;
- gap: 8px;
- width: 100%;
- padding: 0 0 8px;
- }
- #mod-product .prod-search-row .quote-search {
- width: 280px;
- flex: 0 0 280px;
- margin: 0;
- }
- #mod-product .prod-search-actions {
- margin-left: auto;
- display: flex;
- align-items: center;
- gap: 6px;
- flex: none;
- }
- #mod-product .erp-table-wrap { max-height: calc(100vh - 260px); }
- #mod-staff .erp-toolbar { padding-top: 0; }
- #mod-staff .erp-table-wrap { max-height: calc(100vh - 360px); }
- .staff-table { min-width: 1280px !important; }
- .staff-name { display: inline-flex; align-items: center; gap: 8px; }
- .staff-av {
- width: 26px; height: 26px; border-radius: 99px; flex: none;
- background: var(--green-soft); color: var(--green-dark);
- display: grid; place-items: center; font-size: 11px; font-weight: 700;
- }
- .staff-due { color: #b91c1c; font-weight: 650; }
- .staff-dept-sel {
- height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 8px; background: #fff; min-width: 120px;
- }
- .quote-search.prod-search { width: 280px; margin-left: 0; }
- .prod-table { min-width: 1180px !important; }
- .prod-table .chk { width: 36px; }
- .prod-table .col-idx { width: 56px; }
- .prod-hz {
- display: inline-flex; align-items: center; justify-content: center;
- min-width: 16px; height: 16px; padding: 0 4px; margin-left: 6px;
- border-radius: 3px; background: #e24b4a; color: #fff; font-size: 10px; font-weight: 650; vertical-align: 1px;
- }
- .prod-st {
- display: inline-flex; align-items: center; height: 22px; padding: 0 10px; border-radius: 11px;
- background: #22a05d; color: #fff; font-size: 12px; font-weight: 650;
- }
- .prod-st.off { background: #94a3b8; }
- .prod-price { color: #e24b4a; }
- .th-filt {
- display: inline-block; width: 11px; height: 11px; margin-left: 4px; opacity: .38; vertical-align: -1px;
- background: currentColor;
- 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;
- }
- .prod-cat-list { display: flex; flex-direction: column; gap: 4px; }
- .prod-cat-list button {
- display: flex; justify-content: space-between; width: 100%; text-align: left;
- padding: 8px 10px; border-radius: 8px; border: 1px solid var(--line); background: #fff;
- }
- .prod-cat-list button.on, .prod-cat-list button:hover { border-color: var(--green-line); background: var(--green-soft); color: var(--green-dark); }
- .quote-pager button:hover:not(.on) { background: #e8f6ee; }
- .quote-goto { height: 28px; width: 48px; border: 1px solid var(--line); border-radius: 6px; padding: 0 6px; text-align: center; }
- .erp-ico-btn {
- width: 32px; height: 32px; border: 1px solid var(--line); border-radius: 8px; background: #fff;
- display: grid; place-items: center; color: #5b6675;
- }
- .erp-ico-btn:hover { border-color: var(--green-line); color: var(--green-dark); }
- .empty { color: var(--muted); padding: 28px 0; text-align: center; }
- dialog.wide-dlg { max-width: min(1080px, 96vw); width: 1080px; }
- dialog.exp-dlg {
- max-width: min(960px, 96vw); width: 960px; border: 0; border-radius: 12px;
- box-shadow: 0 18px 50px rgba(28, 36, 48, .18); overflow: hidden; background: #fff;
- margin: auto;
- }
- dialog.exp-dlg .dlg-h {
- padding: 14px 22px; border-bottom: 0; background: #e8f6ee;
- }
- dialog.exp-dlg .dlg-h h3 { font-size: 15px; font-weight: 650; color: var(--ink); }
- dialog.exp-dlg .dlg-x { font-size: 22px; color: #6a7380; width: 32px; height: 32px; }
- dialog.exp-dlg .dlg-b { max-height: 72vh; padding: 18px 24px 4px; }
- dialog.exp-dlg .dlg-f { padding: 12px 24px 18px; border-top: 0; gap: 10px; }
- dialog.exp-dlg .dlg-f .btn { display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 14px; }
- .exp-new-top {
- display: grid; grid-template-columns: 1fr 1fr 1.25fr; gap: 16px 40px; margin-bottom: 6px;
- }
- .exp-new-top label { display: flex; flex-direction: column; gap: 8px; font-size: 13px; color: #3d4754; font-weight: 500; }
- .exp-new-top input, .exp-new-top select, .exp-field-date {
- height: 38px; border: 1px solid #d8dee6; border-radius: 8px; padding: 0 12px; background: #fff; outline: none; color: var(--ink); font-size: 13px;
- }
- .exp-field-date {
- display: flex; align-items: center; gap: 8px; padding: 0 10px; position: relative;
- }
- .exp-field-date svg { width: 16px; height: 16px; stroke: #8a94a0; fill: none; stroke-width: 1.8; flex: none; }
- .exp-field-date input {
- flex: 1; height: 36px; border: 0 !important; padding: 0 !important; background: transparent; min-width: 0;
- }
- .exp-field-date input::-webkit-calendar-picker-indicator {
- opacity: 0; position: absolute; inset: 0; width: 100%; height: 100%; cursor: pointer;
- }
- .exp-new-top input:focus, .exp-new-top select:focus,
- .exp-line-table input:focus, .exp-line-table select:focus, .exp-note:focus {
- border-color: var(--green-line);
- }
- .exp-block { margin-top: 8px; padding-top: 18px; border-top: 1px dashed #e4e9ed; }
- .exp-block.tight { border-top: 0; padding-top: 18px; margin-top: 4px; }
- .exp-block > h4 { font-size: 13px; font-weight: 650; color: var(--ink); margin-bottom: 10px; }
- .exp-block > h4 .opt { color: var(--muted); font-weight: 400; }
- .exp-line-wrap { overflow: auto; border-radius: 8px; }
- .exp-line-table { width: 100%; min-width: 760px; border-collapse: separate; border-spacing: 0; }
- .exp-line-table th {
- background: #f4f6f8; font-size: 12px; color: #5b6675; font-weight: 600;
- padding: 11px 12px; border-bottom: 1px solid #eef1f4; text-align: left;
- }
- .exp-line-table th:first-child { border-radius: 8px 0 0 0; }
- .exp-line-table th:last-child { text-align: center; border-radius: 0 8px 0 0; }
- .exp-line-table td { padding: 10px 12px; vertical-align: middle; font-size: 13px; color: var(--muted); background: #fff; }
- .exp-line-table td:last-child { text-align: center; }
- .exp-line-table input, .exp-line-table select {
- width: 100%; height: 36px; border: 1px solid #d8dee6; border-radius: 8px; padding: 0 10px; background: #fff; outline: none; color: var(--ink);
- }
- .exp-line-table select:invalid, .exp-line-table select.placeholder { color: #9aa3ad; }
- .exp-line-table input::placeholder { color: #9aa3ad; }
- .exp-datewrap { position: relative; display: flex; align-items: center; }
- .exp-datewrap svg {
- position: absolute; left: 10px; width: 16px; height: 16px;
- stroke: #8a94a0; fill: none; stroke-width: 1.8; pointer-events: none; z-index: 1;
- }
- .exp-datewrap input[type="date"] { color: var(--ink); padding-left: 32px; }
- .exp-datewrap:not(.has) input[type="date"] { color: transparent; }
- .exp-datewrap:not(.has) input[type="date"]::-webkit-datetime-edit { color: transparent; }
- .exp-datewrap input[type="date"]::-webkit-calendar-picker-indicator {
- opacity: 0; position: absolute; inset: 0; width: 100%; height: 100%; cursor: pointer;
- }
- .exp-date-ph {
- position: absolute; left: 32px; top: 50%; transform: translateY(-50%);
- color: #9aa3ad; pointer-events: none; font-size: 13px;
- }
- .exp-datewrap.has .exp-date-ph { display: none; }
- .exp-line-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 14px; gap: 12px; flex-wrap: wrap; }
- .exp-line-foot .btn { height: 34px; padding: 0 12px; }
- .exp-total { font-size: 13px; color: var(--ink); }
- .exp-total b { color: #e24b4a; font-size: 16px; font-weight: 750; margin-left: 4px; }
- .exp-inv-meta { font-size: 13px; color: var(--ink); margin-bottom: 12px; }
- .exp-inv-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; min-height: 0; }
- .exp-up {
- width: 132px; height: 110px; border: 1px dashed #8fd0aa; border-radius: 12px;
- background: #e8f6ee; display: flex; flex-direction: column; align-items: center; justify-content: center;
- color: var(--green); gap: 2px; cursor: pointer; font-weight: 500; font-size: 13px;
- }
- .exp-up:hover { background: #def1e5; }
- .exp-up input { display: none; }
- .exp-up .plus { font-size: 30px; line-height: 1; color: #22a05d; font-weight: 400; }
- .exp-note { width: 100%; min-height: 40px; height: 40px; border: 1px solid #d8dee6; border-radius: 8px; padding: 8px 12px; resize: vertical; outline: none; }
- .btn-ico { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.8; flex: none; }
- .inv-pick-list { display: flex; flex-direction: column; gap: 8px; }
- .inv-pick-item {
- display: flex; align-items: center; gap: 10px; padding: 10px 12px;
- border: 1px solid var(--line); border-radius: 8px; background: #fff; width: 100%; text-align: left;
- }
- .inv-pick-item.on { border-color: var(--green); background: var(--green-soft); }
- .inv-pick-item .amt { margin-left: auto; font-weight: 650; }
- @media (max-width: 1100px) {
- .exp-new-top { grid-template-columns: 1fr; }
- }
- .plate-cell { font-size: 12px; line-height: 1.45; white-space: normal; vertical-align: top; min-width: 360px; }
- .plate-mini {
- width: auto; min-width: 0; border-collapse: collapse; background: transparent;
- }
- .plate-mini th, .plate-mini td {
- padding: 2px 18px 2px 0; border: 0; background: transparent !important;
- white-space: nowrap; font-size: 12px; vertical-align: baseline;
- }
- .plate-mini th {
- color: var(--muted); font-weight: 600; font-size: 11px;
- }
- .plate-mini td:first-child { font-weight: 650; color: var(--ink); padding-right: 22px; }
- .plate-mini th:nth-child(n+2), .plate-mini td:nth-child(n+2) {
- text-align: right; padding-left: 16px; padding-right: 16px;
- }
- .plate-mini th:last-child, .plate-mini td:last-child { padding-right: 0; }
- .attach-mode #buyOcrBox, .attach-mode #buyFormCol { display: none !important; }
- .attach-mode .ocr-pair, .attach-mode .split-h { grid-template-columns: 1fr; }
- .trans-slot {
- margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--line);
- }
- .trans-slot-h { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
- .trans-slot-h h4 { font-size: 13px; }
- .exp-board { margin-bottom: 14px; }
- .exp-map {
- background: #fff; border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
- }
- .exp-map-grid {
- display: grid;
- grid-template-columns: 88px repeat(5, minmax(0, 1fr));
- }
- .exp-map-head, .exp-map-row {
- display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; align-items: stretch;
- }
- .exp-map-head { background: #f7f9f8; border-bottom: 1px solid #eef1f4; }
- .exp-colh {
- padding: 8px 6px; font-size: 11px; color: var(--muted); text-align: center;
- }
- .exp-colh b { color: var(--ink); font-weight: 650; margin-right: 3px; }
- .exp-map-row.bx { background: linear-gradient(90deg, #eef8f2 0, #fff 88px); border-bottom: 1px dashed #e6eaee; }
- .exp-map-row.sq { background: linear-gradient(90deg, #fff6eb 0, #fff 88px); }
- .exp-map-lab {
- display: flex; flex-direction: column; justify-content: center;
- padding: 10px 12px;
- }
- .exp-map-lab b { font-size: 13px; font-weight: 750; color: var(--green); line-height: 1.2; }
- .exp-map-row.sq .exp-map-lab b { color: #c2410c; }
- .exp-map-lab span { font-size: 11px; color: var(--muted); margin-top: 2px; }
- .exp-cell {
- position: relative; display: flex; align-items: center; gap: 8px;
- margin: 8px 8px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
- background: #fff; text-align: left; cursor: pointer; min-width: 0;
- }
- .exp-cell:hover { border-color: var(--green-line); }
- .exp-map-row.sq .exp-cell:hover { border-color: #f6c38b; }
- .exp-cell.on { border-color: var(--green); background: var(--green-soft); }
- .exp-map-row.sq .exp-cell.on { border-color: #ea580c; background: #fff7ed; }
- .exp-cell.ghost {
- border-style: dashed; background: #f4f7f5; cursor: default; color: var(--muted);
- }
- .exp-cell.ghost:hover { border-color: var(--line); }
- .exp-cell .t { min-width: 0; flex: 1; }
- .exp-cell .t b { display: block; font-size: 12.5px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
- .exp-cell .t em { display: block; font-style: normal; font-size: 10px; color: var(--muted); }
- .exp-cell .c { flex: none; font-size: 16px; font-weight: 750; letter-spacing: -.03em; }
- .exp-cell:not(.last)::after {
- content: ""; position: absolute; right: -9px; top: 50%; z-index: 1; width: 10px; height: 10px;
- margin-top: -5px; pointer-events: none;
- 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;
- }
- @media (max-width: 1100px) {
- .exp-map-grid { display: block; }
- .exp-map-head { display: none; }
- .exp-map-row { display: grid; grid-template-columns: 1fr 1fr; }
- .exp-map-lab { grid-column: 1 / -1; padding-bottom: 0; }
- .exp-cell:not(.last)::after { display: none; }
- }
- .fin-sec { margin-bottom: 18px; }
- .fin-sec:last-child { margin-bottom: 0; }
- .fin-sec-h {
- display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 700; margin: 0 0 8px;
- }
- .kind-tag {
- display: inline-flex; align-items: center; height: 18px; padding: 0 6px; border-radius: 4px;
- font-size: 10px; font-weight: 700; margin-right: 4px; vertical-align: middle;
- }
- .kind-tag.bx { background: var(--green-soft); color: var(--green); }
- .kind-tag.sq { background: #fff7ed; color: #c2410c; }
- .exp-log { display: flex; flex-direction: column; gap: 0; }
- .exp-log .row {
- display: grid; grid-template-columns: 118px 88px 1fr; gap: 8px;
- padding: 8px 0; border-bottom: 1px solid #eef1f4; font-size: 12px;
- }
- .exp-log .row:last-child { border: 0; }
- .duty-bar {
- display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px;
- }
- .duty-brand {
- display: inline-flex; align-items: center; gap: 8px; padding-left: 10px;
- border-left: 3px solid var(--green); font-size: 15px; font-weight: 750; color: var(--green-dark);
- }
- .duty-brand svg { width: 18px; height: 18px; stroke: var(--green); fill: none; stroke-width: 1.8; }
- .duty-nav { display: inline-flex; align-items: center; gap: 6px; }
- .duty-nav .m { font-weight: 650; min-width: 92px; text-align: center; }
- .duty-ico {
- width: 28px; height: 28px; border-radius: 8px; border: 1px solid var(--line); background: #fff;
- display: grid; place-items: center; cursor: pointer;
- }
- .duty-ico svg { width: 14px; height: 14px; stroke: #4b5563; fill: none; stroke-width: 1.8; }
- .duty-legend {
- margin-left: auto; display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
- font-size: 12px; color: var(--muted);
- }
- .duty-legend i {
- display: inline-block; width: 8px; height: 8px; border-radius: 99px; margin-right: 4px; vertical-align: 1px;
- }
- .duty-legend .am { background: #e67e22; }
- .duty-legend .pm { background: #c45c26; }
- .duty-legend .watch { background: #d94841; }
- .duty-legend .other { background: #8a94a0; }
- .duty-link { color: var(--green); font-weight: 500; background: none; border: 0; cursor: pointer; padding: 0; }
- .duty-stats {
- display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px;
- }
- .duty-pills { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
- .duty-pill {
- display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: 8px;
- background: #fff; border: 1px solid var(--line); font-size: 12.5px;
- }
- .duty-pill i { width: 8px; height: 8px; border-radius: 99px; display: inline-block; }
- .duty-pill i.am { background: #e67e22; }
- .duty-pill i.pm { background: #c45c26; }
- .duty-pill i.watch { background: #d94841; }
- .duty-pill b { font-size: 14px; font-weight: 750; }
- .duty-sum { font-size: 12.5px; color: var(--ink); }
- .duty-sum b { color: var(--green); font-size: 14px; font-weight: 750; }
- .duty-people { display: flex; flex-wrap: wrap; gap: 10px 16px; font-size: 12.5px; color: var(--muted); align-items: center; }
- .duty-people em { font-style: normal; color: var(--ink); }
- .duty-people b { color: var(--green); font-weight: 750; }
- .duty-cal { background: #fff; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
- .duty-week {
- display: grid; grid-template-columns: repeat(7, 1fr); background: #f7f9f8; border-bottom: 1px solid var(--line);
- }
- .duty-week span { padding: 8px; text-align: center; font-size: 12px; color: var(--muted); font-weight: 500; }
- .duty-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
- .duty-day {
- min-height: 124px; padding: 8px; text-align: left; border: 0; border-right: 1px solid #eef1f4; border-bottom: 1px solid #eef1f4;
- background: #fff; vertical-align: top; cursor: pointer; font: inherit; color: inherit; display: block; width: 100%;
- }
- .duty-day:nth-child(7n) { border-right: 0; }
- .duty-day.out { background: #fafbfb; color: #c5cbd1; }
- .duty-day:hover { background: #f8faf9; }
- .duty-day .num { font-weight: 650; margin-bottom: 6px; font-size: 13px; }
- .duty-day.today .num {
- width: 22px; height: 22px; border-radius: 99px; background: var(--green); color: #fff;
- display: grid; place-items: center; font-size: 12px;
- }
- .duty-chip {
- display: block; width: 100%; border-radius: 4px; padding: 3px 6px; margin-bottom: 4px;
- font-size: 11px; color: #fff; text-align: left; line-height: 1.35; font-weight: 500;
- }
- .duty-chip.am { background: #e67e22; }
- .duty-chip.pm { background: #c45c26; }
- .duty-chip.watch { background: #d94841; }
- .duty-chip.other { background: #6b7280; }
- .duty-empty { color: #9aa3ad; font-size: 12px; }
- .chip.pick { background: #fff; color: var(--ink); border: 1px solid var(--line); cursor: pointer; height: 28px; }
- .chip.pick.on { background: var(--green-soft); color: var(--green-dark); border-color: var(--green); }
- .duty-exist { display: flex; flex-direction: column; gap: 8px; }
- .duty-exist .row {
- display: flex; align-items: center; gap: 8px; font-size: 12.5px;
- }
- .wh-tree {
- display: flex;
- flex-direction: column;
- gap: 16px;
- margin-bottom: 16px;
- }
- #view-wh-info, #view-wh-mode, #view-wh-proc, #view-wh-stock {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
- .wh-nav-card { overflow: visible; }
- .wh-nav {
- padding: 10px 14px;
- display: flex;
- flex-direction: column;
- gap: 0;
- }
- .wh-nav-mains {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 8px;
- }
- .wh-nav-lab {
- font-size: 13px; font-weight: 650; color: var(--ink);
- margin-right: 4px;
- }
- .wh-nav-item, .wh-nav-child {
- display: inline-flex; align-items: center; gap: 6px;
- width: auto; text-align: left; border-radius: 99px;
- padding: 6px 12px;
- }
- .wh-nav-item {
- font-weight: 650;
- background: #fff;
- color: #5b6675;
- border: 1.5px solid #c5ccd4;
- }
- .wh-nav-item.acid { color: #2a6bb3; background: #eef4fb; border-color: #b7cde6; }
- .wh-nav-item.alkali { color: #c47d11; background: #fbf4e8; border-color: #e4c792; }
- .wh-nav-item.hcl { color: #c45b5b; background: #fbf0f0; border-color: #e3b4b4; }
- .wh-nav-item.pack { color: #7c5cbf; background: #f4f0fb; border-color: #c9b8e4; }
- .wh-nav-item.solid { color: #5b6675; background: #f2f4f6; border-color: #c5ccd4; }
- .wh-nav-item:hover { filter: none; }
- .wh-nav-item.acid:hover { background: #e3edf8; }
- .wh-nav-item.alkali:hover { background: #f7ebd6; }
- .wh-nav-item.hcl:hover { background: #f6e4e4; }
- .wh-nav-item.pack:hover { background: #ece4f7; }
- .wh-nav-item.solid:hover { background: #e8ebef; }
- .wh-nav-item.on {
- color: #fff;
- box-shadow: 0 2px 8px rgba(28, 36, 48, 0.18);
- }
- .wh-nav-item.on.acid { background: #2a6bb3; border-color: #1d4f86; }
- .wh-nav-item.on.alkali { background: #c47d11; border-color: #8f5a0a; }
- .wh-nav-item.on.hcl { background: #c45b5b; border-color: #8f3e3e; }
- .wh-nav-item.on.pack { background: #7c5cbf; border-color: #5a3f91; }
- .wh-nav-item.on.solid { background: #5b6675; border-color: #3d4754; }
- .wh-nav-item.on .wh-dot { background: #fff; }
- .wh-nav-child {
- font-size: 12.5px;
- font-weight: 500;
- padding: 5px 10px;
- color: #1c2430;
- background: #fff;
- border: 1px solid #c5ccd4;
- }
- .wh-nav-child:hover { background: #f5f7f8; }
- .wh-nav-child.on {
- background: #f3f5f7;
- border-color: #8a94a0;
- font-weight: 650;
- }
- .wh-nav-child .wh-dot { background: #8a94a0; }
- .wh-nav-subs {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 6px;
- margin-top: 10px;
- padding-top: 10px;
- border-top: 1px solid #eef1f4;
- }
- .wh-nav-subs-lab {
- font-size: 11px; color: #8a94a0; font-weight: 600; margin-right: 2px;
- }
- .wh-nav-child .mini {
- font-size: 10px; color: #8a94a0; font-weight: 500;
- }
- .wh-dot {
- width: 8px; height: 8px; border-radius: 50%; flex: none;
- background: currentColor;
- }
- .wh-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
- .wh-kpi {
- background: var(--white); border: 1px solid var(--line); border-radius: 10px;
- padding: 12px 14px;
- }
- .wh-kpi .k { color: var(--muted); font-size: 12px; }
- .wh-kpi .v { font-size: 22px; font-weight: 700; letter-spacing: -.03em; margin: 4px 0 2px; }
- .wh-kpi .s { font-size: 11px; color: var(--muted); }
- .wh-cap { height: 6px; background: #eef1f4; border-radius: 99px; overflow: hidden; margin-top: 8px; }
- .wh-cap i { display: block; height: 100%; background: linear-gradient(90deg, #167c4a, #22a05d); }
- .wh-cap i.warn { background: linear-gradient(90deg, #e24b4a, #f97316); }
- .wh-stock-name { font-weight: 650; }
- .wh-path { color: var(--muted); font-size: 11px; }
- .grade {
- display: inline-flex; align-items: center; height: 18px; padding: 0 6px;
- border-radius: 4px; font-size: 11px; font-weight: 700;
- }
- .grade.ind { background: #eaf2fb; color: var(--blue); }
- .grade.rg { background: #f3eefb; color: #7c5cbf; }
- .grade.raw { background: var(--amber-soft); color: var(--amber); }
- .grade.fin { background: var(--green-soft); color: var(--green-dark); }
- .grade.ref { background: #e6f4ef; color: #0f7a5a; }
- .wh-stock-cards {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 10px;
- }
- .wh-mini {
- cursor: pointer;
- background: var(--white);
- border: 1px solid var(--line);
- border-radius: 10px;
- padding: 12px 14px;
- min-height: 0;
- box-shadow: none;
- transition: background .15s;
- }
- .wh-mini:hover { background: #f7faf8; }
- .wh-mini-top {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- }
- .wh-mini-name {
- display: flex;
- align-items: center;
- gap: 7px;
- min-width: 0;
- }
- .wh-mini-name i {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- flex: none;
- background: currentColor;
- }
- .wh-mini-name b {
- font-size: 13px;
- font-weight: 650;
- color: #1c2430;
- line-height: 1.3;
- }
- .wh-mini .qty {
- flex: none;
- font-size: 18px;
- font-weight: 700;
- letter-spacing: -.03em;
- font-variant-numeric: tabular-nums;
- line-height: 1;
- }
- .wh-mini .qty span {
- font-size: 11px;
- font-weight: 500;
- margin-left: 3px;
- opacity: .75;
- }
- .wh-mini.tone-ind { color: var(--blue); }
- .wh-mini.tone-rg { color: #7c5cbf; }
- .wh-mini.tone-hcl { color: #c45b5b; }
- .wh-mini.tone-ind .qty { color: var(--blue); }
- .wh-mini.tone-rg .qty { color: #7c5cbf; }
- .wh-mini.tone-hcl .qty { color: #c45b5b; }
- .wh-mini .tanks {
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
- margin-top: 8px;
- }
- .wh-mini .tanks span {
- display: inline-flex;
- align-items: center;
- height: 20px;
- padding: 0 7px;
- border-radius: 4px;
- background: #f3f5f4;
- color: #5b6675;
- font-size: 11px;
- font-variant-numeric: tabular-nums;
- }
- .wh-mini.tone-ind i,
- .wh-mini.tone-rg i,
- .wh-mini.tone-hcl i { background: currentColor; }
- .proc-table { min-width: 720px; border-collapse: separate; border-spacing: 5px 0; }
- .proc-table.wide { min-width: 1480px; border-spacing: 4px 3px; }
- .proc-table thead th {
- background: #1a3d73;
- color: #fff;
- font-weight: 600;
- white-space: nowrap;
- text-align: center;
- padding: 9px 8px;
- border-bottom: 0;
- border-radius: 4px;
- }
- .proc-table td {
- text-align: center;
- white-space: nowrap;
- font-variant-numeric: tabular-nums;
- padding: 9px 8px;
- border-bottom: 0;
- }
- .proc-table tfoot td {
- font-weight: 700;
- background: #f3f6f4;
- border-radius: 4px;
- }
- .proc-table.wide td {
- color: #243040;
- font-size: 12.5px;
- padding: 10px 8px;
- }
- .proc-table.wide tbody tr:nth-child(even) td { background: #f4f7fb; }
- .proc-table.wide tbody tr:hover td { background: #e8f0fa; }
- .proc-table.wide .qty-in { color: var(--green); font-weight: 700; }
- .proc-table.wide .qty-out { color: var(--blue); font-weight: 700; }
- .proc-table.wide .qty-bal { color: #1a3d73; font-weight: 700; }
- .proc-table.wide .amt { font-weight: 650; }
- .proc-table.wide .dash { color: #c2c8d0; }
- .proc-table th.col-name, .proc-table td.col-name {
- text-align: left;
- padding-left: 12px;
- min-width: 148px;
- }
- .wh-mode {
- border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; background: #fbfefc;
- }
- .wh-mode h3 { font-size: 13px; margin-bottom: 8px; }
- .wh-mode p { color: var(--muted); font-size: 12px; line-height: 1.55; }
- .wh-proc {
- display: grid; grid-template-columns: 1fr auto 1fr auto 1fr; gap: 8px;
- align-items: center; margin-top: 10px;
- }
- .wh-proc .box {
- background: #fff; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
- }
- .wh-proc .box b { display: block; font-size: 13px; }
- .wh-proc .box span { display: block; color: var(--muted); font-size: 11px; margin-top: 4px; }
- .wh-proc .arr { color: #4a7fd4; font-weight: 700; font-size: 16px; text-align: center; }
- .tank-map {
- display: grid; grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
- gap: 8px;
- }
- .tank-card {
- border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px; background: #fff;
- min-height: 74px;
- }
- .tank-card .id { font-weight: 750; font-size: 13px; letter-spacing: 0.02em; }
- .tank-card .p { color: var(--muted); font-size: 11px; margin: 3px 0 6px; line-height: 1.35; }
- .tank-card .q { font-size: 11px; font-variant-numeric: tabular-nums; font-weight: 650; }
- .tank-card.sulfur { border-color: #f3ddb0; background: #fffdf6; }
- .tank-group { margin-top: 10px; }
- .tank-group .tg-lab { font-size: 11px; color: var(--muted); margin-bottom: 4px; font-weight: 650; }
- .tank-group .wh-tank { margin-top: 0; }
- .wh-prod-row { display: flex; justify-content: space-between; gap: 8px; padding: 8px 0; border-bottom: 1px solid #eef1f4; font-size: 12px; }
- .wh-prod-row:last-child { border: 0; }
- .calc-box { background: #f7fbf8; border: 1px dashed var(--green-line); border-radius: 10px; padding: 12px; margin-top: 10px; font-size: 12px; }
- .calc-box b { font-weight: 650; }
- .tank-use { display: flex; align-items: center; gap: 8px; }
- .tank-use .bar { flex: 1; height: 6px; background: #eef1f4; border-radius: 99px; overflow: hidden; min-width: 64px; }
- .tank-use .bar i { display: block; height: 100%; background: linear-gradient(90deg, #167c4a, #22a05d); }
- .tank-use .bar i.warn { background: linear-gradient(90deg, #e24b4a, #f97316); }
- .print-slip { max-width: 720px; margin: 0 auto; padding: 8px 4px; }
- .print-slip h1 { font-size: 18px; text-align: center; margin-bottom: 4px; }
- .print-slip .sub { text-align: center; color: var(--muted); font-size: 12px; margin-bottom: 16px; }
- .print-slip table { width: 100%; }
- .print-slip th, .print-slip td { border-bottom: 1px solid var(--line); padding: 8px 6px; }
- .print-sign { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; margin-top: 28px; font-size: 12px; color: var(--muted); }
- .wh-dir { width: 100%; }
- .op-chip {
- display: inline-flex; align-items: center; height: 20px; padding: 0 7px; margin: 0 4px 4px 0;
- border-radius: 4px; font-size: 11px; font-weight: 650;
- background: var(--green-soft); color: var(--green-dark);
- }
- .op-chip.out { background: #eaf2fb; color: var(--blue); }
- .op-chip.mix { background: #fff6e5; color: var(--amber); }
- .wh-tree-box {
- display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;
- }
- .wh-branch {
- border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: #fff;
- }
- .wh-branch h4 { font-size: 12px; color: var(--muted); margin-bottom: 8px; font-weight: 650; }
- .wh-branch button {
- display: flex; justify-content: space-between; width: 100%;
- padding: 6px 0; border-bottom: 1px solid #eef1f4; text-align: left; font-size: 12.5px;
- }
- .wh-branch button:last-child { border: 0; }
- .wh-branch button:hover { color: var(--green-dark); }
- @media (max-width: 1100px) {
- .wh-info-grid, .wh-proc, .wh-tree-box, .wh-stock-cards { grid-template-columns: 1fr; }
- }
- @media (max-width: 1100px) {
- .app { grid-template-columns: 1fr; }
- .stats, .stats.five, .grid, .quick, .form, .apply, .ocr-pair, .split-h, .sheet, .wh-tree, .wh-kpis { grid-template-columns: 1fr; }
- .cert-duo { grid-template-columns: minmax(0, 1fr) minmax(0, 3fr); }
- .sidebar, .search { display: none; }
- .form .span2 { grid-column: auto; }
- }
- #detailStats { grid-template-columns: repeat(4, 1fr); }
- </style>
- </head>
- <body>
- <div class="app">
- <aside class="sidebar">
- <div class="brand">
- <div class="logo" aria-hidden="true">峡</div>
- <div>
- <strong>山峡代工</strong>
- <span class="sub">一体化平台</span>
- </div>
- </div>
- <nav class="nav">
- <div class="nav-label">工作台</div>
- <button class="side-item" type="button" data-page="fav">
- <svg viewBox="0 0 24 24"><path d="M12 4 4 19h16L12 4z"/><path d="M12 10v4M12 16h.01"/></svg>
- 收藏
- </button>
- <button class="side-item" type="button" data-page="todo">
- <svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="M8 9h8M8 13h5"/></svg>
- 待办列表
- </button>
- <div class="nav-group" data-group="crm">
- <button class="side-item" type="button" data-toggle="crm">
- <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>
- CRM管理
- <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
- </button>
- <div class="nav-sub">
- <button class="side-item side-sub" type="button" data-mod="product">产品管理</button>
- <button class="side-item side-sub" type="button" data-mod="quote">报价单管理 <span class="badge-n" id="quoteBadge">0</span></button>
- <button class="side-item side-sub" type="button" data-mod="contract">合同管理 <span class="badge-n" id="ctBadge">0</span></button>
- <button class="side-item side-sub" type="button" data-page="crm-customer">客户管理</button>
- </div>
- </div>
- <div class="nav-group open" data-group="transport">
- <button class="side-item" type="button" data-toggle="transport">
- <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>
- 运输管理
- <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
- </button>
- <div class="nav-sub">
- <button class="side-item side-sub active" type="button" data-mod="plan">运输计划 <span class="badge-n" id="planBadge">0</span></button>
- <button class="side-item side-sub" type="button" data-mod="archive">运输归档 <span class="badge-n" id="archiveBadge">0</span></button>
- <button class="side-item side-sub" type="button" data-mod="task">任务单管理 <span class="badge-n" id="taskBadge">0</span></button>
- <button class="side-item side-sub" type="button" data-mod="cert">购买证管理 <span class="badge-n" id="certBadge">0</span></button>
- <button class="side-item side-sub" type="button" data-page="weigh">地磅管理</button>
- <button class="side-item side-sub" type="button" data-page="freight">运费结算</button>
- </div>
- </div>
- <div class="nav-group" data-group="fleet">
- <button class="side-item" type="button" data-toggle="fleet">
- <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>
- 车辆管理
- <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
- </button>
- <div class="nav-sub">
- <button class="side-item side-sub" type="button" data-mod="fleet" data-fleet-view="etc">ETC充值卡管理</button>
- <button class="side-item side-sub" type="button" data-mod="fleet" data-fleet-view="fuel">加油登记管理</button>
- <button class="side-item side-sub" type="button" data-mod="fleet" data-fleet-view="oilcard">加油充值卡管理</button>
- <button class="side-item side-sub" type="button" data-mod="fleet" data-fleet-view="maint">车辆维保管理</button>
- </div>
- </div>
- <div class="nav-group" data-group="warehouse">
- <button class="side-item" type="button" data-toggle="warehouse">
- <svg viewBox="0 0 24 24"><path d="M3 10h18v9H3zM5 10V7l3-3h8l3 3v3"/></svg>
- 仓储管理
- <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
- </button>
- <div class="nav-sub">
- <button class="side-item side-sub" type="button" data-mod="warehouse" data-wh-view="info">库存信息表</button>
- <button class="side-item side-sub" type="button" data-mod="warehouse" data-wh-view="mode">仓库信息表</button>
- <button class="side-item side-sub" type="button" data-mod="warehouse" data-wh-view="proc">加工单明细表</button>
- <button class="side-item side-sub" type="button" data-mod="warehouse" data-wh-view="stock">库存成本明细表</button>
- </div>
- </div>
- <div class="nav-group" data-group="finance">
- <button class="side-item" type="button" data-toggle="finance">
- <svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="M8 9h8M8 13h5"/></svg>
- 财务管理
- <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
- </button>
- <div class="nav-sub">
- <button class="side-item side-sub" type="button" data-page="fin-ar">应收应付</button>
- </div>
- </div>
- <div class="nav-group" data-group="hr">
- <button class="side-item" type="button" data-toggle="hr">
- <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>
- 人事管理
- <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
- </button>
- <div class="nav-sub">
- <button class="side-item side-sub" type="button" data-mod="expense">报销管理 <span class="badge-n" id="expBadge">0</span></button>
- <button class="side-item side-sub" type="button" data-mod="duty">值班管理</button>
- <button class="side-item side-sub" type="button" data-mod="staff">员工档案</button>
- </div>
- </div>
- <div class="nav-group" data-group="system">
- <button class="side-item" type="button" data-toggle="system">
- <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>
- 系统管理
- <svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
- </button>
- <div class="nav-sub">
- <button class="side-item side-sub" type="button" data-page="sys-dict">字典管理</button>
- </div>
- </div>
- </nav>
- </aside>
- <section class="main">
- <header class="topbar">
- <div class="crumb">工作台 / <b id="crumbMod">运输管理</b><span id="crumbTail"> / <span id="crumb">运输计划</span></span></div>
- <label class="search">
- <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>
- <input class="top-search" id="globalSearch" placeholder="搜索车牌、单号、证号" />
- </label>
- <div class="user">
- <div class="avatar">王</div>
- 调度员
- </div>
- </header>
- <div class="page">
- <div id="mod-placeholder" class="hidden">
- <div class="page-head">
- <div>
- <h1 id="phTitle">功能建设中</h1>
- <p id="phDesc">该模块尚未接入本演示,菜单结构已按一体化平台一级 / 二级导航铺好。</p>
- </div>
- </div>
- <div class="coming">
- <h2 id="phCardTitle">敬请期待</h2>
- <p id="phComing">当前演示重点在运输计划与购买证管理。</p>
- </div>
- </div>
- <div id="mod-product" class="hidden">
- <section class="erp-page">
- <div class="prod-search-row">
- <label class="quote-search prod-search">
- <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
- <input id="prodQ" type="search" placeholder="产品名称/编号/拼音首字母" oninput="prodPage=1;renderProducts()" />
- </label>
- <div class="prod-search-actions">
- <button class="erp-ico-btn" type="button" title="刷新" onclick="prodPage=1;renderProducts()">↻</button>
- <button class="erp-ico-btn" type="button" title="列设置" onclick="toast('演示:列设置')">☰</button>
- </div>
- </div>
- <div class="erp-toolbar">
- <button class="btn btn-primary" type="button" onclick="openProdDlg()">+ 新增产品</button>
- <button class="btn" type="button" onclick="editSelectedProd()">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M4 20h4l11-11-4-4L4 16v4z"/><path d="m13 7 4 4"/></svg>
- 编辑
- </button>
- <button class="btn" type="button" onclick="deleteProds()">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M5 7h14M9 7V5h6v2M8 7l1 12h6l1-12"/></svg>
- 删除
- </button>
- <button class="btn" type="button" onclick="shelfProds('上架')">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 19V7M8 11l4-4 4 4"/></svg>
- 批量上架
- </button>
- <button class="btn" type="button" onclick="shelfProds('下架')">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 5v12M8 13l4 4 4-4"/></svg>
- 批量下架
- </button>
- <button class="btn" type="button" onclick="toast('演示:已打开导入模板')">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 16V6M8 10l4-4 4 4"/><path d="M5 18h14"/></svg>
- 导入
- </button>
- <div class="erp-drop" id="prodExportDrop">
- <button class="btn" type="button" onclick="toggleProdExport(event)">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 4v10M8 10l4 4 4-4M5 18h14"/></svg>
- 导出
- </button>
- <div class="erp-drop-menu">
- <button type="button" onclick="exportProducts('page')">导出当前页</button>
- <button type="button" onclick="exportProducts('all')">导出全部</button>
- </div>
- </div>
- <button class="btn" type="button" onclick="openProdCatDlg()">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M4 6h7l2 2h7v10H4z"/></svg>
- 产品分类
- </button>
- </div>
- <article class="card erp-card">
- <div class="erp-table-wrap" id="prodTable"></div>
- <div class="plan-foot">
- <span class="muted" id="prodSelHint">已选 0 项</span>
- <span class="quote-pager" id="prodPager"></span>
- </div>
- </article>
- </section>
- </div>
- <div id="mod-quote" class="hidden">
- <section class="erp-page">
- <div class="stats" id="quoteStats"></div>
- <div class="erp-tabs-row">
- <div class="plan-tabs" id="quoteTabs">
- <button class="on" type="button" data-tab="全部">全部</button>
- <button type="button" data-tab="昨日">昨日</button>
- <button type="button" data-tab="今日">今日</button>
- <button type="button" data-tab="明日">明日</button>
- <button type="button" data-tab="本周">本周</button>
- </div>
- <label class="quote-search">
- <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
- <input id="quoteQ" type="search" placeholder="报价主题/客户名称" oninput="quotePage=1;renderQuotes()" />
- </label>
- </div>
- <div class="erp-toolbar">
- <button class="btn btn-primary" type="button" onclick="openQuoteDlg()">+ 新增</button>
- <button class="btn" type="button" onclick="copyQuote()">
- <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>
- 复制新增
- </button>
- <button class="btn" type="button" onclick="submitQuotes()">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M6 4h9l5 5v11H6z"/><path d="M14 4v5h5"/></svg>
- 提交审批
- </button>
- <button class="btn" type="button" onclick="genQuotePdf()">
- <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>
- 生成报价单
- </button>
- <button class="btn" type="button" onclick="voidQuotes()">
- <svg class="btn-ico" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><path d="M8 12h8"/></svg>
- 作废
- </button>
- <button class="btn" type="button" onclick="toast('演示:已导出当前列表')">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 4v10M8 10l4 4 4-4M5 18h14"/></svg>
- 导出
- </button>
- <div class="right">
- <button class="erp-ico-btn" type="button" title="刷新" onclick="renderQuotes()">↻</button>
- <button class="erp-ico-btn" type="button" title="列设置" onclick="toast('演示:列设置')">☰</button>
- </div>
- </div>
- <article class="card erp-card">
- <div class="erp-table-wrap" id="quoteTable"></div>
- <div class="plan-foot">
- <span class="muted" id="quoteSelHint">已选 0 项</span>
- <span class="quote-pager" id="quotePager"></span>
- </div>
- </article>
- </section>
- </div>
- <div id="mod-contract" class="hidden">
- <section class="erp-page">
- <div class="stats" id="ctStats"></div>
- <div class="erp-tabs-row">
- <div class="plan-tabs" id="ctTabs">
- <button class="on" type="button" data-tab="全部">全部</button>
- <button type="button" data-tab="昨日">昨日</button>
- <button type="button" data-tab="今日">今日</button>
- <button type="button" data-tab="明日">明日</button>
- <button type="button" data-tab="本周">本周</button>
- </div>
- <label class="quote-search wide">
- <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
- <input id="ctQ" type="search" placeholder="合同编号/合同名称/客户名称" oninput="ctPage=1;renderContracts()" />
- </label>
- </div>
- <div class="erp-toolbar">
- <button class="btn btn-primary" type="button" onclick="openCtDlg()">+ 新增</button>
- <button class="btn" type="button" onclick="copyContract()">
- <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>
- 合同复制
- </button>
- <button class="btn" type="button" onclick="submitContracts()">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M6 4h9l5 5v11H6z"/><path d="M14 4v5h5"/></svg>
- 提交审批
- </button>
- <button class="btn" type="button" onclick="openCtScan()">
- <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>
- 上传扫描件
- </button>
- <div class="erp-drop" id="ctExportDrop">
- <button class="btn" type="button" onclick="toggleCtExport(event)">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 4v10M8 10l4 4 4-4M5 18h14"/></svg>
- 导出
- </button>
- <div class="erp-drop-menu">
- <button type="button" onclick="exportContracts('page')">导出当前页</button>
- <button type="button" onclick="exportContracts('all')">导出全部</button>
- </div>
- </div>
- <div class="right">
- <button class="erp-ico-btn" type="button" title="刷新" onclick="renderContracts()">↻</button>
- <button class="erp-ico-btn" type="button" title="列设置" onclick="toast('演示:列设置')">☰</button>
- </div>
- </div>
- <article class="card erp-card">
- <div class="erp-table-wrap" id="ctTable"></div>
- <div class="plan-foot">
- <span class="muted" id="ctSelHint">已选 0 项</span>
- <span class="quote-pager" id="ctPager"></span>
- </div>
- </article>
- </section>
- </div>
- <div id="mod-fleet" class="hidden">
- <nav class="subnav" id="subnav">
- <button class="on" type="button" data-view="dash">总览</button>
- <button type="button" data-view="fleet">车辆台账</button>
- <button type="button" data-view="fuel">加油登记管理</button>
- <button type="button" data-view="oilcard">加油充值卡管理</button>
- <button type="button" data-view="fuelwarn">油耗预警</button>
- <button type="button" data-view="etc">ETC 申请登记</button>
- <button type="button" data-view="maint">维修保养</button>
- <button type="button" data-view="stats">统计</button>
- </nav>
- <section id="view-dash">
- <div class="page-head">
- <div>
- <h1>危化品车辆作业</h1>
- <p>公司自有危货车辆的加油、ETC 申请登记、维修保养,以及油耗与费用统计。</p>
- </div>
- <div class="actions">
- <button class="btn" type="button" onclick="showView('fuel')">登记加油</button>
- <button class="btn" type="button" onclick="showView('etc')">ETC 申请</button>
- <button class="btn btn-primary" type="button" onclick="showView('maint')">登记维保</button>
- </div>
- </div>
- <div class="alerts" id="dashAlerts"></div>
- <div class="stats" id="dashStats"></div>
- <div class="grid">
- <div class="stack">
- <article class="card">
- <div class="card-h"><h2>日常登记</h2><span style="color:var(--muted);font-size:12px">同一车辆贯穿三类业务</span></div>
- <div class="card-b">
- <div class="quick">
- <button class="qbtn" type="button" onclick="showView('fuel')"><div class="ico">油</div><h3>加油</h3><p>油品、升数、金额、里程、附件</p></button>
- <button class="qbtn" type="button" onclick="showView('fuelwarn')"><div class="ico">警</div><h3>油耗预警</h3><p>超过 0.33 L/km 即预警</p></button>
- <button class="qbtn" type="button" onclick="showView('etc')"><div class="ico">E</div><h3>ETC 申请</h3><p>自有车牌、期初公里、附件</p></button>
- <button class="qbtn" type="button" onclick="showView('maint')"><div class="ico">保</div><h3>维修保养</h3><p>进厂、项目、费用、下次保养</p></button>
- </div>
- </div>
- </article>
- <article class="card">
- <div class="card-h"><h2>车辆运行状态</h2><button class="btn" type="button" onclick="showView('fleet')">台账</button></div>
- <div class="card-b" style="padding-top:4px"><div id="dashFleet"></div></div>
- </article>
- </div>
- <div class="stack">
- <article class="card">
- <div class="card-h"><h2>最近登记</h2></div>
- <div class="card-b"><div class="timeline" id="dashTl"></div></div>
- </article>
- <article class="card">
- <div class="card-h"><h2>本月单车油耗(升)</h2><button class="btn" type="button" onclick="showView('fuelwarn')">油耗预警</button></div>
- <div class="card-b"><div class="bars" id="dashBars"></div></div>
- </article>
- </div>
- </div>
- </section>
- <section id="view-fleet" class="hidden">
- <div class="page-head">
- <div><h1>车辆台账</h1><p>危货牵引车 / 罐车基础信息与资质到期。</p></div>
- </div>
- <div class="filters">
- <input id="fleetQ" placeholder="车牌 / 驾驶员" oninput="renderFleet()" />
- <select id="fleetSt" onchange="renderFleet()">
- <option value="">全部状态</option>
- <option>运行</option>
- <option>维保中</option>
- <option>停运</option>
- </select>
- </div>
- <article class="card"><div class="card-b" style="overflow:auto" id="fleetTable"></div></article>
- </section>
- <section id="view-fuel" class="hidden">
- <div class="page-head">
- <div><h1>加油登记管理</h1><p>按自有车辆查看当月加油汇总,点击「加油登记」录入小票、升数、金额与里程。</p></div>
- <div class="actions">
- <button class="btn btn-primary" type="button" onclick="openFuelDlg()">加油登记</button>
- </div>
- </div>
- <div class="fuel-plate-grid" id="fuelPlateCards"></div>
- <article class="card"><div class="card-h"><h2>加油记录</h2></div><div class="card-b" id="fuelTable"></div></article>
- </section>
- <section id="view-fuelwarn" class="hidden">
- <div class="page-head">
- <div>
- <h1>油耗预警</h1>
- <p>按相邻两次加油核对每公里油耗。标准:33 L / 100 km = <b>0.33 L/km</b>,超过即预警。</p>
- </div>
- </div>
- <div class="stats" id="fuelwarnKpis"></div>
- <div class="filters">
- <select id="fuelwarnSt" onchange="renderFuelWarn()">
- <option value="">全部区间</option>
- <option value="预警">仅超标预警</option>
- <option value="正常">仅正常</option>
- <option value="不足">数据不足 / 异常</option>
- </select>
- <input id="fuelwarnQ" placeholder="车牌" oninput="renderFuelWarn()" />
- </div>
- <article class="card" style="margin-bottom:12px">
- <div class="card-h"><h2>单车油耗对照(L/km)</h2><span style="color:var(--muted);font-size:12px">红线为 0.33</span></div>
- <div class="card-b"><div class="bars" id="fuelwarnBars"></div></div>
- </article>
- <article class="card">
- <div class="card-h"><h2>区间明细</h2></div>
- <div class="card-b" id="fuelwarnTable"></div>
- </article>
- </section>
- <section id="view-etc" class="hidden">
- <div class="page-head">
- <div><h1>ETC充值卡管理</h1><p>按自有车辆查看当月 ETC 充值次数、公里与金额,点击卡片或「新增申请」登记。</p></div>
- <div class="actions">
- <button class="btn btn-primary" type="button" onclick="openEtcDlg()">新增申请</button>
- </div>
- </div>
- <div class="fuel-plate-grid" id="etcPlateCards"></div>
- <article class="card"><div class="card-h"><h2>申请记录</h2></div><div class="card-b" id="etcTable"></div></article>
- </section>
- <section id="view-oilcard" class="hidden">
- <div class="page-head">
- <div><h1>加油充值卡管理</h1><p>按自有车辆查看当月油卡充值次数、公里与金额,点击卡片或「新增申请」登记。</p></div>
- <div class="actions">
- <button class="btn btn-primary" type="button" onclick="openOilCardDlg()">新增申请</button>
- </div>
- </div>
- <div class="fuel-plate-grid" id="oilCardPlateCards"></div>
- <article class="card"><div class="card-h"><h2>申请记录</h2></div><div class="card-b" id="oilCardTable"></div></article>
- </section>
- <section id="view-maint" class="hidden">
- <div class="page-head">
- <div><h1>车辆维保管理</h1><p>按自有车辆查看本年度维修次数、维修金额,以及本月 / 本年度合计。点击「新增申请」弹出表单登记。</p></div>
- <div class="actions">
- <button class="btn btn-primary" type="button" onclick="openMaintDlg()">新增申请</button>
- </div>
- </div>
- <div class="fuel-plate-grid" id="maintPlateCards"></div>
- <article class="card"><div class="card-h"><h2>维保记录</h2></div><div class="card-b" id="maintTable"></div></article>
- </section>
- <section id="view-stats" class="hidden">
- <div class="page-head">
- <div><h1>统计</h1><p>本月油耗、维保费用与 ETC 申请笔数,辅助单车成本对照。</p></div>
- </div>
- <div class="stats" id="statKpis"></div>
- <div class="grid">
- <article class="card">
- <div class="card-h"><h2>单车加油量</h2></div>
- <div class="card-b"><div class="bars" id="statBars"></div></div>
- </article>
- <article class="card">
- <div class="card-h"><h2>本月费用构成</h2></div>
- <div class="card-b">
- <div class="pie-wrap">
- <svg width="140" height="140" viewBox="0 0 32 32" id="pie"></svg>
- <div class="legend" id="pieLegend"></div>
- </div>
- </div>
- </article>
- </div>
- <article class="card" style="margin-top:12px">
- <div class="card-h"><h2>资质到期一览</h2></div>
- <div class="card-b" id="certTable"></div>
- </article>
- </section>
- </div>
- <div id="mod-plan" class="hidden">
- <section id="view-plan" class="erp-page">
- <div class="alerts" id="planAlerts"></div>
- <div class="rel-flow">易制毒 / 易制爆计划须先 <em>上传购买证及运输证</em>,再在证件页 <em>人工点击关联对应计划</em>。系统按购买证、运输证数量计算各车运输计划量;计划完成后扣减已完成量;购买证或运输证余量 ≤ 32 吨则预警。</div>
- <div class="stats" id="planKpis"></div>
- <div class="erp-tabs-row">
- <div class="plan-tabs" id="planTabs">
- <button type="button" data-tab="待安排">待安排</button>
- <button type="button" data-tab="已安排">已安排</button>
- <button type="button" data-tab="草稿中">草稿中</button>
- <button class="on" type="button" data-tab="全部">全部</button>
- <button type="button" data-tab="入库单">入库单</button>
- <button type="button" data-tab="出库单">出库单</button>
- <button type="button" data-tab="直销单">直销单</button>
- </div>
- </div>
- <div class="erp-tabs-row plan-action-row">
- <div class="plan-tabs" id="planDateTabs">
- <button class="on" type="button" data-tab="全部">全部</button>
- <button type="button" data-tab="昨日">昨日</button>
- <button type="button" data-tab="今日">今日</button>
- <button type="button" data-tab="明日">明日</button>
- <button type="button" data-tab="本周">本周</button>
- </div>
- <label class="quote-search plan-search">
- <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
- <input id="planQ" type="search" placeholder="计划编号/供方/需方/产品名称/车牌号" oninput="renderPlans()" />
- </label>
- </div>
- <div class="erp-toolbar plan-toolbar">
- <button class="btn btn-primary" type="button" onclick="openPlanDlg()">+ 新增计划</button>
- <button class="btn" type="button" onclick="copyPlan()">
- <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>
- 复制
- </button>
- <button class="btn" type="button" onclick="openPlanTaskDlg()">
- <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>
- 下发任务单
- </button>
- <button class="btn" type="button" onclick="printPlanDocs()">
- <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>
- 打印单据
- </button>
- <button class="btn" type="button" onclick="voidSelectedPlans()">
- <svg class="btn-ico" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><path d="M8 12h8"/></svg>
- 作废
- </button>
- <button class="btn btn-danger" type="button" onclick="createTriParty()">三方人车快速创建</button>
- <button class="btn" type="button" onclick="toast('演示:已导出当前列表')">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 4v10M8 10l4 4 4-4M5 18h14"/></svg>
- 导出
- </button>
- <button class="btn" type="button" onclick="goLinkBuyTransCert()">去关联购买&运输证</button>
- </div>
- <article class="card erp-card">
- <div class="erp-table-wrap" id="planTable"></div>
- <div class="plan-foot">
- <span class="muted" id="planCount"></span>
- <span class="muted">搜索合计
- <select class="page-size" id="planPageSize" onchange="renderPlans()">
- <option>20条/页</option>
- <option>50条/页</option>
- </select>
- </span>
- </div>
- </article>
- </section>
- </div>
- <div id="mod-archive" class="hidden">
- <section id="view-archive" class="erp-page">
- <div class="page-head">
- <div>
- <h1>运输归档</h1>
- <p>运输计划完成后进入本页待审。人工审核通过后归档:出库单 / 入库单按品名自动入对应仓库;直销单无中转仓库,不记库存。</p>
- </div>
- <div class="actions">
- <button class="btn" type="button" onclick="showMod('plan')">返回运输计划</button>
- <button class="btn btn-primary" type="button" onclick="showMod('warehouse')">查看仓储</button>
- </div>
- </div>
- <div class="stats" id="archiveStats"></div>
- <div class="erp-tabs-row">
- <div class="plan-tabs" id="archiveTabs">
- <button class="on" type="button" data-tab="待审核">待审核</button>
- <button type="button" data-tab="已归档">已归档</button>
- <button type="button" data-tab="全部">全部</button>
- </div>
- <div class="erp-hint" id="archiveHintBar"></div>
- </div>
- <div class="erp-toolbar">
- <button class="btn btn-primary" type="button" onclick="batchReviewArchive()">审核归档</button>
- <button class="btn" type="button" onclick="toast('演示:打印 / 导出')">打印 / 导出</button>
- <input id="archiveQ" type="search" placeholder="单号 / 品名 / 客户 / 车牌" oninput="renderArchive()" />
- <select id="archiveType" onchange="renderArchive()">
- <option value="">全部类型</option>
- <option value="出库单">出库单</option>
- <option value="入直单">入库单</option>
- <option value="直销单">直销单</option>
- </select>
- <select id="archiveWh" onchange="renderArchive()">
- <option value="">全部仓库</option>
- </select>
- </div>
- <article class="card erp-card">
- <div class="erp-table-wrap" id="archiveTable"></div>
- <div class="plan-foot">
- <span class="muted" id="archiveCount"></span>
- <span class="muted">出库 / 入库按品名入仓 直销无中转仓</span>
- </div>
- </article>
- </section>
- </div>
- <div id="mod-task" class="hidden">
- <section id="view-task" class="erp-page">
- <div class="page-head">
- <div>
- <h1>任务单管理</h1>
- <p>运输计划下发给现场后进入本页。现场人员接收任务后可回传完成:稀释、精制、分装。</p>
- </div>
- <div class="actions">
- <button class="btn" type="button" onclick="showMod('plan')">返回运输计划</button>
- <button class="btn btn-primary" type="button" onclick="showMod('plan');toast('勾选运输计划后点「下发任务单」')">从计划下发</button>
- </div>
- </div>
- <div class="stats" id="taskStats"></div>
- <div class="erp-tabs-row">
- <div class="plan-tabs" id="taskTabs">
- <button class="on" type="button" data-tab="全部">全部</button>
- <button type="button" data-tab="稀释">稀释任务单</button>
- <button type="button" data-tab="精制">精制任务单</button>
- <button type="button" data-tab="分装">分装任务单</button>
- <button type="button" data-tab="待执行">待执行</button>
- <button type="button" data-tab="已完成">已完成</button>
- </div>
- <div class="erp-hint" id="taskHint"></div>
- </div>
- <div class="erp-toolbar">
- <button class="btn" type="button" onclick="batchReceiveTask()">接收任务</button>
- <button class="btn btn-primary" type="button" onclick="batchReportTask()">回传完成</button>
- <span class="split"></span>
- <input id="taskQ" type="search" placeholder="单号 / 品名 / 执行人 / 计划号" oninput="renderTasks()" />
- <select id="taskSt" onchange="renderTasks()">
- <option value="">全部状态</option>
- <option>待执行</option>
- <option>执行中</option>
- <option>已完成</option>
- </select>
- </div>
- <article class="card erp-card">
- <div class="erp-table-wrap" id="taskTable"></div>
- <div class="plan-foot">
- <span class="muted" id="taskCount"></span>
- <span class="muted">现场接收后回传完成</span>
- </div>
- </article>
- </section>
- </div>
- <div id="mod-cert" class="hidden">
- <nav class="subnav" id="certSubnav">
- <button class="on" type="button" data-view="wait">待关联计划</button>
- <button type="button" data-view="list">证件列表</button>
- <button type="button" data-view="detail" id="navDetail" disabled>证件信息</button>
- </nav>
- <section id="view-cert-wait">
- <div class="page-head">
- <div>
- <h1>待关联计划</h1>
- <p>已安排、易制毒且已选车号的运输计划,在此关联购买证和运输证。</p>
- </div>
- </div>
- <div class="stats three" id="waitStats"></div>
- <article class="card wait-link-box" id="certWaitCard">
- <div class="card-h">
- <h2>待关联计划</h2>
- </div>
- <div class="card-b" id="certWaitPlans"></div>
- </article>
- </section>
- <section id="view-cert-list" class="hidden">
- <div class="page-head">
- <div>
- <h1>购买证 / 运输证</h1>
- <p>流程固定三步:先上传购买证和运输证,再人工点击关联对应计划;系统按两证数量计算各车运输计划量,计划完成后扣减已完成量;购买证或运输证余量 ≤ 自设预警量时预警(默认核准总量的 10%,可改)。</p>
- </div>
- <div class="actions">
- <button class="btn btn-primary" type="button" onclick="openCertDlg(false)">上传购买证及运输证</button>
- </div>
- </div>
- <div class="cert-steps hidden" id="certSteps"></div>
- <div class="alerts hidden" id="listAlerts"></div>
- <div class="stats" id="listStats"></div>
- <div class="filters">
- <input id="certQ" placeholder="证号 / 品名 / 购买单位 / 销售单位 / 车号" oninput="renderCertList()" />
- <select id="certSt" onchange="renderCertList()">
- <option value="">全部状态</option>
- <option>有余量</option>
- <option>余量预警</option>
- <option>运输证预警</option>
- <option>已用尽</option>
- <option>即将到期</option>
- <option>超证</option>
- <option>车辆预警</option>
- <option>已核销</option>
- </select>
- <button class="btn" type="button" onclick="openCertDlg(false)">上传购买证及运输证</button>
- </div>
- <article class="card">
- <div class="card-h"><h2>购买证及运输证</h2><span class="muted" id="listCount"></span></div>
- <div class="card-b" style="overflow:auto" id="certListTable"></div>
- </article>
- </section>
- <section id="view-cert-detail" class="hidden">
- <div class="page-head">
- <div>
- <h1 id="detailTitle">证件信息</h1>
- <p id="detailSub">由购买证与运输证识别合并生成</p>
- </div>
- <div class="actions">
- <button class="btn" type="button" onclick="showCertView('list')">返回列表</button>
- <button class="btn" type="button" onclick="openCertLinkPlanDlg(currentCertId)">关联计划</button>
- <button class="btn btn-primary" type="button" onclick="openCertDlg(false, currentCertId)">追加运输证</button>
- </div>
- </div>
- <div class="rel-flow" id="certRelHint"></div>
- <div class="stats" id="detailStats"></div>
- <article class="card" id="certWarnQtyCard" style="margin-top:12px"></article>
- <div class="alerts" id="detailWarns"></div>
- <article class="card" style="margin-top:12px">
- <div class="card-h">
- <h2>关联车辆计划量预警</h2>
- </div>
- <div class="card-b veh-board">
- <div class="qty-formula" id="certQtyFormula"></div>
- <div id="certVehicleWarn"></div>
- </div>
- </article>
- <div id="detailSheet"></div>
- <article class="card" style="margin-top:12px">
- <div class="card-h">
- <h2>已关联运输计划</h2>
- </div>
- <div class="card-b" id="certLinkedPlans"></div>
- </article>
- <article class="card" style="margin-top:12px">
- <div class="card-h">
- <h2>按运输证分派车辆</h2>
- <div class="actions">
- <button class="btn btn-primary" type="button" onclick="openCertDlg(false, currentCertId)">追加运输证</button>
- </div>
- </div>
- <div class="card-b">
- <p class="muted" id="dispatchHint" style="margin-bottom:10px"></p>
- <div id="detailDispatch"></div>
- </div>
- </article>
- </section>
- </div>
- <div id="mod-expense" class="hidden">
- <section id="view-expense" class="erp-page">
- <div class="page-head">
- <div>
- <h1>报销单据管理</h1>
- </div>
- <div class="actions">
- <button class="btn" type="button" onclick="openPreExpDlg()">备用金申请</button>
- <button class="btn btn-primary" type="button" onclick="openExpenseDlg()">填报报销单</button>
- </div>
- </div>
- <div class="exp-board" id="expFlow"></div>
- <div class="alerts" id="expAlerts"></div>
- <div class="erp-tabs-row">
- <div class="plan-tabs" id="expTabs">
- <button class="on" type="button" data-tab="全部">全部</button>
- <button type="button" data-tab="我的报销">我的报销</button>
- <button type="button" data-tab="备用金申请">我的备用金</button>
- <button type="button" data-tab="待财务初审">财务初审</button>
- <button type="button" data-tab="待终审">领导审核</button>
- <button type="button" data-tab="待付款">出纳付款</button>
- <button type="button" data-tab="待归档">凭证归档</button>
- <button type="button" data-tab="借款">借款 / 冲销</button>
- </div>
- <div class="erp-hint" id="expHint"></div>
- </div>
- <div class="erp-toolbar" id="expToolbar"></div>
- <article class="card erp-card">
- <div class="erp-table-wrap" id="expTable"></div>
- <div class="plan-foot">
- <span class="muted" id="expCount"></span>
- <span class="muted" id="expFootHint">报销:财务核票 → 领导审核 → 出纳付款 备用金:财务核额 → 先支出 → 事后报销冲销</span>
- </div>
- </article>
- </section>
- </div>
- <div id="mod-duty" class="hidden">
- <section id="view-duty" class="erp-page">
- <div class="duty-bar">
- <div class="duty-brand">
- <svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="15" rx="2"/><path d="M8 3v4M16 3v4M4 10h16"/></svg>
- 值班排班
- </div>
- <div class="duty-nav">
- <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>
- <div class="m" id="dutyMonthLab">2026年10月</div>
- <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>
- </div>
- <button class="btn" type="button" onclick="goDutyToday()">今天</button>
- <div class="duty-legend">
- <span><i class="am"></i>早班</span>
- <span><i class="pm"></i>晚班</span>
- <span><i class="watch"></i>值班</span>
- <span><i class="other"></i>其他</span>
- <span>点击日期配置</span>
- <button class="duty-link" type="button" onclick="copyDutyLink()">复制链接</button>
- </div>
- </div>
- <div class="duty-stats">
- <div class="duty-pills" id="dutyPills"></div>
- <div class="duty-people" id="dutyPeople"></div>
- </div>
- <div class="duty-cal">
- <div class="duty-week">
- <span>日</span><span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span>
- </div>
- <div class="duty-grid" id="dutyGrid"></div>
- </div>
- </section>
- </div>
- <div id="mod-staff" class="hidden">
- <section id="view-staff" class="erp-page">
- <div class="stats" id="staffStats"></div>
- <div class="erp-tabs-row">
- <div class="plan-tabs" id="staffTabs">
- <button class="on" type="button" data-tab="全部" onclick="setStaffTab('全部')">全部</button>
- <button type="button" data-tab="在职" onclick="setStaffTab('在职')">在职</button>
- <button type="button" data-tab="试用" onclick="setStaffTab('试用')">试用</button>
- <button type="button" data-tab="离职" onclick="setStaffTab('离职')">离职</button>
- </div>
- <select class="staff-dept-sel" id="staffDept" onchange="staffPage=1;renderStaff()">
- <option value="">全部部门</option>
- </select>
- <label class="quote-search wide">
- <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
- <input id="staffQ" type="search" placeholder="姓名 / 工号 / 岗位 / 手机" oninput="staffPage=1;renderStaff()" />
- </label>
- </div>
- <div class="erp-toolbar">
- <button class="btn btn-primary" type="button" onclick="openStaffDlg()">+ 新增员工</button>
- <button class="btn" type="button" onclick="editSelectedStaff()">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M4 20h4l11-11-4-4L4 16v4z"/><path d="m13 7 4 4"/></svg>
- 编辑
- </button>
- <button class="btn" type="button" onclick="leaveSelectedStaff()">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M15 8v8M9 12h12M9 12l3-3M9 12l3 3M5 5v14"/></svg>
- 办理离职
- </button>
- <button class="btn" type="button" onclick="toast('演示:已导出员工档案')">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M12 4v10M8 10l4 4 4-4M5 18h14"/></svg>
- 导出
- </button>
- <div class="right">
- <button class="erp-ico-btn" type="button" title="刷新" onclick="staffPage=1;renderStaff()">↻</button>
- <button class="erp-ico-btn" type="button" title="列设置" onclick="toast('演示:列设置')">☰</button>
- </div>
- </div>
- <article class="card erp-card">
- <div class="erp-table-wrap" id="staffTable"></div>
- <div class="plan-foot">
- <span class="muted" id="staffSelHint">已选 0 项</span>
- <span class="quote-pager" id="staffPager"></span>
- </div>
- </article>
- </section>
- </div>
- <div id="mod-warehouse" class="hidden">
- <nav class="subnav" id="whSubnav">
- <button class="on" type="button" data-view="info" onclick="showWhView('info')">库存信息表</button>
- <button type="button" data-view="mode" onclick="showWhView('mode')">仓库信息表</button>
- <button type="button" data-view="proc" onclick="showWhView('proc')">加工单明细表</button>
- <button type="button" data-view="stock" onclick="showWhView('stock')">库存成本明细表</button>
- </nav>
- <div class="page-head">
- <div>
- <h1 id="whPageTitle">库存信息表</h1>
- <p id="whPageDesc">98% 工业硫酸可直销、加水稀释。试剂硫酸单独成线。工业 / 试剂分罐。</p>
- </div>
- <div class="actions" id="whPageActions"></div>
- </div>
- <div class="wh-tree">
- <article class="card wh-nav-card">
- <div class="wh-nav" id="whNav"></div>
- </article>
- <div class="stack">
- <div id="view-wh-info"></div>
- <div id="view-wh-mode" class="hidden"></div>
- <div id="view-wh-proc" class="hidden"></div>
- <div id="view-wh-stock" class="hidden">
- <div class="wh-kpis" id="whKpis"></div>
- <article class="card" id="whLedgerCard">
- <div class="card-h">
- <h2 id="whTableTitle">库存成本明细账</h2>
- <span class="muted" id="whTableCount"></span>
- </div>
- <div class="filters" style="padding: 10px 14px 0">
- <input id="whQ" placeholder="单号 / 物料 / 往来单位 / 单据类型" oninput="renderWhTable()" />
- <select id="whDocFilter" onchange="onWhDocFilter()">
- <option value="">全部单据</option>
- <option>进货单</option>
- <option>产成品入库单</option>
- <option>其他入库单</option>
- <option>调拨单</option>
- <option>组装单</option>
- <option>形态转换单</option>
- <option>拆卸单</option>
- <option>盘点单</option>
- <option>销货单</option>
- <option>材料出库单</option>
- <option>其他出库单</option>
- </select>
- </div>
- <div class="card-b" style="overflow:auto" id="whTable"></div>
- </article>
- </div>
- </div>
- </div>
- </div>
- </div>
- </section>
- </div>
- <div class="toast" id="toast">已保存</div>
- <dialog class="apply-dlg" id="prodDialog">
- <div class="dlg-h green">
- <h3 id="prodDlgTitle">新增产品</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('prodDialog').close()">×</button>
- </div>
- <form id="prodForm" onsubmit="return false">
- <input type="hidden" name="id" />
- <div class="dlg-b">
- <div class="apply">
- <div class="row"><div class="lab"><span class="star">*</span>产品名称</div><input name="name" required placeholder="如 工业硫酸" /></div>
- <div class="row"><div class="lab">产品编号</div><input name="no" placeholder="留空则自动生成" /></div>
- <div class="row"><div class="lab">产品分类</div>
- <select name="cat" id="prodCatSelect"></select>
- </div>
- <div class="row"><div class="lab">品牌</div><input name="brand" placeholder="如 益泉" /></div>
- <div class="row"><div class="lab">别名</div><input name="alias" /></div>
- <div class="row"><div class="lab">规格型号</div><input name="spec" placeholder="如 98%" /></div>
- <div class="row"><div class="lab">标准价(元)</div><input name="price" type="number" step="0.01" min="0" placeholder="选填" /></div>
- <div class="row"><div class="lab">拼音首字母</div><input name="py" placeholder="如 GYSF" /></div>
- <div class="row"><div class="lab">危化标识</div>
- <select name="hz"><option value="">无</option><option>毒</option><option>爆</option></select>
- </div>
- <div class="row"><div class="lab">状态</div>
- <select name="status"><option>上架</option><option>下架</option></select>
- </div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="document.getElementById('prodDialog').close()">取消</button>
- <button class="btn btn-primary" type="button" onclick="saveProd()">保存</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg" id="prodCatDialog" style="width:480px;max-width:92vw">
- <div class="dlg-h green">
- <h3>产品分类</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('prodCatDialog').close()">×</button>
- </div>
- <div class="dlg-b">
- <div class="prod-cat-list" id="prodCatList"></div>
- <div style="display:flex;gap:8px;margin-top:12px">
- <input id="prodCatNew" placeholder="新分类名称" style="flex:1;height:32px;border:1px solid var(--line);border-radius:8px;padding:0 10px" />
- <button class="btn btn-primary" type="button" onclick="addProdCat()">新增</button>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="document.getElementById('prodCatDialog').close()">关闭</button>
- </div>
- </dialog>
- <dialog class="apply-dlg" id="quoteDialog">
- <div class="dlg-h green">
- <h3 id="quoteDlgTitle">新增报价单</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('quoteDialog').close()">×</button>
- </div>
- <form id="quoteForm" onsubmit="return false">
- <div class="dlg-b">
- <div class="apply">
- <div class="row"><div class="lab"><span class="star">*</span>客户名称</div><input name="customer" required placeholder="如 安庆汇辰科技有限公司" /></div>
- <div class="row"><div class="lab"><span class="star">*</span>报价主题</div><input name="title" required placeholder="如 山峡-安庆汇辰" /></div>
- <div class="row"><div class="lab">报价类型</div>
- <select name="kind"><option>销售</option><option>代工</option><option>询价</option></select>
- </div>
- <div class="row"><div class="lab">报价日期</div><input name="date" type="date" required /></div>
- <div class="row"><div class="lab"><span class="star">*</span>产品信息</div><input name="product" required placeholder="如 片碱 2吨" /></div>
- <div class="row"><div class="lab"><span class="star">*</span>产品金额</div><input name="prodAmt" type="number" step="0.01" min="0" required /></div>
- <div class="row"><div class="lab">运费</div><input name="freight" type="number" step="0.01" min="0" value="0" /></div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="document.getElementById('quoteDialog').close()">取消</button>
- <button class="btn btn-primary" type="button" onclick="saveQuote()">保存草稿</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg" id="ctDialog">
- <div class="dlg-h green">
- <h3 id="ctDlgTitle">新增合同</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('ctDialog').close()">×</button>
- </div>
- <form id="ctForm" onsubmit="return false">
- <div class="dlg-b">
- <div class="apply">
- <div class="row"><div class="lab"><span class="star">*</span>客户名称</div><input name="customer" required placeholder="如 安庆汇辰科技有限公司" /></div>
- <div class="row"><div class="lab"><span class="star">*</span>合同名称</div><input name="title" required placeholder="如 山峡-安庆汇辰" /></div>
- <div class="row"><div class="lab">合同类型</div>
- <select name="kind"><option>销售合同</option><option>框架销售合同</option><option>采购合同</option></select>
- </div>
- <div class="row"><div class="lab">签订日期</div><input name="signDate" type="date" required /></div>
- <div class="row"><div class="lab"><span class="star">*</span>产品信息</div><input name="product" required placeholder="如 片碱 2吨" /></div>
- <div class="row"><div class="lab"><span class="star">*</span>合同金额</div><input name="amount" type="number" step="0.01" min="0" required /></div>
- <div class="row"><div class="lab">运费</div><input name="freight" type="number" step="0.01" min="0" value="0" /></div>
- <div class="row"><div class="lab">桶押金</div><input name="deposit" type="number" step="0.01" min="0" value="0" /></div>
- <div class="row"><div class="lab">负责人</div><input name="owner" value="万金强" /></div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="document.getElementById('ctDialog').close()">取消</button>
- <button class="btn btn-primary" type="button" onclick="saveContract()">保存草稿</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg" id="ctScanDialog">
- <div class="dlg-h green">
- <h3>上传扫描件</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('ctScanDialog').close()">×</button>
- </div>
- <form id="ctScanForm" onsubmit="return false">
- <div class="dlg-b">
- <p class="muted" id="ctScanHint" style="margin-bottom:12px">为所选合同上传双章 / 单章扫描件。</p>
- <div class="apply">
- <div class="row"><div class="lab">附件类型</div>
- <select name="att"><option>双章</option><option>单章</option></select>
- </div>
- <div class="row"><div class="lab"><span class="star">*</span>扫描件</div>
- <input name="file" type="file" accept="application/pdf,image/*" required />
- </div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="document.getElementById('ctScanDialog').close()">取消</button>
- <button class="btn btn-primary" type="button" onclick="saveCtScan()">保存</button>
- </div>
- </form>
- </dialog>
- <dialog class="exp-dlg" id="expenseDialog">
- <div class="dlg-h green">
- <h3 id="expDlgTitle">新增报销申请</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeExpenseDlg()">×</button>
- </div>
- <form id="expenseForm" onsubmit="return false">
- <div class="dlg-b">
- <div class="exp-new-top">
- <label>申请人
- <select name="user" required onchange="onExpUserChange()"></select>
- </label>
- <label>报销日期
- <div class="exp-field-date">
- <svg viewBox="0 0 24 24"><rect x="4" y="6" width="16" height="14" rx="2"/><path d="M8 4v4M16 4v4M4 11h16"/></svg>
- <input name="date" type="date" required onchange="syncExpTitle()" />
- </div>
- </label>
- <label>报销标题
- <input name="title" required placeholder="自动生成,可修改" />
- </label>
- </div>
- <div class="exp-block tight">
- <h4>报销明细</h4>
- <div class="exp-line-wrap">
- <table class="exp-line-table">
- <thead>
- <tr>
- <th style="width:52px">序号</th>
- <th>费用项目 <span class="star">*</span></th>
- <th style="width:120px">金额(元) <span class="star">*</span></th>
- <th>报销事由 <span class="star">*</span></th>
- <th style="width:150px">发生时间</th>
- </tr>
- </thead>
- <tbody id="expLineBody"></tbody>
- </table>
- </div>
- <div class="exp-line-foot">
- <button class="btn" type="button" onclick="addExpLine()">+ 新增明细行</button>
- <div class="exp-total">合计金额(元):<b id="expLineSum">¥0.00</b></div>
- </div>
- </div>
- <div class="exp-block">
- <h4>关联发票 <span class="opt">(可选)</span></h4>
- <div class="exp-inv-meta">当前发票余额:<b id="expInvBal">¥0.00</b> ;本次合计:<b id="expInvSum">¥0.00</b></div>
- <div class="exp-inv-chips" id="expInvChips"></div>
- <button class="btn" type="button" onclick="openInvPick()">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M4 7h5l2 2h9v10H4z"/></svg>
- 从发票夹选择
- </button>
- </div>
- <div class="exp-block">
- <h4>申请附件 <span class="opt">(可选)</span></h4>
- <label class="exp-up">
- <input name="attach" type="file" multiple accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.zip,.rar" onchange="previewExpFiles(this)" />
- <span class="plus">+</span>
- <span>上传附件</span>
- </label>
- <div class="chips" id="expFileChips" style="margin-top:8px"></div>
- <p class="hint">补充行程单、说明截图、合同等;与发票夹无关。支持图片、视频、PDF、Office、压缩包等。</p>
- </div>
- <div class="exp-block">
- <h4>备注说明</h4>
- <textarea class="exp-note" name="remark" placeholder="请输入报销说明"></textarea>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeExpenseDlg()">× 取消</button>
- <button class="btn" type="button" onclick="saveExpense(false)">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M6 4h9l5 5v11H6z"/><path d="M14 4v5h5M8 13h8M8 17h6"/></svg>
- 保存草稿
- </button>
- <button class="btn btn-primary" type="button" onclick="saveExpense(true)">
- <svg class="btn-ico" viewBox="0 0 24 24"><path d="M4 12 20 4l-6 16-2-7z"/></svg>
- 提交审批
- </button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg" id="invPickDialog">
- <div class="dlg-h green">
- <h3>从发票夹选择</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('invPickDialog').close()">×</button>
- </div>
- <div class="dlg-b">
- <p class="muted" style="margin-bottom:10px">勾选未核销发票,关联到本张报销单。可选。</p>
- <div class="inv-pick-list" id="invPickList"></div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="document.getElementById('invPickDialog').close()">取消</button>
- <button class="btn btn-primary" type="button" onclick="confirmInvPick()">确认选择</button>
- </div>
- </dialog>
- <dialog class="apply-dlg" id="preExpDialog">
- <div class="dlg-h green">
- <h3>备用金申请</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('preExpDialog').close()">×</button>
- </div>
- <form id="preExpForm" onsubmit="return false">
- <div class="dlg-b">
- <p class="muted" style="margin-bottom:12px">事前申请,无需发票。财务核额通过后再支出,事后凭票报销冲销。与报销单(事后贴票结算)不是同一条流程。</p>
- <div class="apply">
- <div class="row"><div class="lab"><span class="star">*</span>申请人</div><input name="user" required value="周建军" /></div>
- <div class="row"><div class="lab"><span class="star">*</span>部门</div>
- <select name="dept" required>
- <option>运输部</option><option>生产部</option><option>行政人事</option><option>销售部</option><option>仓储部</option>
- </select>
- </div>
- <div class="row"><div class="lab"><span class="star">*</span>事项</div>
- <select name="type" required>
- <option>出差</option><option>采购</option><option>招待</option><option>其他</option>
- </select>
- </div>
- <div class="row"><div class="lab"><span class="star">*</span>预估金额</div><input name="amount" type="number" min="0" step="0.01" required /></div>
- <div class="row full"><div class="lab"><span class="star">*</span>申请事由</div><textarea name="remark" required placeholder="为何支出、预计发生时间"></textarea></div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="document.getElementById('preExpDialog').close()">取消</button>
- <button class="btn btn-primary" type="button" onclick="savePreExp()">提交申请</button>
- </div>
- </form>
- </dialog>
- <dialog class="wide-dlg" id="expDetailDialog">
- <div class="dlg-h green">
- <h3 id="expDetailTitle">报销单</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('expDetailDialog').close()">×</button>
- </div>
- <div class="dlg-b" id="expDetailBody"></div>
- <div class="dlg-f" id="expDetailFoot"></div>
- </dialog>
- <dialog class="apply-dlg" id="expRejectDlg">
- <div class="dlg-h">
- <h3>驳回 / 退回补资料</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('expRejectDlg').close()">×</button>
- </div>
- <form id="expRejectForm" onsubmit="return false">
- <div class="dlg-b">
- <div class="apply">
- <div class="row full"><div class="lab"><span class="star">*</span>原因</div>
- <textarea name="reason" required placeholder="须写明原因,便于经办人补资料后重新发起"></textarea>
- </div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="document.getElementById('expRejectDlg').close()">取消</button>
- <button class="btn btn-primary" type="button" onclick="confirmExpReject()">确认退回</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg" id="dutyDialog" style="width:520px;max-width:94vw">
- <div class="dlg-h green">
- <h3 id="dutyDlgTitle">值班配置</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('dutyDialog').close()">×</button>
- </div>
- <form id="dutyForm" onsubmit="return false">
- <div class="dlg-b">
- <div class="apply">
- <div class="row"><div class="lab">班次</div>
- <select name="type">
- <option value="am">早班</option>
- <option value="pm">晚班</option>
- <option value="watch">值班</option>
- <option value="other">其他</option>
- </select>
- </div>
- <div class="row"><div class="lab">岗位</div>
- <input name="post" value="配酸" />
- </div>
- <div class="row full"><div class="lab">人员</div>
- <div id="dutyStaffPick" class="chips" style="gap:8px"></div>
- </div>
- </div>
- <div id="dutyExist" style="margin-top:12px"></div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="document.getElementById('dutyDialog').close()">取消</button>
- <button class="btn btn-primary" type="button" onclick="saveDutyShift()">保存排班</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg" id="staffDialog">
- <div class="dlg-h green">
- <h3 id="staffDlgTitle">新增员工</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('staffDialog').close()">×</button>
- </div>
- <form id="staffForm" onsubmit="return false">
- <input type="hidden" name="id" />
- <div class="dlg-b">
- <div class="apply">
- <div class="row"><div class="lab"><span class="star">*</span>姓名</div><input name="name" required placeholder="如 吴九华" /></div>
- <div class="row"><div class="lab">工号</div><input name="no" placeholder="留空则自动生成" /></div>
- <div class="row"><div class="lab">性别</div>
- <select name="gender"><option>男</option><option>女</option></select>
- </div>
- <div class="row"><div class="lab"><span class="star">*</span>部门</div>
- <select name="dept" id="staffDeptSelect"></select>
- </div>
- <div class="row"><div class="lab"><span class="star">*</span>岗位</div><input name="post" required placeholder="如 驾驶员" /></div>
- <div class="row"><div class="lab">手机</div><input name="phone" placeholder="11 位手机号" /></div>
- <div class="row"><div class="lab">入职日期</div><input name="hire" type="date" /></div>
- <div class="row"><div class="lab">合同到期</div><input name="contractEnd" type="date" /></div>
- <div class="row"><div class="lab">用工类型</div>
- <select name="kind"><option>劳动合同</option><option>劳务合同</option><option>实习</option></select>
- </div>
- <div class="row"><div class="lab">状态</div>
- <select name="status"><option>在职</option><option>试用</option><option>离职</option></select>
- </div>
- <div class="row"><div class="lab">学历</div>
- <select name="edu"><option>本科</option><option>大专</option><option>高中</option><option>中专</option><option>初中</option></select>
- </div>
- <div class="row"><div class="lab">籍贯</div><input name="native" placeholder="如 安徽安庆" /></div>
- <div class="row"><div class="lab">紧急联系人</div><input name="emergency" /></div>
- <div class="row"><div class="lab">联系人电话</div><input name="emergPhone" /></div>
- <div class="row full"><div class="lab">备注</div><textarea name="remark" placeholder="证件、资质、特殊说明等"></textarea></div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="document.getElementById('staffDialog').close()">取消</button>
- <button class="btn btn-primary" type="button" onclick="saveStaff()">保存</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg" id="staffDetailDialog">
- <div class="dlg-h green">
- <h3 id="staffDetailTitle">员工档案</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="document.getElementById('staffDetailDialog').close()">×</button>
- </div>
- <div class="dlg-b" id="staffDetailBody"></div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="document.getElementById('staffDetailDialog').close()">关闭</button>
- <button class="btn btn-primary" type="button" onclick="editStaffFromDetail()">编辑</button>
- </div>
- </dialog>
- <dialog class="apply-dlg" id="acidDlg">
- <div class="dlg-h green">
- <h3>加水 / 加工转换</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeAcidDlg()">×</button>
- </div>
- <form id="acidForm" onsubmit="return false">
- <div class="dlg-b">
- <p class="muted" style="margin-bottom:12px" id="acidDlgHint">98% 工业硫酸可加水稀释。工业 / 试剂分线,不得混兑。</p>
- <div class="apply">
- <div class="row"><div class="lab"><span class="star">*</span>原料酸</div>
- <select name="rawId" required onchange="onAcidRawChange()"></select>
- </div>
- <div class="row"><div class="lab"><span class="star">*</span>加工去向</div>
- <select name="target" required onchange="onAcidTarget()">
- <option value="30">加水 · 30%</option>
- <option value="50">加水 · 50%</option>
- <option value="custom">加水 · 其他定制浓度</option>
- </select>
- </div>
- <div class="row" id="acidCustomRow" style="display:none"><div class="lab"><span class="star">*</span>定制浓度</div>
- <input name="customPct" type="number" min="1" max="97" step="0.1" placeholder="如 40、70、80" oninput="updateAcidCalc()" />
- </div>
- <div class="row"><div class="lab"><span class="star">*</span>成品数量</div>
- <input name="outQty" type="number" min="0.01" step="0.01" value="20" required oninput="updateAcidCalc()" />
- </div>
- </div>
- <div class="calc-box" id="acidCalc"></div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeAcidDlg()">取消</button>
- <button class="btn" type="button" onclick="saveAcidConvert(false)">确认转换</button>
- <button class="btn btn-primary" type="button" onclick="saveAcidConvert(true)">确认并打印加工单</button>
- </div>
- </form>
- </dialog>
- <dialog id="attachDialog">
- <div class="dlg-h">
- <h3 id="attachTitle">附件</h3>
- <button class="btn" type="button" onclick="closeAttach()">关闭</button>
- </div>
- <div class="dlg-b" id="attachBody"></div>
- </dialog>
- <dialog class="apply-dlg wide-dlg" id="fuelDialog">
- <div class="dlg-h green">
- <h3>加油登记</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeFuelDlg()">×</button>
- </div>
- <form class="form" id="fuelForm" onsubmit="return addFuel(event)" style="grid-template-columns:1fr;padding:0;display:flex;flex-direction:column;gap:0">
- <div class="dlg-b" style="display:grid;grid-template-columns:1fr 1fr;gap:10px;width:100%">
- <label>车辆<select name="plate" required></select></label>
- <label>日期<input name="date" type="date" required /></label>
- <label>加油站<input name="station" required placeholder="如 中石化猇亭站" /></label>
- <label>油品<select name="oil"><option>0# 柴油</option><option>-10# 柴油</option><option>LNG</option></select></label>
- <label>升数<input name="liters" type="number" step="0.01" required /></label>
- <label>金额(元)<input name="amount" type="number" step="0.01" required /></label>
- <label>当前里程(km)<input name="km" type="number" required /></label>
- <label>备注<input name="note" placeholder="可选" /></label>
- <label class="full" style="grid-column:1 / -1">附件(必传)
- <div class="file-box">
- <input name="attach" type="file" required multiple accept="image/*,.pdf,application/pdf" onchange="previewFuelFiles(this)" />
- <span class="file-hint">须上传加油小票、发票或加油凭证(图片或 PDF,可多选)</span>
- <div class="chips" id="fuelFileChips"></div>
- </div>
- </label>
- </div>
- <div class="dlg-f" style="grid-column:1 / -1">
- <button class="btn" type="button" onclick="closeFuelDlg()">取消</button>
- <button class="btn btn-primary" type="submit">保存登记</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg wide-dlg" id="maintDialog">
- <div class="dlg-h green">
- <h3>新增申请</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeMaintDlg()">×</button>
- </div>
- <form id="maintForm" onsubmit="return addMaint(event)">
- <div class="dlg-b">
- <div class="apply">
- <div class="row">
- <div class="lab"><span class="star">*</span>申请人</div>
- <select name="applicant" required></select>
- </div>
- <div class="row">
- <div class="lab"><span class="star">*</span>车牌号</div>
- <select name="plate" required>
- <option value="">请选择车牌</option>
- </select>
- </div>
- <div class="row">
- <div class="lab"><span class="star">*</span>维保类型</div>
- <select name="type" required>
- <option value="">请选择或输入维保类型</option>
- <option>保养</option>
- <option>维修</option>
- <option>年检</option>
- </select>
- </div>
- <div class="row">
- <div class="lab"><span class="star">*</span>申请日期</div>
- <input name="applyDate" type="date" required />
- </div>
- <div class="row">
- <div class="lab"><span class="star">*</span>开始日期</div>
- <input name="inDate" type="date" required />
- </div>
- <div class="row">
- <div class="lab">预计取车日期</div>
- <input name="outDate" type="date" />
- </div>
- <div class="row full">
- <div class="lab">维保内容</div>
- <textarea name="item" placeholder="请输入维保内容"></textarea>
- </div>
- <div class="row full">
- <div class="lab">维修店地址</div>
- <input name="shop" placeholder="请输入维修店地址" />
- </div>
- <div class="row full">
- <div class="lab">附件</div>
- <div>
- <label class="upload">
- <input name="attach" type="file" multiple accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,application/pdf" onchange="previewMaintFiles(this)" />
- <span class="plus">+</span>
- <span>上传附件</span>
- </label>
- <div class="hint">支持图片、视频、PDF、Word、Excel 等文件;可拖拽或在此区域粘贴上传</div>
- <div class="chips" id="maintFileChips" style="margin-top:8px"></div>
- </div>
- </div>
- <div class="row full">
- <div class="lab">备注</div>
- <textarea name="note" placeholder="备注"></textarea>
- </div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeMaintDlg()">× 取消</button>
- <button class="btn btn-primary" type="submit">✓ 确定</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg" id="etcDialog">
- <div class="dlg-h green">
- <h3>新增申请</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeEtcDlg()">×</button>
- </div>
- <form id="etcForm" onsubmit="return false">
- <div class="dlg-b">
- <div class="apply">
- <div class="row">
- <div class="lab"><span class="star">*</span>申请人</div>
- <select name="applicant" required></select>
- </div>
- <div class="row">
- <div class="lab"><span class="star">*</span>车牌号</div>
- <select name="plate" required onchange="syncEtcKm()"></select>
- </div>
- <div class="row">
- <div class="lab"><span class="star">*</span>充值类型</div>
- <select name="type" required>
- <option>ETC</option>
- </select>
- </div>
- <div class="row">
- <div class="lab"><span class="star">*</span>申请日期</div>
- <input name="date" type="date" required />
- </div>
- <div class="row">
- <div class="lab"><span class="star">*</span>充值金额</div>
- <input name="amount" type="number" min="0" step="0.01" required placeholder="元" />
- </div>
- <div class="row">
- <div class="lab">使用金额</div>
- <input name="used" type="number" min="0" step="0.01" placeholder="元" />
- </div>
- <div class="row full">
- <div class="lab"><span class="star">*</span>期初公里数</div>
- <div class="stepper">
- <button type="button" onclick="stepKm(-1)">−</button>
- <input name="km" type="number" min="0" required placeholder="请输入期初公里数" />
- <button type="button" onclick="stepKm(1)">+</button>
- </div>
- </div>
- <div class="row full">
- <div class="lab"><span class="star">*</span>附件</div>
- <div>
- <label class="upload">
- <input name="attach" type="file" required multiple accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,application/pdf" onchange="previewEtcFiles(this)" />
- <span class="plus">+</span>
- <span>上传附件</span>
- </label>
- <div class="hint">支持图片、视频、PDF、Word、Excel 等文件;可拖拽或在此区域粘贴上传</div>
- <div class="chips" id="etcFileChips" style="margin-top:8px"></div>
- </div>
- </div>
- <div class="row full">
- <div class="lab">备注</div>
- <textarea name="note" placeholder="备注"></textarea>
- </div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeEtcDlg()">× 取消</button>
- <button class="btn" type="button" onclick="saveEtc(false)">✓ 保存</button>
- <button class="btn btn-primary" type="button" onclick="saveEtc(true)">↑ 保存并提交</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg" id="oilCardDialog">
- <div class="dlg-h green">
- <h3>新增申请</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeOilCardDlg()">×</button>
- </div>
- <form id="oilCardForm" onsubmit="return false">
- <div class="dlg-b">
- <div class="apply">
- <div class="row">
- <div class="lab"><span class="star">*</span>申请人</div>
- <select name="applicant" required></select>
- </div>
- <div class="row">
- <div class="lab"><span class="star">*</span>车牌号</div>
- <select name="plate" required onchange="syncOilKm()"></select>
- </div>
- <div class="row">
- <div class="lab"><span class="star">*</span>充值类型</div>
- <select name="type" required>
- <option>油卡</option>
- </select>
- </div>
- <div class="row">
- <div class="lab"><span class="star">*</span>申请日期</div>
- <input name="date" type="date" required />
- </div>
- <div class="row">
- <div class="lab"><span class="star">*</span>充值金额</div>
- <input name="amount" type="number" min="0" step="0.01" required placeholder="元" />
- </div>
- <div class="row">
- <div class="lab">使用金额</div>
- <input name="used" type="number" min="0" step="0.01" placeholder="元" />
- </div>
- <div class="row full">
- <div class="lab"><span class="star">*</span>期初公里数</div>
- <div class="stepper">
- <button type="button" onclick="stepOilKm(-1)">−</button>
- <input name="km" type="number" min="0" required placeholder="请输入期初公里数" />
- <button type="button" onclick="stepOilKm(1)">+</button>
- </div>
- </div>
- <div class="row full">
- <div class="lab"><span class="star">*</span>附件</div>
- <div>
- <label class="upload">
- <input name="attach" type="file" required multiple accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,application/pdf" onchange="previewOilCardFiles(this)" />
- <span class="plus">+</span>
- <span>上传附件</span>
- </label>
- <div class="hint">支持图片、视频、PDF、Word、Excel 等文件;可拖拽或在此区域粘贴上传</div>
- <div class="chips" id="oilCardFileChips" style="margin-top:8px"></div>
- </div>
- </div>
- <div class="row full">
- <div class="lab">备注</div>
- <textarea name="note" placeholder="备注"></textarea>
- </div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeOilCardDlg()">× 取消</button>
- <button class="btn" type="button" onclick="saveOilCard(false)">✓ 保存</button>
- <button class="btn btn-primary" type="button" onclick="saveOilCard(true)">↑ 保存并提交</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg" id="planDialog">
- <div class="dlg-h green">
- <h3>新增计划</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closePlanDlg()">×</button>
- </div>
- <form id="planForm" onsubmit="return false">
- <div class="dlg-b">
- <div class="apply">
- <div class="row"><div class="lab"><span class="star">*</span>单据类型</div>
- <select name="type" required><option>出库单</option><option value="入直单">入库单</option><option>直销单</option></select>
- </div>
- <div class="row"><div class="lab"><span class="star">*</span>单据日期</div><input name="date" type="date" required /></div>
- <div class="row"><div class="lab">交货日期</div><input name="deliverDate" type="date" /></div>
- <div class="row"><div class="lab"><span class="star">*</span>品名</div><input name="product" required placeholder="如 30%以上烧碱" /></div>
- <div class="row"><div class="lab">产品规格</div><input name="spec" placeholder="如 32%" /></div>
- <div class="row"><div class="lab">管控</div>
- <select name="ctrl"><option value="">普通危化</option><option>易制毒</option><option>易制爆</option></select>
- </div>
- <div class="row"><div class="lab">运输方式</div>
- <select name="shipMode"><option>槽车运输</option><option>车辆运输</option></select>
- </div>
- <div class="row"><div class="lab"><span class="star">*</span>计划数量</div><input name="planQty" type="number" step="0.01" required /></div>
- <div class="row"><div class="lab">单位</div><input name="unit" value="吨" /></div>
- <div class="row"><div class="lab">包装规格</div><input name="pack" value="槽罐车" /></div>
- <div class="row"><div class="lab">业务员</div><input name="salesman" placeholder="如 杨磊" /></div>
- <div class="row"><div class="lab">业务部门</div><input name="dept" value="运输调度组" /></div>
- <div class="row"><div class="lab">发货单位</div><input name="loadFrom" /></div>
- <div class="row"><div class="lab">结算单位</div><input name="settler" /></div>
- <div class="row full"><div class="lab">客户 / 收货</div><input name="customer" /></div>
- <div class="row full"><div class="lab">合同</div>
- <div>
- <div class="chips" id="planCtPick" style="gap:8px"></div>
- <div class="muted" style="margin-top:6px">可多选。同一客户多份合同可并入一条运输计划。</div>
- </div>
- </div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closePlanDlg()">取消</button>
- <button class="btn btn-primary" type="button" onclick="savePlan()">保存待安排</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg wide-dlg" id="planTaskDialog">
- <div class="dlg-h green">
- <h3>下发任务单</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closePlanTaskDlg()">×</button>
- </div>
- <form id="planTaskForm" onsubmit="return false">
- <div class="dlg-b">
- <p class="muted" id="planTaskHint" style="margin-bottom:12px"></p>
- <div class="apply">
- <div class="row"><div class="lab">任务单号</div><input name="no" readonly /></div>
- <div class="row"><div class="lab">任务类型</div>
- <select name="kind" onchange="onPlanTaskKindChange()">
- <option value="分装">分装任务单</option>
- <option value="稀释">稀释任务单</option>
- <option value="精制">精制任务单</option>
- </select>
- </div>
- <div class="row"><div class="lab"><span class="star">*</span>下单日期</div><input name="orderDate" type="date" required /></div>
- <div class="row"><div class="lab"><span class="star">*</span>交货日期</div><input name="deliverDate" type="date" required /></div>
- <div class="row"><div class="lab"><span class="star">*</span>下单人</div><input name="orderBy" required /></div>
- <div class="row"><div class="lab">经手人</div><select name="handler"></select></div>
- <div class="row"><div class="lab">制单人</div><input name="maker" /></div>
- <div class="row"><div class="lab">审核</div><input name="auditor" placeholder="待审核" /></div>
- <div class="row"><div class="lab">生产主管</div><input name="supervisor" /></div>
- <div class="row"><div class="lab">桶型说明</div>
- <select name="bucket">
- <option>桶循环</option>
- <option>全新桶</option>
- <option>是否回收桶</option>
- </select>
- </div>
- <div class="row full"><div class="lab">产品明细</div>
- <div class="task-line-wrap" id="planTaskLines"></div>
- </div>
- <div class="row full"><div class="lab">备注</div>
- <textarea name="remark" placeholder="如客户简称、现场说明"></textarea>
- </div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closePlanTaskDlg()">取消</button>
- <button class="btn btn-primary" type="button" onclick="savePlanTask()">下发任务单</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg wide-dlg" id="taskSheetDialog">
- <div class="dlg-h green">
- <h3>任务单</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeTaskSheetDlg()">×</button>
- </div>
- <div class="dlg-b" id="taskSheetBody"></div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeTaskSheetDlg()">关闭</button>
- <button class="btn btn-primary" type="button" onclick="toast('演示:已打印任务单')">打印</button>
- </div>
- </dialog>
- <dialog class="apply-dlg" id="taskReportDialog">
- <div class="dlg-h green">
- <h3 id="taskReportTitle">回传完成任务</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeTaskReportDlg()">×</button>
- </div>
- <form id="taskReportForm" onsubmit="return false">
- <div class="dlg-b">
- <p class="muted" id="taskReportHint" style="margin-bottom:12px"></p>
- <div class="apply">
- <div class="row"><div class="lab">任务单号</div><input name="no" readonly /></div>
- <div class="row"><div class="lab">指派人员</div><input name="assignee" readonly /></div>
- <div class="row"><div class="lab"><span class="star">*</span>完成时间</div>
- <input name="doneTime" type="datetime-local" required />
- </div>
- <div class="row"><div class="lab"><span class="star">*</span>完成数量</div>
- <input name="doneQty" type="number" min="0" step="0.01" required />
- </div>
- <div class="row full"><div class="lab"><span class="star">*</span>回传说明</div>
- <textarea name="report" required placeholder="现场完成情况、异常或交接说明"></textarea>
- </div>
- <div class="row full">
- <div class="lab"><span class="star">*</span>纸质分装单</div>
- <div>
- <div class="task-photo-row">
- <label class="upload task-photo-up" id="taskSheetPhotoBtn">
- <input name="sheetPhotos" type="file" accept="image/*" capture="environment" multiple onchange="previewTaskSheetPhotos(this)" />
- <span class="plus">📷</span>
- <span>拍照 / 上传</span>
- </label>
- <div class="task-photo-thumbs" id="taskSheetPhotoThumbs"></div>
- </div>
- <p class="file-hint" id="taskSheetPhotoHint" style="margin-top:8px">须拍摄已签字完成的纸质分装任务单,可多张。</p>
- <div class="chips" id="taskSheetPhotoChips"></div>
- </div>
- </div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeTaskReportDlg()">取消</button>
- <button class="btn btn-primary" type="button" id="taskReportSave" onclick="saveTaskReport()">确认回传完成</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg" id="archiveDialog">
- <div class="dlg-h green">
- <h3>运输归档</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeArchiveDlg()">×</button>
- </div>
- <form id="archiveForm" onsubmit="return false">
- <div class="dlg-b">
- <p class="muted" id="archiveHint" style="margin-bottom:12px"></p>
- <div class="apply">
- <div class="row"><div class="lab">单号</div><input id="archiveNo" readonly /></div>
- <div class="row"><div class="lab">类型 / 品名</div><input id="archiveProd" readonly /></div>
- <div class="row"><div class="lab"><span class="star">*</span>实发数量</div>
- <input name="realQty" type="number" step="0.01" min="0.01" required />
- </div>
- <div class="row"><div class="lab">单位</div><input id="archiveUnit" readonly /></div>
- <div class="row full" id="archiveWhRow">
- <div class="lab">对应仓库</div>
- <input id="archiveWhName" readonly />
- <input type="hidden" name="warehouseId" />
- </div>
- </div>
- <p class="hint" id="archiveWhHint">出库单 / 入库单按品名自动对应仓库;直销单无中转仓库。</p>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeArchiveDlg()">取消</button>
- <button class="btn btn-primary" type="button" onclick="confirmArchive()">确认归档</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg" id="dispatchDialog">
- <div class="dlg-h green">
- <h3>分派车辆运输量</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeDispatchDlg()">×</button>
- </div>
- <form id="dispatchForm" onsubmit="return false">
- <div class="dlg-b">
- <p class="muted" id="dispatchDlgHint" style="margin-bottom:12px"></p>
- <div class="apply">
- <div class="row full"><div class="lab">运输公司</div><input name="company" readonly /></div>
- <div class="row"><div class="lab"><span class="star">*</span>车辆</div>
- <select name="plate" required onchange="syncDispatchDriver()"></select>
- </div>
- <div class="row"><div class="lab">驾驶员</div><input name="driver" /></div>
- <div class="row full hidden" id="dispatchNewPlateRow">
- <div class="lab"><span class="star">*</span>新车牌</div>
- <input name="newPlate" placeholder="同公司其他车辆,共用本运输证" />
- </div>
- <div class="row full"><div class="lab"><span class="star">*</span>分派运量</div>
- <input name="qty" type="number" step="0.01" min="0.01" required placeholder="按运输证未分派量填写" />
- </div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeDispatchDlg()">取消</button>
- <button class="btn btn-primary" type="button" onclick="saveDispatch()">确认分派</button>
- </div>
- </form>
- </dialog>
- <dialog class="apply-dlg wide-dlg" id="linkCertDialog">
- <div class="dlg-h green">
- <h3>从计划关联证件</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeLinkCertDlg()">×</button>
- </div>
- <div class="dlg-b">
- <p class="muted" id="linkCertHint" style="margin-bottom:10px"></p>
- <div class="link-sec">
- <h4>选择购买证下的运输证</h4>
- <div id="linkCertTable"></div>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeLinkCertDlg()">取消</button>
- <button class="btn" type="button" onclick="showMod('cert');closeLinkCertDlg()">去上传新证</button>
- </div>
- </dialog>
- <dialog class="apply-dlg wide-dlg wait-link-dlg" id="waitBuyDialog">
- <div class="dlg-h green">
- <h3>关联购买证</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeWaitBuyDlg()">×</button>
- </div>
- <div class="dlg-b">
- <p class="muted" id="waitBuyHint" style="margin-bottom:10px"></p>
- <div id="waitBuyBody"></div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeWaitBuyDlg()">取消</button>
- <button class="btn btn-primary" type="button" onclick="saveWaitBuyCert()">确认关联购买证</button>
- </div>
- </dialog>
- <dialog class="apply-dlg wide-dlg wait-link-dlg" id="waitTransDialog">
- <div class="dlg-h green">
- <h3>关联运输证</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeWaitTransDlg()">×</button>
- </div>
- <div class="dlg-b">
- <p class="muted" id="waitTransHint" style="margin-bottom:10px"></p>
- <div id="waitTransBody"></div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeWaitTransDlg()">取消</button>
- </div>
- </dialog>
- <dialog class="apply-dlg wide-dlg" id="vehPlanDialog">
- <div class="dlg-h green">
- <h3 id="vehPlanTitle">关联运输计划</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeVehPlanDlg()">×</button>
- </div>
- <div class="dlg-b" id="vehPlanBody"></div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeVehPlanDlg()">关闭</button>
- </div>
- </dialog>
- <dialog class="cert-img-dlg" id="certImgDialog" onclick="closeCertImg(event)">
- <img id="certImgView" alt="证件原图" />
- </dialog>
- <dialog class="apply-dlg" id="voidCertDialog">
- <div class="dlg-h green">
- <h3 id="voidCertTitle">核销证件</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeVoidCertDlg()">×</button>
- </div>
- <div class="dlg-b">
- <p class="muted" id="voidCertHint"></p>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeVoidCertDlg()">取消</button>
- <button class="btn btn-danger" type="button" onclick="confirmVoidCert()">确认核销</button>
- </div>
- </dialog>
- <dialog class="apply-dlg wide-dlg" id="certLinkPlanDialog">
- <div class="dlg-h green">
- <h3>人工关联运输计划</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeCertLinkPlanDlg()">×</button>
- </div>
- <div class="dlg-b">
- <p class="muted" id="certLinkPlanHint" style="margin-bottom:10px"></p>
- <div id="certLinkPreview" class="link-preview"></div>
- <label class="quote-search plan-search" style="margin:0 0 10px;max-width:360px">
- <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></svg>
- <input id="certLinkPlanQ" type="search" placeholder="筛选计划编号 / 品名 / 客户 / 车牌" oninput="filterCertLinkPlanRows()" />
- </label>
- <div class="erp-table-wrap" id="certLinkPlanTable"></div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeCertLinkPlanDlg()">取消</button>
- <button class="btn btn-primary" type="button" onclick="saveCertLinkPlans()">确认关联所选计划</button>
- </div>
- </dialog>
- <dialog class="wide-dlg" id="certDialog">
- <div class="dlg-h green">
- <h3 id="certDlgTitle">识别购买证 + 运输证</h3>
- <button class="dlg-x" type="button" aria-label="关闭" onclick="closeCertDlg()">×</button>
- </div>
- <form id="certForm" onsubmit="return false">
- <div class="dlg-b">
- <p class="hint" id="certDlgHint" style="margin-bottom:12px">一张购买证可同时识别、挂多张运输证;也可保存后再继续追加。</p>
- <div class="ocr-pair">
- <div class="ocr-box buy" id="buyOcrBox">
- <h4><span class="cert-tag buy">购买证</span> 第二类、第三类易制毒化学品购买备案证明</h4>
- <p class="hint">上传或拖拽购买证照片或扫描件,读取编号、购买单位、品名与数量。</p>
- <label class="upload">
- <input id="buyFile" type="file" accept="image/*,.pdf" onchange="recognize('buy', this)" />
- <span class="plus">+</span>
- <span>上传购买证</span>
- </label>
- <img class="ocr-preview hidden" id="buyPreview" alt="购买证预览" />
- <div class="ocr-status" id="buyStatus">待上传</div>
- </div>
- <div class="ocr-box trans" id="transOcrBox">
- <h4><span class="cert-tag trans">运输证</span> 第三类易制毒化学品运输备案证明</h4>
- <p class="hint">上传或拖拽运输证。同一购买证可识别多张运输证。</p>
- <label class="upload">
- <input id="transFile" type="file" accept="image/*,.pdf" onchange="recognize('trans', this)" />
- <span class="plus">+</span>
- <span>上传运输证</span>
- </label>
- <img class="ocr-preview hidden" id="transPreview" alt="运输证预览" />
- <div class="ocr-status" id="transStatus">待上传</div>
- </div>
- </div>
- <div class="split-h">
- <div id="buyFormCol" class="col-buy">
- <h4>购买证信息</h4>
- <div class="form" style="grid-template-columns:1fr 1fr">
- <label><span><span class="star">*</span>编号</span><input name="buyNo" required /></label>
- <label>核发日期<input name="buyDate" type="date" /></label>
- <label class="span2"><span><span class="star">*</span>购买单位</span><input name="buyer" required /></label>
- <label>法定代表人<input name="buyerLegal" /></label>
- <label>电话<input name="buyerTel" /></label>
- <label class="span2">住所 / 住址<input name="buyerAddr" /></label>
- <label class="span2">销售单位<input name="seller" /></label>
- <label>销售方法人<input name="sellerLegal" /></label>
- <label>销售方电话<input name="sellerTel" /></label>
- <label class="span2">销售方住所<input name="sellerAddr" /></label>
- <label><span><span class="star">*</span>购买物品</span><input name="product" required /></label>
- <label>用途<input name="usage" /></label>
- <label><span><span class="star">*</span>数量</span><input name="buyQty" type="number" step="0.01" required /></label>
- <label>单位<input name="unit" value="吨" /></label>
- <label>余量预警<input name="warnQty" type="number" step="0.01" min="0" placeholder="核准总量 10%" title="默认按证件核准总量的 10% 预警,可改" /></label>
- <label>有效次数<input name="buyTimes" /></label>
- <label>有效开始<input name="buyFrom" type="date" /></label>
- <label>有效截止<input name="buyDue" type="date" /></label>
- <label class="span2">公安机关<input name="buyPolice" /></label>
- <label>经办人<input name="buyHandler" /></label>
- <label>联系电话<input name="buyPoliceTel" /></label>
- </div>
- </div>
- <div class="col-trans">
- <h4>运输证信息(第 1 张)</h4>
- <div class="form" id="transForm0" style="grid-template-columns:1fr 1fr">
- <label><span><span class="star">*</span>编号</span><input name="transNo" required /></label>
- <label>核发日期<input name="transDate" type="date" /></label>
- <label class="span2">货主<input name="owner" /></label>
- <label>法定代表人<input name="ownerLegal" /></label>
- <label>电话<input name="ownerTel" /></label>
- <label class="span2">货主住所<input name="ownerAddr" /></label>
- <label>包装<input name="pack" /></label>
- <label>购买许可/备案证号<input name="permitNo" /></label>
- <label class="span2">运输单位<input name="carrier" /></label>
- <label>住所/电话<input name="carrierAddr" /></label>
- <label>运输方式<input name="mode" /></label>
- <label>车号(次)<input name="plates" /></label>
- <label class="span2">运输路线<input name="route" /></label>
- <label class="span2">收货单位<input name="consignee" /></label>
- <label>运输数量<input name="transQty" type="number" step="0.01" /></label>
- <label>余量预警<input name="warnQty" type="number" step="0.01" min="0" placeholder="核准总量 10%" title="默认按证件核准总量的 10% 预警,可改" /></label>
- <label>有效次数<input name="transTimes" /></label>
- <label>有效开始<input name="transFrom" type="date" /></label>
- <label>有效截止<input name="transDue" type="date" /></label>
- <label class="span2">申请单位<input name="applicantUnit" /></label>
- <label class="span2">公安机关<input name="transPolice" /></label>
- <label>经办人<input name="transHandler" /></label>
- </div>
- </div>
- </div>
- <div id="extraTransSlots"></div>
- <div style="margin-top:12px">
- <button class="btn" type="button" onclick="addTransSlot()">+ 再识别一张运输证</button>
- </div>
- </div>
- <div class="dlg-f">
- <button class="btn" type="button" onclick="closeCertDlg()">取消</button>
- <button class="btn btn-primary" type="button" id="certSaveBtn" onclick="saveCert()">生成证件信息</button>
- </div>
- </form>
- </dialog>
- <script>
- const today = "2026-10-01";
- const DUTY_TODAY = "2026-10-07";
- const DUTY_STAFF = ["吴自国", "刘茂生", "吴自泰", "黄天宝", "刘丹", "汪璇", "吴良芹"];
- const DUTY_TYPE = { am: "早班", pm: "晚班", watch: "值班", other: "其他" };
- let dutyYM = { y: 2026, m: 10 };
- let dutyPickDate = "";
- let dutyPickPeople = [];
- let dutySeq = 20;
- let dutyShifts = [
- { id: 1, date: "2026-10-01", type: "am", people: ["刘茂生", "吴自国"], post: "配酸" },
- { id: 2, date: "2026-10-01", type: "watch", people: ["黄天宝"], post: "配酸" },
- { id: 3, date: "2026-10-02", type: "am", people: ["吴自泰", "黄天宝"], post: "" },
- { id: 4, date: "2026-10-02", type: "watch", people: ["吴自国"], post: "配酸" },
- { id: 5, date: "2026-10-03", type: "am", people: ["刘茂生", "吴自国"], post: "配酸" },
- { id: 6, date: "2026-10-03", type: "watch", people: ["吴自泰"], post: "配酸" },
- { id: 7, date: "2026-10-04", type: "am", people: ["黄天宝", "吴自泰"], post: "配酸" },
- { id: 8, date: "2026-10-04", type: "watch", people: ["吴自国"], post: "配酸" },
- { id: 9, date: "2026-10-05", type: "am", people: ["吴自泰", "刘茂生"], post: "配酸" },
- { id: 10, date: "2026-10-06", type: "am", people: ["黄天宝", "刘茂生"], post: "配酸" },
- { id: 11, date: "2026-10-06", type: "watch", people: ["吴自国"], post: "配酸" },
- { id: 12, date: "2026-10-07", type: "am", people: ["刘茂生", "吴自泰"], post: "配酸" },
- { id: 13, date: "2026-10-07", type: "watch", people: ["吴自国"], post: "配酸" },
- { id: 14, date: "2026-10-11", type: "watch", people: ["吴良芹"], post: "" },
- { id: 15, date: "2026-10-18", type: "watch", people: ["汪璇"], post: "" },
- { id: 16, date: "2026-10-25", type: "watch", people: ["刘丹"], post: "" }
- ];
- const dayShift = (d, n) => {
- const x = new Date(d + "T00:00:00");
- x.setDate(x.getDate() + n);
- return x.toISOString().slice(0, 10);
- };
- const quoteMoney = (n) => "¥" + Number(n || 0).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
- let quoteTab = "全部";
- let quoteStatusFilter = "";
- let quotePage = 1;
- let quotePageSize = 20;
- let quoteSeq = 12;
- let selectedQuoteIds = new Set();
- let expandedQuoteIds = new Set();
- let quotes = [
- { id: 1, no: "SXDD-20260930002", title: "山峡-安庆汇辰 (SXDD-20260930002)", kind: "销售", customer: "安庆汇辰科技有限公司", product: "一水柠檬酸 99.50% 0.125 吨", prodAmt: 1375, freight: 0, date: "2026-09-30", status: "已生效" },
- { id: 2, no: "SXDD-20260930001", title: "山峡-安徽金兽 (SXDD-20260930001)", kind: "销售", customer: "安徽金兽医药科技有限公司", product: "片碱 2 吨", prodAmt: 7400, freight: 0, date: "2026-09-30", status: "已生效" },
- { id: 3, no: "SXDD-20260929004", title: "山峡-厦门圣益 (SXDD-20260929004)", kind: "销售", customer: "厦门圣益石化有限公司", product: "30%液碱 30% 65 吨", prodAmt: 46800, freight: 0, date: "2026-09-29", status: "已生效" },
- { id: 4, no: "SXDD-20260929003", title: "山峡-安徽中禹 (SXDD-20260929003)", kind: "销售", customer: "安徽省中禹商贸有限公司", product: "30% 工业硫酸 30% 21 吨", prodAmt: 25200, freight: 0, date: "2026-09-29", status: "已生效" },
- { 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 },
- { id: 6, no: "SXDD-20260929001", title: "山峡-安徽金兽 (SXDD-20260929001)", kind: "销售", customer: "安徽金兽医药科技有限公司", product: "片碱 2 吨", prodAmt: 7400, freight: 0, date: "2026-09-29", status: "已生效" },
- { id: 7, no: "SXDD-20260928001", title: "山峡-安徽紫荆花 (SXDD-20260928001)", kind: "销售", customer: "安徽紫荆花新型材料科技有限公司", product: "聚合氯化铝 5 吨", prodAmt: 10000, freight: 0, date: "2026-09-28", status: "已生效" },
- { id: 8, no: "SXDD-20260924003", title: "山峡-桐城金涛 (SXDD-20260924003)", kind: "销售", customer: "桐城市金涛表面处理有限公司", product: "次氯酸钠 13% 30 吨", prodAmt: 20400, freight: 0, date: "2026-09-24", status: "已生效" },
- { id: 9, no: "SXDD-20260924002", title: "山峡-安庆飞霖 (SXDD-20260924002)", kind: "销售", customer: "安庆飞霖新材料有限公司", product: "32%液碱 32% 30 吨", prodAmt: 25200, freight: 0, date: "2026-09-24", status: "已生效" },
- { id: 10, no: "SXDD-20260924001", title: "山峡-安庆百谊生物 (SXDD-20260924001)", kind: "销售", customer: "安庆百谊生物科技有限公司", product: "工业盐酸 31% 0.025 吨", prodAmt: 50, freight: 0, date: "2026-09-24", status: "已生效" },
- { id: 11, no: "SXDD-20260923001", title: "山峡-三土 (SXDD-20260923001)", kind: "销售", customer: "三土新材料科技(上海)有限公司", product: "氯化钠 国标 0.65 吨", prodAmt: 910, freight: 0, date: "2026-09-23", status: "已生效" },
- { id: 12, no: "SXDD-20261001001", title: "山峡-宜昌安达 (SXDD-20261001001)", kind: "销售", customer: "宜昌安达危运有限公司", product: "98% 工业硫酸 32 吨", prodAmt: 28800, freight: 0, date: "2026-10-01", status: "草稿" },
- { id: 13, no: "SXDD-20261002001", title: "山峡-上饶敏达 (SXDD-20261002001)", kind: "销售", customer: "上饶市敏达精细化工有限公司", product: "硫酸 98% 30 吨", prodAmt: 27000, freight: 0, date: "2026-10-02", status: "草稿" }
- ];
- function quoteWeekRange() {
- const d = new Date(today + "T00:00:00");
- const day = d.getDay() || 7;
- const start = dayShift(today, 1 - day);
- const end = dayShift(start, 6);
- return [start, end];
- }
- function filteredQuotes() {
- const q = (document.getElementById("quoteQ")?.value || "").trim();
- let rows = quotes.filter((x) => x.status !== "已作废" || quoteTab === "全部");
- if (quoteTab === "昨日") rows = rows.filter((x) => x.date === dayShift(today, -1));
- else if (quoteTab === "今日") rows = rows.filter((x) => x.date === today);
- else if (quoteTab === "明日") rows = rows.filter((x) => x.date === dayShift(today, 1));
- else if (quoteTab === "本周") {
- const [a, b] = quoteWeekRange();
- rows = rows.filter((x) => x.date >= a && x.date <= b);
- }
- if (quoteStatusFilter === "待审核") rows = rows.filter((x) => x.status === "待审批");
- else if (quoteStatusFilter) rows = rows.filter((x) => x.status === quoteStatusFilter);
- if (q) rows = rows.filter((x) => [x.no, x.title, x.customer, x.product].join(" ").includes(q));
- return rows;
- }
- function toggleQuote(id, on) {
- if (on) selectedQuoteIds.add(id); else selectedQuoteIds.delete(id);
- renderQuotes();
- }
- function toggleQuoteAll(on) {
- filteredQuotes().forEach((x) => on ? selectedQuoteIds.add(x.id) : selectedQuoteIds.delete(x.id));
- renderQuotes();
- }
- function toggleQuoteExpand(id) {
- if (expandedQuoteIds.has(id)) expandedQuoteIds.delete(id); else expandedQuoteIds.add(id);
- renderQuotes();
- }
- function selectedQuotes() {
- return quotes.filter((x) => selectedQuoteIds.has(x.id));
- }
- function setQuoteStatus(st) {
- quoteStatusFilter = quoteStatusFilter === st ? "" : st;
- quotePage = 1;
- renderQuotes();
- }
- function waitReviewBanner(n, on, onclick, goText) {
- const empty = !n;
- return `<button class="wait-banner ${on ? "on" : ""} ${empty ? "empty" : ""}" type="button" onclick="${onclick}">
- <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>
- <span class="wb-n">${n}</span>
- <span class="wb-k">待审核</span>
- <span class="wb-go">${empty ? "暂无待审" : goText}</span>
- </button>`;
- }
- function renderQuotes() {
- const rows = filteredQuotes();
- const pages = Math.max(1, Math.ceil(rows.length / quotePageSize));
- if (quotePage > pages) quotePage = pages;
- const start = (quotePage - 1) * quotePageSize;
- const pageRows = rows.slice(start, start + quotePageSize);
- const allOn = pageRows.length && pageRows.every((x) => selectedQuoteIds.has(x.id));
- document.getElementById("quoteSelHint").textContent = `已选 ${selectedQuoteIds.size} 项`;
- const waitN = quotes.filter((x) => x.status === "待审批").length;
- const qStats = document.getElementById("quoteStats");
- if (qStats) {
- if (quoteStatusFilter === "草稿" || quoteStatusFilter === "已生效") quoteStatusFilter = "";
- qStats.innerHTML = waitReviewBanner(waitN, quoteStatusFilter === "待审核", "setQuoteStatus('待审核')", "查看待审报价");
- }
- const qBadge = document.getElementById("quoteBadge");
- if (qBadge) {
- const n = quotes.filter((x) => x.status === "待审批" || x.status === "草稿").length;
- qBadge.textContent = n;
- qBadge.style.display = n ? "" : "none";
- }
- document.getElementById("quoteTable").innerHTML = pageRows.length ? `<table class="erp-table quote-table">
- <thead><tr>
- <th class="quote-expand"></th>
- <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="toggleQuoteAll(this.checked)" /></th>
- <th class="col-no"><span class="th-sort">报价单号</span></th>
- <th class="col-title"><span class="th-sort">报价主题</span></th>
- <th><span class="th-sort">报价类型</span></th>
- <th><span class="th-sort">客户名称</span></th>
- <th><span class="th-sort">产品信息</span></th>
- <th><span class="th-sort">最终金额</span></th>
- <th><span class="th-sort">产品总金额</span></th>
- <th><span class="th-sort">运费</span></th>
- </tr></thead>
- <tbody>${pageRows.map((x) => {
- const open = expandedQuoteIds.has(x.id);
- const finalAmt = x.prodAmt + (x.freight || 0);
- const title = String((x.title || "").replace(/\s*\([^)]*\)\s*$/, "") || x.title).replace(/"/g, """);
- return `<tr class="${x.alert || x.status === "待审批" ? "row-alert" : ""}">
- <td class="quote-expand ${open ? "on" : ""}" onclick="toggleQuoteExpand(${x.id})">${open ? "▾" : "▸"}</td>
- <td class="chk"><input type="checkbox" ${selectedQuoteIds.has(x.id) ? "checked" : ""} onchange="toggleQuote(${x.id}, this.checked)" /></td>
- <td class="cell-no col-no"><button class="quote-link" type="button" onclick="toggleQuoteExpand(${x.id})">${x.no}</button></td>
- <td class="col-title" title="${title}"><button class="quote-link" type="button" onclick="toggleQuoteExpand(${x.id})">${title}</button></td>
- <td><span class="sale-tag">${x.kind}</span></td>
- <td>${x.customer}</td>
- <td>${x.product}</td>
- <td class="amt-red">${quoteMoney(finalAmt)}</td>
- <td class="amt-red">${quoteMoney(x.prodAmt)}</td>
- <td class="amt-red">${quoteMoney(x.freight)}</td>
- </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>` : ""}`;
- }).join("")}</tbody></table>` : `<div class="empty">暂无报价单</div>`;
- const nums = [];
- const maxBtn = Math.min(pages, 6);
- for (let i = 1; i <= maxBtn; i++) nums.push(i);
- document.getElementById("quotePager").innerHTML = `
- <span class="muted">共 ${rows.length} 条</span>
- <select class="page-size" onchange="quotePageSize=+this.value;quotePage=1;renderQuotes()">
- <option value="20" ${quotePageSize === 20 ? "selected" : ""}>20条/页</option>
- <option value="50" ${quotePageSize === 50 ? "selected" : ""}>50条/页</option>
- </select>
- ${nums.map((n) => `<button type="button" class="${n === quotePage ? "on" : ""}" onclick="quotePage=${n};renderQuotes()">${n}</button>`).join("")}
- ${pages > 6 ? `<span class="muted">… ${pages}</span>` : ""}
- <span class="muted">前往</span>
- <input class="quote-goto" type="number" min="1" max="${pages}" value="${quotePage}" onchange="quotePage=Math.min(${pages}, Math.max(1, +this.value||1));renderQuotes()" />
- <span class="muted">页</span>`;
- }
- function openQuoteDlg(copyFrom) {
- const f = document.getElementById("quoteForm");
- f.reset();
- f.date.value = today;
- document.getElementById("quoteDlgTitle").textContent = copyFrom ? "复制新增报价单" : "新增报价单";
- if (copyFrom) {
- f.customer.value = copyFrom.customer;
- f.title.value = copyFrom.title.replace(/\(SXDD-[^)]+\)/, "").trim();
- f.kind.value = copyFrom.kind;
- f.product.value = copyFrom.product;
- f.prodAmt.value = copyFrom.prodAmt;
- f.freight.value = copyFrom.freight || 0;
- }
- document.getElementById("quoteDialog").showModal();
- }
- function saveQuote() {
- const f = document.getElementById("quoteForm");
- if (!f.reportValidity()) return;
- quoteSeq += 1;
- const no = "SXDD-" + today.replaceAll("-", "") + String(quoteSeq).padStart(3, "0");
- const titleBase = f.title.value.trim();
- quotes.unshift({
- id: Date.now(), no,
- title: `${titleBase} (${no})`,
- kind: f.kind.value, customer: f.customer.value.trim(),
- product: f.product.value.trim(), prodAmt: +f.prodAmt.value, freight: +f.freight.value || 0,
- date: f.date.value, status: "草稿"
- });
- document.getElementById("quoteDialog").close();
- toast("已保存草稿 " + no);
- renderQuotes();
- }
- function copyQuote() {
- const rows = selectedQuotes();
- if (rows.length !== 1) { toast("请勾选一张报价单后再复制新增"); return; }
- openQuoteDlg(rows[0]);
- }
- function submitQuotes() {
- const rows = selectedQuotes().filter((x) => x.status === "草稿");
- if (!rows.length) { toast("请勾选草稿状态的报价单"); return; }
- rows.forEach((x) => { x.status = "待审批"; x.alert = true; });
- toast(`已提交 ${rows.length} 张报价单审批`);
- renderQuotes();
- }
- function genQuotePdf() {
- const rows = selectedQuotes();
- if (!rows.length) { toast("请先勾选报价单"); return; }
- toast("演示:已生成 " + rows.map((x) => x.no).join("、"));
- }
- function voidQuotes() {
- const rows = selectedQuotes();
- if (!rows.length) { toast("请先勾选报价单"); return; }
- rows.forEach((x) => { x.status = "已作废"; x.alert = false; });
- selectedQuoteIds.clear();
- toast("已作废所选报价单");
- renderQuotes();
- }
- let ctTab = "全部";
- let ctStatusFilter = "";
- let ctPage = 1;
- let ctPageSize = 20;
- let ctSeq = 12;
- let selectedCtIds = new Set();
- let expandedCtIds = new Set();
- let scanCtIds = [];
- let contracts = [
- { 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 },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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 },
- { 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" },
- { 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" }
- ];
- function ctDateOf(x) { return (x.signDate || x.created || "").slice(0, 10); }
- function filteredContracts() {
- const q = (document.getElementById("ctQ")?.value || "").trim();
- let rows = contracts.filter((x) => x.status !== "已作废" || ctTab === "全部");
- if (ctTab === "昨日") rows = rows.filter((x) => ctDateOf(x) === dayShift(today, -1));
- else if (ctTab === "今日") rows = rows.filter((x) => ctDateOf(x) === today);
- else if (ctTab === "明日") rows = rows.filter((x) => ctDateOf(x) === dayShift(today, 1));
- else if (ctTab === "本周") {
- const [a, b] = quoteWeekRange();
- rows = rows.filter((x) => { const d = ctDateOf(x); return d >= a && d <= b; });
- }
- if (ctStatusFilter === "待审核") rows = rows.filter((x) => x.status === "待审批");
- if (q) rows = rows.filter((x) => [x.no, x.title, x.customer, x.product].join(" ").includes(q));
- return rows;
- }
- function toggleCt(id, on) {
- if (on) selectedCtIds.add(id); else selectedCtIds.delete(id);
- renderContracts();
- }
- function toggleCtAll(on) {
- filteredContracts().forEach((x) => on ? selectedCtIds.add(x.id) : selectedCtIds.delete(x.id));
- renderContracts();
- }
- function toggleCtExpand(id) {
- if (expandedCtIds.has(id)) expandedCtIds.delete(id); else expandedCtIds.add(id);
- renderContracts();
- }
- function selectedContracts() { return contracts.filter((x) => selectedCtIds.has(x.id)); }
- function setCtStatus(st) {
- ctStatusFilter = ctStatusFilter === st ? "" : st;
- ctPage = 1;
- renderContracts();
- }
- function ctAttHtml(x) {
- const pdf = x.att ? `<span class="pdf-chip"><span class="pdf-box">PDF</span><em>${x.att}附件</em></span>` : "";
- 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>`;
- }
- function renderContracts() {
- const rows = filteredContracts();
- const pages = Math.max(1, Math.ceil(rows.length / ctPageSize));
- if (ctPage > pages) ctPage = pages;
- const start = (ctPage - 1) * ctPageSize;
- const pageRows = rows.slice(start, start + ctPageSize);
- const allOn = pageRows.length && pageRows.every((x) => selectedCtIds.has(x.id));
- document.getElementById("ctSelHint").textContent = `已选 ${selectedCtIds.size} 项`;
- const waitN = contracts.filter((x) => x.status === "待审批").length;
- const ctStats = document.getElementById("ctStats");
- if (ctStats) {
- if (ctStatusFilter === "已生效" || ctStatusFilter === "本月") ctStatusFilter = "";
- ctStats.innerHTML = waitReviewBanner(waitN, ctStatusFilter === "待审核", "setCtStatus('待审核')", "查看待审合同");
- }
- const badge = document.getElementById("ctBadge");
- if (badge) {
- const n = contracts.filter((x) => x.status === "待审批" || x.status === "草稿").length;
- badge.textContent = n;
- badge.style.display = n ? "" : "none";
- }
- document.getElementById("ctTable").innerHTML = pageRows.length ? `<table class="erp-table ct-table">
- <thead><tr>
- <th class="quote-expand"></th>
- <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="toggleCtAll(this.checked)" /></th>
- <th class="col-no"><span class="th-sort">合同编号</span></th>
- <th><span class="th-sort">合同名称</span></th>
- <th><span class="th-sort">客户名称</span></th>
- <th><span class="th-sort">合同类型</span></th>
- <th><span class="th-sort">合同附件</span></th>
- <th><span class="th-sort">产品信息</span></th>
- <th><span class="th-sort">合同总金额 (元)</span></th>
- <th><span class="th-sort">合同金额</span></th>
- <th><span class="th-sort">运费</span></th>
- <th><span class="th-sort">桶押金</span></th>
- <th><span class="th-sort">合同状态</span></th>
- <th><span class="th-sort">签订日期</span></th>
- <th><span class="th-sort">负责人</span></th>
- <th><span class="th-sort">创建时间</span></th>
- </tr></thead>
- <tbody>${pageRows.map((x) => {
- const open = expandedCtIds.has(x.id);
- const total = Number(x.amount || 0) + Number(x.freight || 0) + Number(x.deposit || 0);
- const alert = x.alert || x.status === "待审批";
- return `<tr class="${alert ? "row-alert" : ""}">
- <td class="quote-expand ${open ? "on" : ""}" onclick="toggleCtExpand(${x.id})">${open ? "▾" : "▸"}</td>
- <td class="chk"><input type="checkbox" ${selectedCtIds.has(x.id) ? "checked" : ""} onchange="toggleCt(${x.id}, this.checked)" /></td>
- <td class="cell-no col-no"><button class="quote-link" type="button" onclick="toggleCtExpand(${x.id})">${x.no}</button></td>
- <td class="ct-name"><button class="quote-link" type="button" onclick="toggleCtExpand(${x.id})">${x.title}<span class="sub">(${x.no})</span></button></td>
- <td class="ct-cust">${x.customer}</td>
- <td>${x.kind}</td>
- <td>${ctAttHtml(x)}</td>
- <td class="ct-prod">${x.product}</td>
- <td class="amt-red">${quoteMoney(total)}</td>
- <td class="amt-red">${quoteMoney(x.amount)}</td>
- <td class="amt-red">${quoteMoney(x.freight)}</td>
- <td class="amt-red">${quoteMoney(x.deposit)}</td>
- <td class="ct-st">${x.status}</td>
- <td>${x.signDate}</td>
- <td>${x.owner}</td>
- <td>${x.created}</td>
- </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>` : ""}`;
- }).join("")}</tbody></table>` : `<div class="empty">暂无合同</div>`;
- const nums = [];
- const maxBtn = Math.min(pages, 6);
- for (let i = 1; i <= maxBtn; i++) nums.push(i);
- document.getElementById("ctPager").innerHTML = `
- <span class="muted">共 ${rows.length} 条</span>
- <select class="page-size" onchange="ctPageSize=+this.value;ctPage=1;renderContracts()">
- <option value="20" ${ctPageSize === 20 ? "selected" : ""}>20条/页</option>
- <option value="50" ${ctPageSize === 50 ? "selected" : ""}>50条/页</option>
- </select>
- ${nums.map((n) => `<button type="button" class="${n === ctPage ? "on" : ""}" onclick="ctPage=${n};renderContracts()">${n}</button>`).join("")}
- ${pages > 6 ? `<span class="muted">… ${pages}</span>` : ""}
- <span class="muted">前往</span>
- <input class="quote-goto" type="number" min="1" max="${pages}" value="${ctPage}" onchange="ctPage=Math.min(${pages}, Math.max(1, +this.value||1));renderContracts()" />
- <span class="muted">页</span>`;
- }
- function openCtDlg(copyFrom) {
- const f = document.getElementById("ctForm");
- f.reset();
- f.signDate.value = today;
- f.owner.value = "万金强";
- document.getElementById("ctDlgTitle").textContent = copyFrom ? "复制新增合同" : "新增合同";
- if (copyFrom) {
- f.customer.value = copyFrom.customer;
- f.title.value = copyFrom.title;
- f.kind.value = copyFrom.kind;
- f.product.value = String(copyFrom.product).replace(/<br>/g, " / ");
- f.amount.value = copyFrom.amount;
- f.freight.value = copyFrom.freight || 0;
- f.deposit.value = copyFrom.deposit || 0;
- f.owner.value = copyFrom.owner || "万金强";
- }
- document.getElementById("ctDialog").showModal();
- }
- function saveContract() {
- const f = document.getElementById("ctForm");
- if (!f.reportValidity()) return;
- ctSeq += 1;
- const no = "SX/X-JT" + today.replaceAll("-", "") + String(ctSeq).padStart(3, "0");
- const now = today + " " + new Date().toTimeString().slice(0, 8);
- contracts.unshift({
- id: Date.now(), no, title: f.title.value.trim(), customer: f.customer.value.trim(),
- kind: f.kind.value, att: "", product: f.product.value.trim(), amount: +f.amount.value,
- freight: +f.freight.value || 0, deposit: +f.deposit.value || 0, status: "草稿",
- signDate: f.signDate.value, owner: f.owner.value.trim() || "万金强", created: now
- });
- document.getElementById("ctDialog").close();
- toast("已保存草稿 " + no);
- renderContracts();
- }
- function copyContract() {
- const rows = selectedContracts();
- if (rows.length !== 1) { toast("请勾选一份合同后再复制"); return; }
- openCtDlg(rows[0]);
- }
- function submitContracts() {
- const rows = selectedContracts().filter((x) => x.status === "草稿");
- if (!rows.length) { toast("请勾选草稿状态的合同"); return; }
- rows.forEach((x) => { x.status = "待审批"; x.alert = true; });
- toast(`已提交 ${rows.length} 份合同审批`);
- renderContracts();
- }
- function toggleCtExport(e) {
- e.stopPropagation();
- document.getElementById("ctExportDrop").classList.toggle("open");
- }
- function exportContracts(scope) {
- document.getElementById("ctExportDrop").classList.remove("open");
- const n = scope === "all" ? filteredContracts().length : Math.min(ctPageSize, filteredContracts().length);
- toast(`演示:已导出 ${n} 条合同`);
- }
- function openCtScan(id) {
- scanCtIds = id ? [id] : selectedCtIds.size ? [...selectedCtIds] : [];
- if (!scanCtIds.length) { toast("请先勾选合同,或直接点行内上传"); return; }
- const f = document.getElementById("ctScanForm");
- f.reset();
- const names = contracts.filter((x) => scanCtIds.includes(x.id)).map((x) => x.no).join("、");
- document.getElementById("ctScanHint").textContent = "将为 " + names + " 上传双章 / 单章扫描件。";
- document.getElementById("ctScanDialog").showModal();
- }
- function saveCtScan() {
- const f = document.getElementById("ctScanForm");
- if (!f.reportValidity()) return;
- const att = f.att.value;
- scanCtIds.forEach((id) => {
- const x = contracts.find((c) => c.id === id);
- if (x) x.att = att;
- });
- document.getElementById("ctScanDialog").close();
- toast("已上传" + att + "扫描件");
- renderContracts();
- }
- const vehicles = [
- { plate: "皖H57837", trailer: "皖HE155", type: "柴油罐车", media: "柴油", driver: "周建军", escort: "李强", km: 188900, status: "运行", tankDue: "2026-10-12", insDue: "2026-12-01", nextMaint: 190000 },
- { plate: "皖HA3690", trailer: "皖HA368", type: "甲醇罐车", media: "甲醇", driver: "陈伟", escort: "王磊", km: 145200, status: "维保中", tankDue: "2027-03-08", insDue: "2026-10-20", nextMaint: 146000 },
- { plate: "皖LB1283", trailer: "皖LB129", type: "液氨罐车", media: "液氨", driver: "胡明", escort: "赵鹏", km: 100480, status: "运行", tankDue: "2026-11-02", insDue: "2027-01-15", nextMaint: 104000 },
- { plate: "皖K92881", trailer: "皖K929", type: "柴油罐车", media: "柴油", driver: "吴涛", escort: "孙斌", km: 213100, status: "运行", tankDue: "2027-06-18", insDue: "2026-09-30", nextMaint: 215000 },
- { plate: "皖H61208", trailer: "皖HE208", type: "硫酸罐车", media: "硫酸", driver: "郑浩", escort: "刘洋", km: 66180, status: "停运", tankDue: "2026-10-05", insDue: "2026-11-11", nextMaint: 68000 },
- { plate: "皖K73155", trailer: "皖KE155", type: "盐酸罐车", media: "盐酸", driver: "李强", escort: "周建军", km: 121050, status: "运行", tankDue: "2027-02-14", insDue: "2026-12-18", nextMaint: 124000 },
- { plate: "皖L88201", trailer: "皖LE201", type: "液碱罐车", media: "液碱", driver: "王磊", escort: "陈伟", km: 159400, status: "运行", tankDue: "2027-04-22", insDue: "2027-02-08", nextMaint: 162000 },
- { plate: "皖H90366", trailer: "皖HE366", type: "柴油罐车", media: "柴油", driver: "孙斌", escort: "吴涛", km: 91880, status: "运行", tankDue: "2026-12-30", insDue: "2026-11-20", nextMaint: 94000 }
- ];
- function hazPlate(v) {
- if (!v) return "—";
- const plate = typeof v === "string" ? v : v.plate;
- const row = typeof v === "string" ? vehicles.find((x) => x.plate === v) : v;
- const trailer = row && row.trailer ? `${row.trailer}挂` : "";
- return `危${plate}${trailer}`;
- }
- let fuels = [
- { plate: "皖H57837", date: "2026-09-21", station: "中石化猇亭站", oil: "0# 柴油", liters: 300, amount: 2220, km: 185210, note: "", attachments: [{ name: "猇亭站小票_0921.jpg" }] },
- { plate: "皖H57837", date: "2026-09-28", station: "中石化猇亭站", oil: "0# 柴油", liters: 320, amount: 2368, km: 186420, note: "", attachments: [{ name: "猇亭站小票_0928.jpg" }] },
- { plate: "皖HA3690", date: "2026-09-10", station: "公司内部油库", oil: "0# 柴油", liters: 350, amount: 2520, km: 141500, note: "", attachments: [{ name: "内库出库单_0910.jpg" }] },
- { plate: "皖HA3690", date: "2026-09-22", station: "中石化夷陵站", oil: "0# 柴油", liters: 340, amount: 2516, km: 142200, note: "", attachments: [{ name: "夷陵站小票_0922.jpg" }] },
- { plate: "皖LB1283", date: "2026-09-20", station: "中石油伍家岗", oil: "0# 柴油", liters: 260, amount: 1924, km: 96800, note: "", attachments: [{ name: "伍家岗小票_0920.jpg" }] },
- { plate: "皖LB1283", date: "2026-09-27", station: "中石油伍家岗", oil: "0# 柴油", liters: 280, amount: 2072, km: 97850, note: "", attachments: [{ name: "伍家岗发票_0927.pdf" }] },
- { plate: "皖K92881", date: "2026-09-18", station: "中石化夷陵站", oil: "0# 柴油", liters: 380, amount: 2736, km: 208900, note: "", attachments: [{ name: "夷陵站小票_0918.jpg" }] },
- { plate: "皖K92881", date: "2026-09-26", station: "公司内部油库", oil: "0# 柴油", liters: 410, amount: 2952, km: 210110, note: "夜班补油", attachments: [{ name: "内库出库单_0926.jpg" }] },
- { plate: "皖H61208", date: "2026-09-16", station: "中石化猇亭站", oil: "0# 柴油", liters: 220, amount: 1628, km: 64200, note: "", attachments: [{ name: "猇亭站小票_0916.jpg" }] },
- { plate: "皖K73155", date: "2026-09-19", station: "中石油伍家岗", oil: "0# 柴油", liters: 240, amount: 1776, km: 116800, note: "", attachments: [{ name: "伍家岗小票_0919.jpg" }] },
- { plate: "皖L88201", date: "2026-09-24", station: "中石化夷陵站", oil: "0# 柴油", liters: 280, amount: 2072, km: 154100, note: "", attachments: [{ name: "夷陵站小票_0924.jpg" }] },
- { plate: "皖H90366", date: "2026-09-25", station: "公司内部油库", oil: "0# 柴油", liters: 250, amount: 1850, km: 88200, note: "", attachments: [{ name: "内库出库单_0925.jpg" }] },
- { plate: "皖H57837", date: "2026-10-01", station: "中石化猇亭站", oil: "0# 柴油", liters: 310, amount: 2294, km: 186420, note: "", attachments: [{ name: "猇亭站小票_1001.jpg" }] },
- { plate: "皖H57837", date: "2026-10-08", station: "中石化猇亭站", oil: "0# 柴油", liters: 360, amount: 2664, km: 188900, note: "", attachments: [{ name: "猇亭站小票_1008.jpg" }] },
- { plate: "皖HA3690", date: "2026-10-01", station: "中石化夷陵站", oil: "0# 柴油", liters: 280, amount: 2072, km: 142880, note: "", attachments: [{ name: "夷陵站小票_1001.jpg" }] },
- { plate: "皖HA3690", date: "2026-10-07", station: "中石化夷陵站", oil: "0# 柴油", liters: 320, amount: 2368, km: 145200, note: "", attachments: [{ name: "夷陵站小票_1007.jpg" }] },
- { plate: "皖LB1283", date: "2026-10-01", station: "中石油伍家岗", oil: "0# 柴油", liters: 250, amount: 1850, km: 97850, note: "", attachments: [{ name: "伍家岗小票_1001.jpg" }] },
- { plate: "皖LB1283", date: "2026-10-08", station: "中石油伍家岗", oil: "0# 柴油", liters: 290, amount: 2146, km: 100480, note: "", attachments: [{ name: "伍家岗小票_1008.jpg" }] },
- { plate: "皖K92881", date: "2026-10-01", station: "公司内部油库", oil: "0# 柴油", liters: 360, amount: 2664, km: 210330, note: "", attachments: [{ name: "内库出库单_1001.jpg" }] },
- { plate: "皖K92881", date: "2026-10-09", station: "公司内部油库", oil: "0# 柴油", liters: 420, amount: 3108, km: 213100, note: "", attachments: [{ name: "内库出库单_1009.jpg" }] },
- { plate: "皖H61208", date: "2026-10-01", station: "中石化猇亭站", oil: "0# 柴油", liters: 180, amount: 1332, km: 65420, note: "停运前补油", attachments: [{ name: "猇亭站小票_1001b.jpg" }] },
- { plate: "皖H61208", date: "2026-10-03", station: "中石化猇亭站", oil: "0# 柴油", liters: 90, amount: 666, km: 66180, note: "", attachments: [{ name: "猇亭站小票_1003.jpg" }] },
- { plate: "皖K73155", date: "2026-10-01", station: "中石油伍家岗", oil: "0# 柴油", liters: 220, amount: 1628, km: 118600, note: "", attachments: [{ name: "伍家岗小票_1001b.jpg" }] },
- { plate: "皖K73155", date: "2026-10-08", station: "中石油伍家岗", oil: "0# 柴油", liters: 260, amount: 1924, km: 121050, note: "", attachments: [{ name: "伍家岗小票_1008b.jpg" }] },
- { plate: "皖L88201", date: "2026-10-01", station: "中石化夷陵站", oil: "0# 柴油", liters: 300, amount: 2220, km: 156200, note: "", attachments: [{ name: "夷陵站小票_1001b.jpg" }] },
- { plate: "皖L88201", date: "2026-10-08", station: "中石化夷陵站", oil: "0# 柴油", liters: 330, amount: 2442, km: 159400, note: "", attachments: [{ name: "夷陵站小票_1008.jpg" }] },
- { plate: "皖H90366", date: "2026-10-01", station: "公司内部油库", oil: "0# 柴油", liters: 260, amount: 1924, km: 89400, note: "", attachments: [{ name: "内库出库单_1001b.jpg" }] },
- { plate: "皖H90366", date: "2026-10-08", station: "公司内部油库", oil: "0# 柴油", liters: 280, amount: 2072, km: 91880, note: "", attachments: [{ name: "内库出库单_1008.jpg" }] }
- ];
- let etcs = [
- { applicant: "周建军", plate: "皖H57837", type: "ETC", date: "2026-09-12", km: 185210, amount: 600, used: 480, note: "新车上牌后开通", status: "已提交", attachments: [{ name: "ETC开通凭证_0912.jpg" }] },
- { applicant: "吴九华", plate: "皖HA3690", type: "ETC", date: "2026-09-25", km: 142880, amount: 800, used: 650, note: "", status: "已提交", attachments: [{ name: "ETC开通凭证.jpg" }] },
- { applicant: "胡明", plate: "皖LB1283", type: "ETC", date: "2026-09-18", km: 96800, amount: 500, used: 420, note: "", status: "已提交", attachments: [{ name: "ETC充值_0918.jpg" }] },
- { applicant: "吴涛", plate: "皖K92881", type: "ETC", date: "2026-09-08", km: 208900, amount: 400, used: 300, note: "", status: "已提交", attachments: [{ name: "ETC充值_0908.jpg" }] },
- { applicant: "吴涛", plate: "皖K92881", type: "ETC", date: "2026-09-26", km: 210110, amount: 500, used: 380, note: "", status: "已提交", attachments: [{ name: "ETC充值_0926.jpg" }] },
- { applicant: "郑浩", plate: "皖H61208", type: "ETC", date: "2026-09-16", km: 64200, amount: 300, used: 180, note: "", status: "已提交", attachments: [{ name: "ETC充值_0916.jpg" }] },
- { applicant: "李强", plate: "皖K73155", type: "ETC", date: "2026-09-19", km: 116800, amount: 450, used: 360, note: "", status: "已提交", attachments: [{ name: "ETC充值_0919.jpg" }] },
- { applicant: "王磊", plate: "皖L88201", type: "ETC", date: "2026-09-24", km: 154100, amount: 700, used: 520, note: "", status: "已提交", attachments: [{ name: "ETC充值_0924.jpg" }] },
- { applicant: "孙斌", plate: "皖H90366", type: "ETC", date: "2026-09-25", km: 88200, amount: 400, used: 290, note: "", status: "已提交", attachments: [{ name: "ETC充值_0925.jpg" }] },
- { applicant: "周建军", plate: "皖H57837", type: "ETC", date: "2026-10-01", km: 186420, amount: 800, used: 620, note: "", status: "已提交", attachments: [{ name: "ETC充值_1001.jpg" }] },
- { applicant: "周建军", plate: "皖H57837", type: "ETC", date: "2026-10-08", km: 188900, amount: 500, used: 410, note: "", status: "已提交", attachments: [{ name: "ETC充值_1008.jpg" }] },
- { applicant: "陈伟", plate: "皖HA3690", type: "ETC", date: "2026-10-07", km: 145200, amount: 1000, used: 780, note: "", status: "已提交", attachments: [{ name: "ETC充值_1007.jpg" }] },
- { applicant: "胡明", plate: "皖LB1283", type: "ETC", date: "2026-10-01", km: 97850, amount: 600, used: 480, note: "", status: "已提交", attachments: [{ name: "ETC充值_1001b.jpg" }] },
- { applicant: "胡明", plate: "皖LB1283", type: "ETC", date: "2026-10-08", km: 100480, amount: 400, used: 310, note: "", status: "已提交", attachments: [{ name: "ETC充值_1008b.jpg" }] },
- { applicant: "吴涛", plate: "皖K92881", type: "ETC", date: "2026-10-01", km: 210330, amount: 700, used: 540, note: "", status: "已提交", attachments: [{ name: "ETC充值_1001c.jpg" }] },
- { applicant: "吴涛", plate: "皖K92881", type: "ETC", date: "2026-10-09", km: 213100, amount: 650, used: 500, note: "", status: "已提交", attachments: [{ name: "ETC充值_1009.jpg" }] },
- { applicant: "郑浩", plate: "皖H61208", type: "ETC", date: "2026-10-03", km: 66180, amount: 200, used: 90, note: "停运前充值", status: "已提交", attachments: [{ name: "ETC充值_1003.jpg" }] },
- { applicant: "李强", plate: "皖K73155", type: "ETC", date: "2026-10-01", km: 118600, amount: 500, used: 390, note: "", status: "已提交", attachments: [{ name: "ETC充值_1001d.jpg" }] },
- { applicant: "李强", plate: "皖K73155", type: "ETC", date: "2026-10-08", km: 121050, amount: 480, used: 360, note: "", status: "已提交", attachments: [{ name: "ETC充值_1008c.jpg" }] },
- { applicant: "王磊", plate: "皖L88201", type: "ETC", date: "2026-10-01", km: 156200, amount: 750, used: 580, note: "", status: "已提交", attachments: [{ name: "ETC充值_1001e.jpg" }] },
- { applicant: "王磊", plate: "皖L88201", type: "ETC", date: "2026-10-08", km: 159400, amount: 600, used: 450, note: "", status: "已提交", attachments: [{ name: "ETC充值_1008d.jpg" }] },
- { applicant: "孙斌", plate: "皖H90366", type: "ETC", date: "2026-10-08", km: 91880, amount: 550, used: 410, note: "", status: "已提交", attachments: [{ name: "ETC充值_1008e.jpg" }] }
- ];
- let oilcards = [
- { applicant: "周建军", plate: "皖H57837", type: "油卡", date: "2026-09-12", km: 185210, amount: 2000, used: 1680, note: "月初充值", status: "已提交", attachments: [{ name: "油卡充值_0912.jpg" }] },
- { applicant: "吴九华", plate: "皖HA3690", type: "油卡", date: "2026-09-25", km: 142880, amount: 2500, used: 2100, note: "", status: "已提交", attachments: [{ name: "油卡充值_0925.jpg" }] },
- { applicant: "胡明", plate: "皖LB1283", type: "油卡", date: "2026-09-18", km: 96800, amount: 1800, used: 1420, note: "", status: "已提交", attachments: [{ name: "油卡充值_0918.jpg" }] },
- { applicant: "吴涛", plate: "皖K92881", type: "油卡", date: "2026-09-08", km: 208900, amount: 2200, used: 1900, note: "", status: "已提交", attachments: [{ name: "油卡充值_0908.jpg" }] },
- { applicant: "吴涛", plate: "皖K92881", type: "油卡", date: "2026-09-26", km: 210110, amount: 1500, used: 980, note: "", status: "已提交", attachments: [{ name: "油卡充值_0926.jpg" }] },
- { applicant: "郑浩", plate: "皖H61208", type: "油卡", date: "2026-09-16", km: 64200, amount: 800, used: 520, note: "", status: "已提交", attachments: [{ name: "油卡充值_0916.jpg" }] },
- { applicant: "李强", plate: "皖K73155", type: "油卡", date: "2026-09-19", km: 116800, amount: 1600, used: 1280, note: "", status: "已提交", attachments: [{ name: "油卡充值_0919.jpg" }] },
- { applicant: "王磊", plate: "皖L88201", type: "油卡", date: "2026-09-24", km: 154100, amount: 2400, used: 1860, note: "", status: "已提交", attachments: [{ name: "油卡充值_0924.jpg" }] },
- { applicant: "孙斌", plate: "皖H90366", type: "油卡", date: "2026-09-25", km: 88200, amount: 1200, used: 860, note: "", status: "已提交", attachments: [{ name: "油卡充值_0925.jpg" }] },
- { applicant: "周建军", plate: "皖H57837", type: "油卡", date: "2026-10-01", km: 186420, amount: 2200, used: 1740, note: "", status: "已提交", attachments: [{ name: "油卡充值_1001.jpg" }] },
- { applicant: "周建军", plate: "皖H57837", type: "油卡", date: "2026-10-08", km: 188900, amount: 1500, used: 1100, note: "", status: "已提交", attachments: [{ name: "油卡充值_1008.jpg" }] },
- { applicant: "陈伟", plate: "皖HA3690", type: "油卡", date: "2026-10-07", km: 145200, amount: 2800, used: 2300, note: "", status: "已提交", attachments: [{ name: "油卡充值_1007.jpg" }] },
- { applicant: "胡明", plate: "皖LB1283", type: "油卡", date: "2026-10-01", km: 97850, amount: 1600, used: 1280, note: "", status: "已提交", attachments: [{ name: "油卡充值_1001b.jpg" }] },
- { applicant: "胡明", plate: "皖LB1283", type: "油卡", date: "2026-10-08", km: 100480, amount: 1200, used: 860, note: "", status: "已提交", attachments: [{ name: "油卡充值_1008b.jpg" }] },
- { applicant: "吴涛", plate: "皖K92881", type: "油卡", date: "2026-10-01", km: 210330, amount: 2000, used: 1640, note: "", status: "已提交", attachments: [{ name: "油卡充值_1001c.jpg" }] },
- { applicant: "吴涛", plate: "皖K92881", type: "油卡", date: "2026-10-09", km: 213100, amount: 1800, used: 1420, note: "", status: "已提交", attachments: [{ name: "油卡充值_1009.jpg" }] },
- { applicant: "郑浩", plate: "皖H61208", type: "油卡", date: "2026-10-03", km: 66180, amount: 500, used: 220, note: "停运前充值", status: "已提交", attachments: [{ name: "油卡充值_1003.jpg" }] },
- { applicant: "李强", plate: "皖K73155", type: "油卡", date: "2026-10-01", km: 118600, amount: 1700, used: 1310, note: "", status: "已提交", attachments: [{ name: "油卡充值_1001d.jpg" }] },
- { applicant: "李强", plate: "皖K73155", type: "油卡", date: "2026-10-08", km: 121050, amount: 1400, used: 980, note: "", status: "已提交", attachments: [{ name: "油卡充值_1008c.jpg" }] },
- { applicant: "王磊", plate: "皖L88201", type: "油卡", date: "2026-10-01", km: 156200, amount: 2100, used: 1680, note: "", status: "已提交", attachments: [{ name: "油卡充值_1001e.jpg" }] },
- { applicant: "王磊", plate: "皖L88201", type: "油卡", date: "2026-10-08", km: 159400, amount: 1600, used: 1210, note: "", status: "已提交", attachments: [{ name: "油卡充值_1008d.jpg" }] },
- { applicant: "孙斌", plate: "皖H90366", type: "油卡", date: "2026-10-08", km: 91880, amount: 1300, used: 940, note: "", status: "已提交", attachments: [{ name: "油卡充值_1008e.jpg" }] }
- ];
- let maints = [
- { plate: "皖H57837", type: "定期保养", inDate: "2026-03-12", outDate: "2026-03-12", shop: "公司维修班", amount: 580, nextKm: 178000, item: "机油机滤" },
- { plate: "皖H57837", type: "定期保养", inDate: "2026-09-08", outDate: "2026-09-08", shop: "公司维修班", amount: 620, nextKm: 190000, item: "机油机滤、检查罐阀" },
- { plate: "皖H57837", type: "故障维修", inDate: "2026-10-06", outDate: "2026-10-07", shop: "宜昌安达汽修", amount: 980, nextKm: 192000, item: "更换半轴油封" },
- { plate: "皖HA3690", type: "定期保养", inDate: "2026-04-18", outDate: "2026-04-18", shop: "公司维修班", amount: 720, nextKm: 138000, item: "机油机滤、空滤" },
- { plate: "皖HA3690", type: "故障维修", inDate: "2026-09-28", outDate: "", shop: "宜昌安达汽修", amount: 1860, nextKm: 146000, item: "更换刹车片、排查气路" },
- { plate: "皖LB1283", type: "定期保养", inDate: "2026-05-06", outDate: "2026-05-06", shop: "公司维修班", amount: 560, nextKm: 98000, item: "机油机滤" },
- { plate: "皖LB1283", type: "故障维修", inDate: "2026-10-03", outDate: "2026-10-04", shop: "宜昌安达汽修", amount: 1280, nextKm: 104000, item: "更换轮胎、动平衡" },
- { plate: "皖K92881", type: "定期保养", inDate: "2026-06-22", outDate: "2026-06-22", shop: "公司维修班", amount: 890, nextKm: 208000, item: "机油机滤、检查罐阀" },
- { plate: "皖K92881", type: "故障维修", inDate: "2026-10-08", outDate: "2026-10-09", shop: "宜昌安达汽修", amount: 2100, nextKm: 215000, item: "更换刹车片、调整间隙" },
- { plate: "皖H61208", type: "定期保养", inDate: "2026-02-16", outDate: "2026-02-16", shop: "公司维修班", amount: 540, nextKm: 62000, item: "机油机滤" },
- { plate: "皖H61208", type: "罐体检验", inDate: "2026-09-20", outDate: "", shop: "省特检院宜昌分院", amount: 4800, nextKm: 68000, item: "罐体定期检验" },
- { plate: "皖K73155", type: "定期保养", inDate: "2026-04-09", outDate: "2026-04-09", shop: "公司维修班", amount: 640, nextKm: 114000, item: "机油机滤、检查泵阀" },
- { plate: "皖K73155", type: "故障维修", inDate: "2026-10-02", outDate: "2026-10-03", shop: "宜昌安达汽修", amount: 1560, nextKm: 124000, item: "更换水泵、处理渗漏" },
- { plate: "皖L88201", type: "定期保养", inDate: "2026-05-21", outDate: "2026-05-21", shop: "公司维修班", amount: 700, nextKm: 150000, item: "机油机滤" },
- { plate: "皖L88201", type: "故障维修", inDate: "2026-08-14", outDate: "2026-08-15", shop: "宜昌安达汽修", amount: 1100, nextKm: 156000, item: "更换皮带、张紧轮" },
- { plate: "皖H90366", type: "定期保养", inDate: "2026-02-27", outDate: "2026-02-27", shop: "公司维修班", amount: 580, nextKm: 86000, item: "机油机滤" },
- { plate: "皖H90366", type: "故障维修", inDate: "2026-10-05", outDate: "2026-10-05", shop: "公司维修班", amount: 430, nextKm: 94000, item: "更换雨刮、检查灯光" }
- ];
- let expSeq = 12;
- let preSeq = 4;
- let expTab = "全部";
- let expFinKind = "";
- let expRejectId = null;
- let expFiles = [];
- let expLines = [];
- let selectedInvIds = [];
- const EXP_TYPES = ["差旅费", "交通费", "办公费", "招待费", "通讯费", "采购费", "其他"];
- const EXP_STAFF = [
- { name: "吴九华", dept: "行政人事" },
- { name: "周建军", dept: "运输部" },
- { name: "陈伟", dept: "运输部" },
- { name: "胡明", dept: "生产部" },
- { name: "吴涛", dept: "销售部" },
- { name: "郑浩", dept: "仓储部" },
- { name: "刘洋", dept: "仓储部" },
- { name: "孙斌", dept: "运输部" }
- ];
- let invWallet = [
- { id: 1, name: "高铁票_0920.pdf", amount: 580, date: "2026-09-20" },
- { id: 2, name: "住宿发票.pdf", amount: 420, date: "2026-09-20" },
- { id: 3, name: "过路过桥发票.pdf", amount: 186, date: "2026-09-27" },
- { id: 4, name: "办公耗材专票.pdf", amount: 890, date: "2026-09-26" },
- { id: 5, name: "通讯费发票.pdf", amount: 128, date: "2026-09-25" }
- ];
- const EXP_BUDGET = {
- "运输部": { total: 20000, used: 15800 },
- "生产部": { total: 12000, used: 4100 },
- "行政人事": { total: 8000, used: 2200 },
- "销售部": { total: 15000, used: 14200 },
- "仓储部": { total: 6000, used: 1800 }
- };
- let expLoans = [
- { no: "JK-20260901-01", user: "周建军", dept: "运输部", amount: 3000, remain: 3000, date: "2026-09-01", status: "未清" },
- { no: "JK-20260812-02", user: "吴涛", dept: "销售部", amount: 2000, remain: 0, date: "2026-08-12", status: "已清" }
- ];
- let preExps = [
- { no: "SQ-20260918-001", user: "周建军", dept: "运输部", type: "出差", amount: 2000, date: "2026-09-18", status: "已批准", remark: "宜昌客户拜访差旅预估" },
- { no: "SQ-20260928-002", user: "吴涛", dept: "销售部", type: "招待", amount: 8000, date: "2026-09-28", status: "待财务初审", remark: "重点客户来访招待" },
- { no: "SQ-20260920-003", user: "郑浩", dept: "仓储部", type: "采购", amount: 1200, date: "2026-09-20", status: "已驳回", remark: "劳保采购,事由不清" }
- ];
- let expenses = [
- {
- id: 1, no: "BX-20260928-007", user: "周建军", dept: "运输部", type: "差旅费", amount: 1280,
- date: "2026-09-20", submit: "2026-09-28", status: "待财务初审",
- remark: "宜昌客户拜访往返,关联备用金申请",
- invoices: ["高铁票.pdf", "住宿发票.pdf", "行程单.jpg"],
- payWay: "冲销借款", loanNo: "JK-20260901-01", bank: "",
- preNo: "SQ-20260918-001", overBudget: false,
- logs: [
- { t: "2026-09-18 09:10", who: "周建军", act: "备用金申请已批准", note: "出差预估 2,000 元" },
- { t: "2026-09-28 16:20", who: "周建军", act: "提交报销", note: "贴票 3 张,申请冲销借款" }
- ]
- },
- {
- id: 2, no: "BX-20260927-006", user: "陈伟", dept: "运输部", type: "交通费", amount: 360,
- date: "2026-09-27", submit: "2026-09-27", status: "待财务初审",
- remark: "过路过桥及停车",
- invoices: ["过路过桥发票.pdf", "停车费收据.jpg"],
- payWay: "个人银行卡", loanNo: "", bank: "6222****6180",
- preNo: "", overBudget: false,
- logs: [
- { t: "2026-09-27 11:02", who: "陈伟", act: "提交报销", note: "贴票 2 张" },
- { t: "2026-09-27 15:40", who: "运输部负责人", act: "部门审核通过", note: "业务真实,金额合理" }
- ]
- },
- {
- id: 3, no: "BX-20260926-005", user: "吴九华", dept: "行政人事", type: "办公费", amount: 890,
- date: "2026-09-26", submit: "2026-09-26", status: "待终审",
- remark: "打印耗材与档案袋",
- invoices: ["增值税普通发票.pdf"],
- payWay: "对公转账", loanNo: "", bank: "山峡代工办公耗材户",
- preNo: "", overBudget: false, finalRole: "部门经理",
- logs: [
- { t: "2026-09-26 10:11", who: "吴九华", act: "提交报销", note: "" },
- { t: "2026-09-26 14:02", who: "行政人事负责人", act: "部门审核通过", note: "部门工作需要" },
- { t: "2026-09-26 17:20", who: "财务会计", act: "财务初审通过", note: "发票真伪核验通过,预算有额度" }
- ]
- },
- {
- id: 4, no: "BX-20260925-004", user: "胡明", dept: "生产部", type: "通讯费", amount: 128,
- date: "2026-09-25", submit: "2026-09-25", status: "待付款",
- remark: "9 月通讯补贴",
- invoices: ["通讯费发票.pdf"],
- payWay: "个人银行卡", loanNo: "", bank: "6228****0902",
- preNo: "", overBudget: false, finalRole: "部门经理",
- logs: [
- { t: "2026-09-25 09:00", who: "胡明", act: "提交报销", note: "" },
- { t: "2026-09-25 10:20", who: "生产部负责人", act: "部门审核通过", note: "属岗位补贴" },
- { t: "2026-09-25 11:10", who: "财务会计", act: "财务初审通过", note: "票据合规" },
- { t: "2026-09-25 16:00", who: "部门经理", act: "终审同意", note: "小额,部门经理终审" }
- ]
- },
- {
- id: 5, no: "BX-20260924-003", user: "郑浩", dept: "仓储部", type: "交通费", amount: 210,
- date: "2026-09-24", submit: "", status: "草稿",
- remark: "送样至检测站",
- invoices: [],
- payWay: "个人银行卡", loanNo: "", bank: "",
- preNo: "", overBudget: false, logs: [{ t: "2026-09-24 18:01", who: "郑浩", act: "保存草稿", note: "待补发票" }]
- },
- {
- id: 6, no: "BX-20260922-002", user: "吴涛", dept: "销售部", type: "招待费", amount: 2460,
- date: "2026-09-22", submit: "2026-09-22", status: "已驳回",
- remark: "客户来访接待",
- invoices: ["餐饮小票.jpg"],
- payWay: "个人银行卡", loanNo: "", bank: "6222****0770",
- preNo: "", overBudget: false,
- logs: [
- { t: "2026-09-22 20:10", who: "吴涛", act: "提交报销", note: "" },
- { t: "2026-09-23 09:30", who: "销售部负责人", act: "部门审核通过", note: "来访属实" },
- { t: "2026-09-23 14:18", who: "财务会计", act: "退回补资料", note: "发票抬头不符、缺少酒水清单,请补增值税发票后重提" }
- ]
- },
- {
- id: 7, no: "BX-20260918-001", user: "刘洋", dept: "仓储部", type: "办公费", amount: 540,
- date: "2026-09-18", submit: "2026-09-18", status: "已归档",
- remark: "劳保用品补购",
- invoices: ["专票.pdf", "入库单.jpg"],
- payWay: "对公转账", loanNo: "", bank: "劳保供应商对公户",
- preNo: "", overBudget: false, voucher: "记-202609-118",
- logs: [
- { t: "2026-09-18 11:00", who: "刘洋", act: "提交报销", note: "" },
- { t: "2026-09-18 15:00", who: "仓储部负责人", act: "部门审核通过", note: "属库区劳保" },
- { t: "2026-09-19 09:10", who: "财务会计", act: "财务初审通过", note: "可抵扣专票" },
- { t: "2026-09-19 11:00", who: "部门经理", act: "终审同意", note: "" },
- { t: "2026-09-19 16:40", who: "出纳", act: "已付款", note: "对公支付 540 元" },
- { t: "2026-09-20 10:05", who: "会计", act: "制单归档", note: "凭证 记-202609-118,发票装订" }
- ]
- },
- {
- id: 8, no: "BX-20260930-008", user: "吴涛", dept: "销售部", type: "招待费", amount: 12800,
- date: "2026-09-30", submit: "2026-09-30", status: "待终审",
- remark: "重点客户商务招待,超部门预算",
- invoices: ["餐饮专票.pdf", "签到表.pdf"],
- payWay: "对公转账", loanNo: "", bank: "酒店对公户",
- preNo: "SQ-20260928-002", overBudget: true, finalRole: "总经理",
- logs: [
- { t: "2026-09-30 08:40", who: "吴涛", act: "提交报销", note: "备用金申请尚未批完,同步提报" },
- { t: "2026-09-30 10:12", who: "销售部负责人", act: "部门审核通过", note: "业务真实,金额偏高已说明" },
- { t: "2026-09-30 14:55", who: "财务会计", act: "财务初审通过", note: "票据合规;销售部预算剩余不足,转总经理专项终审" }
- ]
- },
- {
- id: 9, no: "BX-20260929-009", user: "孙斌", dept: "运输部", type: "差旅费", amount: 4860,
- date: "2026-09-12", submit: "2026-09-29", status: "待财务初审",
- remark: "安庆办事处驻点一周",
- invoices: ["住宿发票.pdf", "餐饮发票.pdf", "高铁票.pdf"],
- payWay: "个人银行卡", loanNo: "", bank: "6222****4410",
- preNo: "", overBudget: false, finalRole: "财务主管",
- logs: [
- { t: "2026-09-29 09:18", who: "孙斌", act: "提交报销", note: "发生日距今已超 15 天" },
- { t: "2026-09-29 11:30", who: "运输部负责人", act: "部门审核通过", note: "驻点任务属实" }
- ]
- },
- {
- id: 10, no: "BX-20260801-010", user: "赵鹏", dept: "生产部", type: "交通费", amount: 180,
- date: "2026-07-10", submit: "2026-09-20", status: "已驳回",
- remark: "取样送检打车",
- invoices: ["打车票.jpg"],
- payWay: "个人银行卡", loanNo: "", bank: "6217****3301",
- preNo: "", overBudget: false,
- logs: [
- { t: "2026-09-20 08:00", who: "赵鹏", act: "提交报销", note: "" },
- { t: "2026-09-20 09:40", who: "财务会计", act: "逾期退回", note: "业务结束已超过 45 天,按制度不予受理" }
- ]
- },
- {
- id: 11, no: "BX-20260921-011", user: "李强", dept: "运输部", type: "差旅费", amount: 960,
- date: "2026-09-21", submit: "2026-09-21", status: "待归档",
- remark: "随车押运补贴及餐费",
- invoices: ["餐票.pdf"],
- payWay: "冲销借款", loanNo: "JK-20260901-01", bank: "",
- preNo: "", overBudget: false, paid: 960,
- logs: [
- { t: "2026-09-21 17:00", who: "李强", act: "提交报销", note: "" },
- { t: "2026-09-22 09:00", who: "运输部负责人", act: "部门审核通过", note: "" },
- { t: "2026-09-22 11:20", who: "财务会计", act: "财务初审通过", note: "" },
- { t: "2026-09-22 15:00", who: "部门经理", act: "终审同意", note: "" },
- { t: "2026-09-23 10:10", who: "出纳", act: "已冲借款", note: "冲销 JK-20260901-01 中 960 元" }
- ]
- }
- ];
- let currentMod = "plan";
- const PLACE_PAGES = {
- fav: { parent: "收藏", title: "收藏", desc: "常用功能与页面可在此固定,便于快速进入。" },
- todo: { parent: "待办列表", title: "待办列表", desc: "汇总审批、预警与待处理事项。" },
- "crm-quote": { parent: "CRM管理", title: "报价单管理", desc: "客户询价、报价编制与跟进。" },
- "crm-contract": { parent: "CRM管理", title: "合同管理", desc: "合同起草、审批、归档与履约跟踪。" },
- "crm-price": { parent: "CRM管理", title: "调价函管理", desc: "调价申请、函件下达与生效记录。" },
- "crm-customer": { parent: "CRM管理", title: "客户管理", desc: "客户档案、信用与跟进记录。", coming: "使用山峡系统原系统页面" },
- "fin-ar": { parent: "财务管理", title: "应收应付", desc: "客户应收、供应商应付对账与核销。" },
- "fin-view": { parent: "财务管理", title: "视图中心", desc: "财务看板与关键指标视图。" },
- "fin-petty": { parent: "财务管理", title: "备用金管理", desc: "备用金领用、核销与结余。" },
- "fin-config": { parent: "财务管理", title: "基础配置", desc: "科目、审批流与财务基础参数。" },
- "hr-org": { parent: "人事管理", title: "组织架构", desc: "部门编制与汇报关系。" },
- weigh: { parent: "运输管理", title: "地磅管理", desc: "过磅记录、皮重净重与出厂对账。" },
- freight: { parent: "运输管理", title: "运费结算", desc: "按计划、车辆与线路核算运费,对账后结算。" },
- system: { parent: "系统管理", title: "系统管理", desc: "用户、角色、权限与基础参数。" },
- "sys-dict": { parent: "系统管理", title: "字典管理", desc: "维护系统枚举、编码与基础数据字典。" }
- };
- let prodPage = 1;
- let prodPageSize = 20;
- let prodSeq = 7463;
- let selectedProdIds = new Set();
- let prodCatFilter = "";
- let prodCats = ["其他", "酸类", "碱类", "溶剂", "助剂", "包装"];
- const PROD_SEED = [
- { name: "硼砂", cat: "其他", brand: "益泉", alias: "", spec: "98%", price: "", hz: "", py: "BS" },
- { name: "38%工业硫酸", cat: "", brand: "", alias: "", spec: "38%", price: "", hz: "毒", py: "GYSF" },
- { name: "吨桶", cat: "其他", brand: "", alias: "", spec: "/", price: "", hz: "", py: "DT" },
- { name: "双氧水", cat: "", brand: "", alias: "", spec: "AR", price: "", hz: "爆", py: "SYS" },
- { name: "活性炭", cat: "", brand: "", alias: "", spec: "", price: "", hz: "", py: "HXT" },
- { name: "液氨", cat: "", brand: "", alias: "", spec: "", price: "", hz: "", py: "YA" },
- { name: "乙二醇", cat: "", brand: "", alias: "", spec: "", price: "", hz: "", py: "YEC" },
- { name: "次氯酸钠", cat: "", brand: "", alias: "", spec: "A型I类", price: "", hz: "", py: "CLSN" },
- { name: "亚硫酸氢钠", cat: "", brand: "", alias: "", spec: "AR", price: "", hz: "", py: "YLSQN" },
- { name: "工业硫酸", cat: "", brand: "", alias: "", spec: "≥93%", price: "", hz: "毒", py: "GYSF" },
- { name: "氨水", cat: "", brand: "", alias: "", spec: "25%-28%", price: "", hz: "", py: "AS" },
- { name: "工业硫酸", cat: "", brand: "", alias: "", spec: "53%-56%", price: "", hz: "毒", py: "GYSF" },
- { name: "片碱", cat: "碱类", brand: "", alias: "氢氧化钠", spec: "99%", price: "3700", hz: "", py: "PJ" },
- { name: "32%液碱", cat: "碱类", brand: "", alias: "", spec: "32%", price: "840", hz: "", py: "YJ" },
- { name: "50%液碱", cat: "碱类", brand: "", alias: "", spec: "50%", price: "1270", hz: "", py: "YJ" },
- { name: "工业盐酸", cat: "酸类", brand: "", alias: "", spec: "31%", price: "200", hz: "毒", py: "YS" },
- { name: "一水柠檬酸", cat: "助剂", brand: "", alias: "", spec: "99.50%", price: "11000", hz: "", py: "NMS" },
- { name: "聚合氯化铝", cat: "助剂", brand: "", alias: "PAC", spec: "28%", price: "2000", hz: "", py: "JHLSL" },
- { name: "氯化钠", cat: "助剂", brand: "", alias: "工业盐", spec: "国标", price: "1400", hz: "", py: "LHN" },
- { name: "98%工业硫酸", cat: "酸类", brand: "", alias: "", spec: "98%", price: "900", hz: "毒", py: "GYSF" }
- ];
- const products = [];
- (function buildProducts() {
- for (let i = 0; i < 143; i++) {
- const s = PROD_SEED[i % PROD_SEED.length];
- const no = "SXWL" + String(7463 - i).padStart(6, "0");
- const extraOff = i > 40 && i % 11 === 0;
- products.push({
- id: i + 1,
- no,
- name: s.name,
- cat: s.cat,
- brand: s.brand,
- alias: s.alias,
- spec: s.spec,
- price: s.price,
- hz: s.hz,
- py: s.py,
- status: extraOff ? "下架" : "上架"
- });
- }
- })();
- function dash(v) { return v === undefined || v === null || String(v).trim() === "" ? "-" : v; }
- function filteredProducts() {
- const q = (document.getElementById("prodQ")?.value || "").trim().toUpperCase();
- let rows = products.slice();
- if (prodCatFilter) rows = rows.filter((x) => (x.cat || "未分类") === prodCatFilter);
- if (q) {
- rows = rows.filter((x) => [x.no, x.name, x.brand, x.alias, x.spec, x.py, x.cat].join(" ").toUpperCase().includes(q));
- }
- return rows;
- }
- function selectedProds() { return products.filter((x) => selectedProdIds.has(x.id)); }
- function toggleProd(id, on) {
- if (on) selectedProdIds.add(id); else selectedProdIds.delete(id);
- renderProducts();
- }
- function toggleProdAll(on) {
- const rows = filteredProducts();
- const start = (prodPage - 1) * prodPageSize;
- rows.slice(start, start + prodPageSize).forEach((x) => on ? selectedProdIds.add(x.id) : selectedProdIds.delete(x.id));
- renderProducts();
- }
- function fillProdCatSelect(val) {
- const sel = document.getElementById("prodCatSelect");
- if (!sel) return;
- sel.innerHTML = `<option value="">未分类</option>` + prodCats.map((c) => `<option>${c}</option>`).join("");
- if (val != null) sel.value = val;
- }
- function renderProducts() {
- const rows = filteredProducts();
- const pages = Math.max(1, Math.ceil(rows.length / prodPageSize));
- if (prodPage > pages) prodPage = pages;
- const start = (prodPage - 1) * prodPageSize;
- const pageRows = rows.slice(start, start + prodPageSize);
- const allOn = pageRows.length && pageRows.every((x) => selectedProdIds.has(x.id));
- document.getElementById("prodSelHint").textContent = `已选 ${selectedProdIds.size} 项`;
- const filt = `<span class="th-filt"></span>`;
- document.getElementById("prodTable").innerHTML = pageRows.length ? `<table class="erp-table prod-table">
- <thead><tr>
- <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="toggleProdAll(this.checked)" /></th>
- <th class="col-idx">序号</th>
- <th>产品编号${filt}</th>
- <th>产品名称${filt}</th>
- <th>产品分类${filt}</th>
- <th>品牌${filt}</th>
- <th>别名${filt}</th>
- <th>规格型号${filt}</th>
- <th>标准价(元)${filt}</th>
- <th>状态</th>
- </tr></thead>
- <tbody>${pageRows.map((x, i) => `<tr>
- <td class="chk"><input type="checkbox" ${selectedProdIds.has(x.id) ? "checked" : ""} onchange="toggleProd(${x.id}, this.checked)" /></td>
- <td>${start + i + 1}</td>
- <td>${x.no}</td>
- <td><button class="quote-link" type="button" onclick="openProdDlg(${x.id})">${x.name}</button>${x.hz ? `<span class="prod-hz">${x.hz}</span>` : ""}</td>
- <td>${x.cat || ""}</td>
- <td>${dash(x.brand)}</td>
- <td>${dash(x.alias)}</td>
- <td>${dash(x.spec)}</td>
- <td class="${x.price ? "" : "prod-price"}">${dash(x.price)}</td>
- <td><span class="prod-st${x.status === "下架" ? " off" : ""}">${x.status}</span></td>
- </tr>`).join("")}</tbody></table>` : `<div class="empty">暂无产品</div>`;
- const nums = [];
- const show = Math.min(pages, 6);
- for (let n = 1; n <= show; n++) nums.push(n);
- document.getElementById("prodPager").innerHTML = `
- <span class="muted">共 ${rows.length} 条</span>
- <select class="page-size" onchange="prodPageSize=+this.value;prodPage=1;renderProducts()">
- <option value="20" ${prodPageSize === 20 ? "selected" : ""}>20条/页</option>
- <option value="50" ${prodPageSize === 50 ? "selected" : ""}>50条/页</option>
- </select>
- <button type="button" onclick="prodPage=Math.max(1, prodPage-1);renderProducts()">‹</button>
- ${nums.map((n) => `<button type="button" class="${n === prodPage ? "on" : ""}" onclick="prodPage=${n};renderProducts()">${n}</button>`).join("")}
- ${pages > 6 ? `<span class="muted">…</span><button type="button" class="${prodPage === pages ? "on" : ""}" onclick="prodPage=${pages};renderProducts()">${pages}</button>` : ""}
- <button type="button" onclick="prodPage=Math.min(${pages}, prodPage+1);renderProducts()">›</button>
- <span class="muted">前往</span>
- <input class="quote-goto" type="number" min="1" max="${pages}" value="${prodPage}" onchange="prodPage=Math.min(${pages}, Math.max(1, +this.value||1));renderProducts()" />
- <span class="muted">页</span>`;
- }
- function openProdDlg(id) {
- const f = document.getElementById("prodForm");
- f.reset();
- const x = id ? products.find((p) => p.id === id) : null;
- fillProdCatSelect(x ? (x.cat || "") : "");
- document.getElementById("prodDlgTitle").textContent = x ? "编辑产品" : "新增产品";
- f.id.value = x ? x.id : "";
- if (x) {
- f.name.value = x.name;
- f.no.value = x.no;
- f.brand.value = x.brand || "";
- f.alias.value = x.alias || "";
- f.spec.value = x.spec || "";
- f.price.value = x.price || "";
- f.py.value = x.py || "";
- f.hz.value = x.hz || "";
- f.status.value = x.status;
- }
- document.getElementById("prodDialog").showModal();
- }
- function saveProd() {
- const f = document.getElementById("prodForm");
- if (!f.reportValidity()) return;
- const payload = {
- name: f.name.value.trim(),
- no: f.no.value.trim(),
- cat: f.cat.value,
- brand: f.brand.value.trim(),
- alias: f.alias.value.trim(),
- spec: f.spec.value.trim(),
- price: f.price.value,
- py: (f.py.value.trim() || f.name.value.trim()).toUpperCase(),
- hz: f.hz.value,
- status: f.status.value
- };
- const id = f.id.value ? +f.id.value : 0;
- if (id) {
- const x = products.find((p) => p.id === id);
- Object.assign(x, payload);
- toast("已保存 " + x.no);
- } else {
- prodSeq += 1;
- if (!payload.no) payload.no = "SXWL" + String(prodSeq).padStart(6, "0");
- products.unshift({ id: Date.now(), ...payload });
- toast("已新增 " + payload.no);
- prodPage = 1;
- }
- document.getElementById("prodDialog").close();
- renderProducts();
- }
- function editSelectedProd() {
- const rows = selectedProds();
- if (rows.length !== 1) { toast("请勾选一条产品后再编辑"); return; }
- openProdDlg(rows[0].id);
- }
- function deleteProds() {
- const rows = selectedProds();
- if (!rows.length) { toast("请先勾选产品"); return; }
- const ids = new Set(rows.map((x) => x.id));
- for (let i = products.length - 1; i >= 0; i--) {
- if (ids.has(products[i].id)) products.splice(i, 1);
- }
- selectedProdIds.clear();
- toast(`已删除 ${rows.length} 条产品`);
- renderProducts();
- }
- function shelfProds(st) {
- const rows = selectedProds();
- if (!rows.length) { toast("请先勾选产品"); return; }
- rows.forEach((x) => { x.status = st; });
- toast(`已${st} ${rows.length} 条产品`);
- renderProducts();
- }
- function toggleProdExport(e) {
- e.stopPropagation();
- document.getElementById("prodExportDrop").classList.toggle("open");
- }
- function exportProducts(scope) {
- document.getElementById("prodExportDrop").classList.remove("open");
- const n = scope === "page" ? Math.min(prodPageSize, filteredProducts().length) : filteredProducts().length;
- toast(`演示:已导出 ${n} 条产品`);
- }
- function openProdCatDlg() {
- renderProdCats();
- document.getElementById("prodCatDialog").showModal();
- }
- function renderProdCats() {
- const counts = {};
- products.forEach((x) => {
- const k = x.cat || "未分类";
- counts[k] = (counts[k] || 0) + 1;
- });
- const keys = ["全部", ...prodCats, "未分类"];
- document.getElementById("prodCatList").innerHTML = keys.map((c) => {
- const n = c === "全部" ? products.length : (counts[c] || 0);
- const on = (c === "全部" && !prodCatFilter) || prodCatFilter === c;
- return `<button type="button" class="${on ? "on" : ""}" onclick="filterProdCat('${c}')"><span>${c}</span><span>${n}</span></button>`;
- }).join("");
- }
- function filterProdCat(c) {
- prodCatFilter = c === "全部" ? "" : c;
- prodPage = 1;
- renderProdCats();
- renderProducts();
- }
- function addProdCat() {
- const name = (document.getElementById("prodCatNew").value || "").trim();
- if (!name) { toast("请输入分类名称"); return; }
- if (prodCats.includes(name)) { toast("分类已存在"); return; }
- prodCats.push(name);
- document.getElementById("prodCatNew").value = "";
- renderProdCats();
- toast("已新增分类 " + name);
- }
- let currentCertId = null;
- let certSeq = 4;
- let transSeq = 5;
- let planSeq = 21;
- let planTab = "全部";
- let planDateTab = "全部";
- let archiveTab = "待审核";
- let selectedArchiveIds = new Set();
- let planCardFilter = "";
- let selectedPlanIds = new Set();
- let linkingPlanTaskIds = [];
- let taskTab = "全部";
- let selectedTaskIds = new Set();
- let reportingTaskId = null;
- const SITE_STAFF = ["刘洋(硫酸库)", "张伟(盐酸库)", "陈刚(分装库)", "王磊(液碱库)", "李强(现场班长)", "赵敏(精制岗)"];
- let taskSeq = 3;
- const TASKS = [
- {
- id: 1, no: "SXFZ20261001-001", kind: "稀释", status: "待执行",
- date: "2026-10-01", orderDate: "2026-10-01", deliverDate: "2026-10-04",
- orderBy: "周建军", handler: "刘洋(硫酸库)", maker: "郑丽娜", auditor: "", supervisor: "",
- bucket: "桶循环", remark: "现场按规程佩戴防护",
- planTime: "2026-10-04T08:00", planId: 7, planNo: "CK20260928001",
- product: "硫酸", spec: "98%", pack: "槽罐车", qty: 30, unit: "吨",
- assignee: "刘洋(硫酸库)",
- lines: [{ planId: 7, planNo: "CK20260928001", product: "硫酸", model: "98%", pack: "槽罐车", unit: "吨", qty: 30, done: false }]
- },
- {
- id: 2, no: "SXFZ20261001-002", kind: "精制", status: "执行中",
- date: "2026-10-01", orderDate: "2026-10-01", deliverDate: "2026-10-03",
- orderBy: "胡明", handler: "张伟(盐酸库)", maker: "郑丽娜", auditor: "", supervisor: "",
- bucket: "全新桶", remark: "试剂线单独作业,不得混兑",
- planTime: "2026-10-03T09:00", planId: 9, planNo: "RZ20260921012",
- product: "工业盐酸", spec: "31%", pack: "槽罐车", qty: 28, unit: "吨",
- assignee: "张伟(盐酸库)",
- receivedAt: "2026-10-03T08:30",
- lines: [{ planId: 9, planNo: "RZ20260921012", product: "工业盐酸", model: "31%", pack: "槽罐车", unit: "吨", qty: 28, done: false }]
- },
- {
- id: 3, no: "SXFZ20260930-001", kind: "分装", status: "已完成",
- date: "2026-09-30", orderDate: "2026-09-30", deliverDate: "2026-10-07",
- orderBy: "调度员", handler: "陈刚(分装库)", maker: "郑丽娜", auditor: "", supervisor: "",
- bucket: "桶循环", remark: "江峰",
- planTime: "2026-10-02T14:00", planId: 8, planNo: "ZX20260925008",
- product: "工业硫酸", spec: "98%", pack: "吨桶", qty: 22, unit: "吨",
- assignee: "陈刚(分装库)",
- receivedAt: "2026-10-02T13:40",
- doneTime: "2026-10-02T17:20", doneQty: 22,
- report: "22 吨已分装,桶循环。",
- 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>') }],
- lines: [{ planId: 8, planNo: "ZX20260925008", product: "工业硫酸", model: "98%", pack: "吨桶", unit: "吨", qty: 22, done: true }]
- }
- ];
- let linkingPlanId = null;
- let waitBuyPlanId = null;
- let waitTransPlanId = null;
- let voidCertTarget = null;
- let linkingCertId = null;
- let linkingCertFocusIds = [];
- let extraTransN = 0;
- let archivingPlanId = null;
- let currentTransId = null;
- let attachBuyId = null;
- const certFiles = { buy: null, trans: null, buyUrl: "", transUrl: "" };
- const ocrBuySample = {
- buyNo: "G36260499487476", buyDate: "2026-09-24",
- buyer: "上饶市敏达精细化工有限公司", buyerLegal: "熊伟", buyerTel: "0793-8224115", buyerAddr: "上饶市胜利路96-3号",
- seller: "安庆市山峡化工有限公司", sellerLegal: "吴春生", sellerTel: "0556-5348918", sellerAddr: "安徽省安庆市高新区霞虹路5号",
- product: "硫酸", usage: "销售", buyQty: 450, unit: "吨", buyTimes: "多次有效",
- buyFrom: "2026-09-24", buyDue: "2026-10-23",
- buyPolice: "上饶市公安局信州分局禁毒大队", buyHandler: "潘佳", buyPoliceTel: "13755372147"
- };
- const ocrTransSample = {
- transNo: "Y34260211715884", transDate: "2026-09-03",
- owner: "铜陵锦帆物资有限责任公司", ownerLegal: "何红燕", ownerTel: "0562-2119658",
- ownerAddr: "安徽省铜陵市滨江大道和铜港路交叉口滨江大道停车场",
- pack: "槽罐", permitNo: "G34260528735347",
- carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
- mode: "汽车", plates: "皖HA3690、皖H57837、皖LB1283",
- route: "自安徽省铜陵市铜官区经池州市至安徽省安庆市高新",
- consignee: "安庆市山峡化工有限公司", transQty: 500,
- transTimes: "多次有效", transFrom: "2026-09-03", transDue: "2026-12-02",
- applicantUnit: "铜陵锦帆物资有限责任公司",
- transPolice: "铜陵市公安局铜官分局环食药(禁毒)大队", transHandler: "桂强"
- };
- const ocrTransSample2 = {
- transNo: "Y34260222800112", transDate: "2026-09-25",
- owner: "铜陵锦帆物资有限责任公司", ownerLegal: "何红燕", ownerTel: "0562-2119658",
- ownerAddr: "安徽省铜陵市滨江大道和铜港路交叉口滨江大道停车场",
- pack: "槽罐", permitNo: "G36260499487476",
- carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
- mode: "汽车", plates: "皖K92881",
- route: "自安徽省铜陵市经安庆至宜昌猇亭",
- consignee: "安庆市山峡化工有限公司", transQty: 200,
- transTimes: "多次有效", transFrom: "2026-09-25", transDue: "2026-12-24",
- applicantUnit: "铜陵锦帆物资有限责任公司",
- transPolice: "铜陵市公安局铜官分局环食药(禁毒)大队", transHandler: "桂强"
- };
- const ocrTransSamples = [ocrTransSample, ocrTransSample2];
- let certs = [
- {
- id: 1, created: today, shipped: 40, ctrl: "易制毒",
- ...ocrBuySample, product: "硫酸", unit: "吨",
- buyFile: "购买备案证明样例.png", buyUrl: "购买备案证明样例.png",
- transList: [
- {
- id: 1, created: today, shipped: 40,
- transNo: "Y34260211715884", transDate: "2026-09-03",
- owner: "铜陵锦帆物资有限责任公司", ownerLegal: "何红燕", ownerTel: "0562-2119658",
- ownerAddr: "安徽省铜陵市滨江大道和铜港路交叉口滨江大道停车场",
- pack: "槽罐", permitNo: "G36260499487476",
- carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
- mode: "汽车", plates: "皖HA3690、皖H57837、皖LB1283",
- route: "自安徽省铜陵市铜官区经池州市至安徽省安庆市高新",
- consignee: "安庆市山峡化工有限公司", transQty: 250,
- transTimes: "多次有效", transFrom: "2026-09-03", transDue: "2026-12-02",
- applicantUnit: "铜陵锦帆物资有限责任公司",
- transPolice: "铜陵市公安局铜官分局环食药(禁毒)大队", transHandler: "桂强",
- transFile: "运输备案证明样例.png", transUrl: "运输备案证明样例.png",
- dispatches: [
- { plate: "皖HA3690", driver: "周建军", qty: 90, shipped: 0 },
- { plate: "皖H57837", driver: "陈伟", qty: 80, shipped: 0 },
- { plate: "皖LB1283", driver: "胡明", qty: 80, shipped: 40 }
- ]
- },
- {
- id: 2, created: today, shipped: 0,
- transNo: "Y34260222800112", transDate: "2026-09-25",
- owner: "铜陵锦帆物资有限责任公司", ownerLegal: "何红燕", ownerTel: "0562-2119658",
- ownerAddr: "安徽省铜陵市滨江大道和铜港路交叉口滨江大道停车场",
- pack: "槽罐", permitNo: "G36260499487476",
- carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
- mode: "汽车", plates: "皖K92881",
- route: "自安徽省铜陵市经安庆至宜昌猇亭",
- consignee: "安庆市山峡化工有限公司", transQty: 200,
- transTimes: "多次有效", transFrom: "2026-09-25", transDue: "2026-12-24",
- applicantUnit: "铜陵锦帆物资有限责任公司",
- transPolice: "铜陵市公安局铜官分局环食药(禁毒)大队", transHandler: "桂强",
- transFile: "运输证_硫酸_2.jpg", transUrl: "",
- dispatches: [
- { plate: "皖K92881", driver: "吴涛", qty: 200, shipped: 0 }
- ],
- voided: true, voidedAt: today, voidQty: 200
- },
- {
- id: 5, created: today, shipped: 0,
- transNo: "Y36260433900188", transDate: "2026-09-26",
- owner: "上饶市敏达精细化工有限公司", ownerLegal: "熊伟", ownerTel: "0793-8224115",
- ownerAddr: "上饶市胜利路96-3号",
- pack: "槽罐", permitNo: "G36260499487476",
- carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
- mode: "汽车", plates: "皖HA3690、皖H57837、皖K92881",
- route: "自安庆市高新区至上饶市信州区",
- consignee: "上饶市敏达精细化工有限公司", transQty: 180,
- transTimes: "多次有效", transFrom: "2026-09-26", transDue: "2026-12-25",
- applicantUnit: "上饶市敏达精细化工有限公司",
- transPolice: "上饶市公安局信州分局禁毒大队", transHandler: "潘佳",
- transFile: "运输证_敏达硫酸.jpg", transUrl: "",
- dispatches: [
- { plate: "皖HA3690", driver: "周建军", qty: 60, shipped: 0 },
- { plate: "皖H57837", driver: "陈伟", qty: 60, shipped: 0 },
- { plate: "皖K92881", driver: "吴涛", qty: 60, shipped: 0 }
- ]
- }
- ]
- },
- {
- id: 2, created: today, shipped: 16, ctrl: "易制爆",
- buyNo: "G42100311880021", buyDate: "2026-09-20",
- buyer: "宜昌爆破器材公司", buyerLegal: "张磊", buyerTel: "0717-8801122", buyerAddr: "宜昌市夷陵区发展大道18号",
- seller: "安庆市山峡化工有限公司", sellerLegal: "吴春生", sellerTel: "0556-5348918", sellerAddr: "安徽省安庆市高新区霞虹路5号",
- product: "硝酸铵", usage: "生产使用", buyQty: 80, unit: "吨", buyTimes: "多次有效",
- buyFrom: "2026-09-20", buyDue: "2026-11-20",
- buyPolice: "宜昌市公安局禁毒支队", buyHandler: "李敏", buyPoliceTel: "0717-1234567",
- buyFile: "购买证_硝酸铵.jpg", buyUrl: "",
- transList: [
- {
- id: 3, created: today, shipped: 16,
- transNo: "Y42100322991008", transDate: "2026-09-20",
- owner: "宜昌爆破器材公司", ownerLegal: "张磊", ownerTel: "0717-8801122",
- ownerAddr: "宜昌市夷陵区发展大道18号",
- pack: "袋装", permitNo: "G42100311880021",
- carrier: "宜昌安达危运有限公司", carrierAddr: "宜昌市猇亭区/0717-6677889",
- mode: "汽车", plates: "鄂E·危D077",
- route: "自安庆市高新区至宜昌市夷陵区",
- consignee: "宜昌爆破器材公司", transQty: 80,
- transTimes: "多次有效", transFrom: "2026-09-20", transDue: "2026-11-20",
- applicantUnit: "宜昌爆破器材公司",
- transPolice: "宜昌市公安局禁毒支队", transHandler: "王芳",
- transFile: "运输证_硝酸铵.jpg", transUrl: "",
- dispatches: [
- { plate: "鄂E·危D077", driver: "吴涛", qty: 80, shipped: 16 }
- ]
- }
- ]
- },
- {
- id: 3, created: today, shipped: 0, ctrl: "易制毒",
- buyNo: "G34080266110031", buyDate: "2026-09-18",
- buyer: "安庆市山峡化工有限公司", buyerLegal: "吴春生", buyerTel: "0556-5348918", buyerAddr: "安徽省安庆市高新区霞虹路5号",
- seller: "铜陵锦帆物资有限责任公司", sellerLegal: "何红燕", sellerTel: "0562-2119658", sellerAddr: "安徽省铜陵市滨江大道和铜港路交叉口",
- product: "硫酸", usage: "生产使用", buyQty: 500, unit: "吨", buyTimes: "多次有效",
- buyFrom: "2026-09-18", buyDue: "2026-12-17",
- buyPolice: "安庆市公安局高新分局禁毒大队", buyHandler: "刘洋", buyPoliceTel: "0556-5201100",
- buyFile: "购买证_山峡采购硫酸.jpg", buyUrl: "",
- transList: [
- {
- id: 4, created: today, shipped: 0,
- transNo: "Y34080277220045", transDate: "2026-09-18",
- owner: "铜陵锦帆物资有限责任公司", ownerLegal: "何红燕", ownerTel: "0562-2119658",
- ownerAddr: "安徽省铜陵市滨江大道和铜港路交叉口滨江大道停车场",
- pack: "槽罐", permitNo: "G34080266110031",
- carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
- mode: "汽车", plates: "皖LB1283、皖K92881",
- route: "自安徽省铜陵市经安庆至上饶",
- consignee: "安庆市山峡化工有限公司", transQty: 300,
- transTimes: "多次有效", transFrom: "2026-09-18", transDue: "2026-12-17",
- applicantUnit: "铜陵锦帆物资有限责任公司",
- transPolice: "铜陵市公安局铜官分局环食药(禁毒)大队", transHandler: "桂强",
- transFile: "运输证_山峡采购硫酸.jpg", transUrl: "",
- dispatches: [
- { plate: "皖LB1283", driver: "胡明", qty: 150, shipped: 0 },
- { plate: "皖K92881", driver: "吴涛", qty: 150, shipped: 0 }
- ]
- },
- {
- id: 7, created: today, shipped: 0,
- transNo: "Y34080277220118", transDate: "2026-09-28",
- owner: "铜陵锦帆物资有限责任公司", ownerLegal: "何红燕", ownerTel: "0562-2119658",
- ownerAddr: "安徽省铜陵市滨江大道和铜港路交叉口滨江大道停车场",
- pack: "槽罐", permitNo: "G34080266110031",
- carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
- mode: "汽车", plates: "皖HA3690、皖H57837",
- route: "自安徽省铜陵市至安庆市高新区",
- consignee: "安庆市山峡化工有限公司", transQty: 160,
- transTimes: "多次有效", transFrom: "2026-09-28", transDue: "2026-12-27",
- applicantUnit: "铜陵锦帆物资有限责任公司",
- transPolice: "铜陵市公安局铜官分局环食药(禁毒)大队", transHandler: "桂强",
- transFile: "运输证_山峡采购硫酸_2.jpg", transUrl: "",
- dispatches: [
- { plate: "皖HA3690", driver: "周建军", qty: 80, shipped: 0 },
- { plate: "皖H57837", driver: "陈伟", qty: 80, shipped: 0 }
- ]
- }
- ]
- },
- {
- id: 4, created: today, shipped: 0, ctrl: "易制毒",
- buyNo: "G36260499510088", buyDate: "2026-09-28",
- buyer: "上饶市敏达精细化工有限公司", buyerLegal: "熊伟", buyerTel: "0793-8224115", buyerAddr: "上饶市胜利路96-3号",
- seller: "安庆市山峡化工有限公司", sellerLegal: "吴春生", sellerTel: "0556-5348918", sellerAddr: "安徽省安庆市高新区霞虹路5号",
- product: "硫酸", usage: "生产使用", buyQty: 220, unit: "吨", buyTimes: "多次有效",
- buyFrom: "2026-09-28", buyDue: "2026-12-27",
- buyPolice: "上饶市公安局信州分局禁毒大队", buyHandler: "潘佳", buyPoliceTel: "13755372147",
- buyFile: "购买证_敏达硫酸_2.jpg", buyUrl: "",
- transList: [
- {
- id: 6, created: today, shipped: 0,
- transNo: "Y36260455110076", transDate: "2026-09-28",
- owner: "上饶市敏达精细化工有限公司", ownerLegal: "熊伟", ownerTel: "0793-8224115",
- ownerAddr: "上饶市胜利路96-3号",
- pack: "槽罐", permitNo: "G36260499510088",
- carrier: "安庆宏泰危险货物运输有限公司", carrierAddr: "安徽省安庆市宜秀区中山大道/0556-5593789",
- mode: "汽车", plates: "皖HA3690、皖H57837、皖LB1283、皖K92881",
- route: "自安庆市高新区至上饶市信州区",
- consignee: "上饶市敏达精细化工有限公司", transQty: 220,
- transTimes: "多次有效", transFrom: "2026-09-28", transDue: "2026-12-27",
- applicantUnit: "上饶市敏达精细化工有限公司",
- transPolice: "上饶市公安局信州分局禁毒大队", transHandler: "潘佳",
- transFile: "运输证_敏达硫酸_2.jpg", transUrl: "",
- dispatches: [
- { plate: "皖HA3690", driver: "周建军", qty: 55, shipped: 0 },
- { plate: "皖H57837", driver: "陈伟", qty: 55, shipped: 0 },
- { plate: "皖LB1283", driver: "胡明", qty: 55, shipped: 0 },
- { plate: "皖K92881", driver: "吴涛", qty: 55, shipped: 0 }
- ]
- }
- ]
- }
- ];
- let plans = [
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" },
- { 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" }
- ];
- const planExtras = {
- 1: { deliverTime: "08:00", escort: "", contractNo: "SX/X-JT202508050191" },
- 2: { deliverTime: "08:30", escort: "李强", contractNo: "SX/X-JT202508050190" },
- 3: { deliverTime: "09:00", escort: "王磊", contractNo: "ZX-QH202609280354" },
- 4: { deliverTime: "10:00", escort: "赵鹏", contractNo: "SX/X-JT202508050117" },
- 5: { deliverTime: "13:00", escort: "孙磊", contractNo: "SX/X-JT202508050116" },
- 6: { deliverTime: "14:00", escort: "孙磊", contractNo: "SX/X-XY202508050109" },
- 7: { deliverTime: "07:30", escort: "李强", contractNo: "SX/X-JT20261003001" },
- 8: { deliverTime: "16:00", escort: "", contractNo: "SX/X-JT20261004001" },
- 9: { deliverTime: "09:30", escort: "王磊", contractNo: "SG-GX2026-01-0031" },
- 13: { deliverTime: "11:00", escort: "王磊", contractNo: "CG10120260930002" },
- 10: { deliverTime: "15:00", escort: "", contractNo: "SX/X-XY202508050083" },
- 11: { deliverTime: "08:20", escort: "孙磊", contractNo: "SX/X-XY202508050084" },
- 12: { deliverTime: "06:40", escort: "李强", contractNo: "SX/X-XY202508050082" },
- 14: { deliverTime: "08:00", escort: "", contractNo: "" },
- 15: { deliverTime: "10:00", escort: "", contractNo: "" },
- 16: { deliverTime: "08:00", escort: "", contractNo: "" },
- 17: { deliverTime: "09:00", escort: "孙磊", contractNo: "SX/X-YC202609200016" },
- 18: { deliverTime: "10:00", escort: "孙磊", contractNo: "SX/X-YC202610080018" },
- 19: { deliverTime: "11:00", escort: "", contractNo: "" },
- 20: { deliverTime: "09:30", escort: "王磊", contractNo: "CG10120261001020" },
- 21: { deliverTime: "13:00", escort: "李强", contractNo: "ZX-MD20261001021" },
- 22: { deliverTime: "14:00", escort: "赵鹏", contractNo: "SX/X-JT202610010022" },
- 23: { deliverTime: "08:40", escort: "李强", contractNo: "SX/X-JT202610010023" },
- 24: { deliverTime: "10:20", escort: "王磊", contractNo: "CG10120261001024" },
- 25: { deliverTime: "15:00", escort: "李强", contractNo: "ZX-MD20261001025" },
- 26: { deliverTime: "11:30", escort: "王磊", contractNo: "CG10120261001026" },
- 27: { deliverTime: "09:00", escort: "李强", contractNo: "ZX-MD20261001027" },
- 28: { deliverTime: "10:40", escort: "赵鹏", contractNo: "ZX-MD20261001028" },
- 29: { deliverTime: "14:20", escort: "李强", contractNo: "ZX-MD20261001029" },
- 30: { deliverTime: "16:00", escort: "王磊", contractNo: "ZX-MD20261001030" },
- 31: { deliverTime: "08:50", escort: "王磊", contractNo: "CG10120261001031" },
- 32: { deliverTime: "11:10", escort: "赵鹏", contractNo: "SX/X-JT202610010032" },
- 33: { deliverTime: "15:40", escort: "李强", contractNo: "ZX-MD20261001033" },
- 34: { deliverTime: "07:40", escort: "王磊", contractNo: "CG10120261001034" },
- 35: { deliverTime: "09:20", escort: "李强", contractNo: "SX/X-JT202610010035" },
- 36: { deliverTime: "12:10", escort: "赵鹏", contractNo: "ZX-MD20261001036" }
- };
- plans.forEach((p) => Object.assign(p, planExtras[p.id] || {}));
- const carrierFleet = [
- { company: "安庆宏泰危险货物运输有限公司", plate: "皖HA3690", driver: "周建军", cap: 35 },
- { company: "安庆宏泰危险货物运输有限公司", plate: "皖H57837", driver: "陈伟", cap: 35 },
- { company: "安庆宏泰危险货物运输有限公司", plate: "皖LB1283", driver: "胡明", cap: 32 },
- { company: "安庆宏泰危险货物运输有限公司", plate: "皖K92881", driver: "吴涛", cap: 32 },
- { company: "宜昌安达危运有限公司", plate: "鄂E·危A123", driver: "周建军", cap: 32 },
- { company: "宜昌安达危运有限公司", plate: "鄂E·危B618", driver: "陈伟", cap: 32 },
- { company: "宜昌安达危运有限公司", plate: "鄂E·危D077", driver: "吴涛", cap: 32 },
- { company: "宜昌安达危运有限公司", plate: "鄂E·危C902", driver: "胡明", cap: 30 },
- { company: "山峡自有车队", plate: "鄂E·危E441", driver: "郑浩", cap: 28 }
- ];
- const WARN_RATE = 0.1;
- function defaultWarnQty(total) {
- const n = Number(total);
- if (!Number.isFinite(n) || n <= 0) return 0;
- return Math.round(n * WARN_RATE * 100) / 100;
- }
- certs.forEach((c) => {
- if (c.warnQty == null || c.warnQty === "") c.warnQty = defaultWarnQty(c.buyQty);
- (c.transList || []).forEach((t) => {
- if (t.warnQty == null || t.warnQty === "") t.warnQty = defaultWarnQty(t.transQty);
- });
- });
- const labels = { dash: "总览", fleet: "车辆台账", fuel: "加油登记管理", fuelwarn: "油耗预警", etc: "ETC充值卡管理", oilcard: "加油充值卡管理", maint: "车辆维保管理", stats: "统计" };
- const fleetMenuView = { dash: "fuel", fleet: "fuel", fuel: "fuel", fuelwarn: "fuel", etc: "etc", oilcard: "oilcard", maint: "maint", stats: "fuel" };
- let currentFleetView = "etc";
- const FUEL_NORM = 33 / 100;
- const money = (n) => Number(n).toLocaleString("zh-CN", { maximumFractionDigits: 0 });
- const stClass = (s) => s === "运行" ? "st-run" : s === "维保中" ? "st-fix" : "st-stop";
- const applicants = ["吴九华", ...vehicles.map((v) => v.driver)].filter((n, i, a) => a.indexOf(n) === i);
- function fillPlates() {
- const fuelPlate = document.querySelector("#fuelForm [name=plate]");
- if (fuelPlate) fuelPlate.innerHTML = vehicles.map((v) => `<option value="${v.plate}">${hazPlate(v)}</option>`).join("");
- const plateOpts = `<option value="">请选择车牌</option>` + vehicles.map((v) => `<option value="${v.plate}">${hazPlate(v)}</option>`).join("");
- const maintPlate = document.querySelector("#maintForm [name=plate]");
- if (maintPlate) maintPlate.innerHTML = plateOpts;
- const etcPlate = document.querySelector("#etcForm [name=plate]");
- etcPlate.innerHTML = `<option value="">请选择自有车牌</option>` + vehicles.map((v) => `<option value="${v.plate}">${hazPlate(v)}</option>`).join("");
- const appOpts = applicants.map((n) => `<option value="${n}">${n}(${n})</option>`).join("");
- const etcApp = document.querySelector("#etcForm [name=applicant]");
- const oilApp = document.querySelector("#oilCardForm [name=applicant]");
- const maintApp = document.querySelector("#maintForm [name=applicant]");
- if (etcApp) etcApp.innerHTML = appOpts;
- if (oilApp) oilApp.innerHTML = appOpts;
- if (maintApp) maintApp.innerHTML = appOpts;
- const oilPlate = document.querySelector("#oilCardForm [name=plate]");
- if (oilPlate) oilPlate.innerHTML = `<option value="">请选择自有车牌</option>` + vehicles.map((v) => `<option value="${v.plate}">${hazPlate(v)}</option>`).join("");
- document.querySelector("#fuelForm [name=date]").value = today;
- document.querySelector("#etcForm [name=date]").value = today;
- const oilDate = document.querySelector("#oilCardForm [name=date]");
- if (oilDate) oilDate.value = today;
- const applyDate = document.querySelector("#maintForm [name=applyDate]");
- const inDate = document.querySelector("#maintForm [name=inDate]");
- if (applyDate) applyDate.value = today;
- if (inDate) inDate.value = today;
- }
- function daysTo(d) {
- return Math.ceil((new Date(d) - new Date(today)) / 86400000);
- }
- function showView(name) {
- currentFleetView = name;
- ["dash", "fleet", "fuel", "fuelwarn", "etc", "oilcard", "maint", "stats"].forEach((v) => {
- document.getElementById("view-" + v).classList.toggle("hidden", v !== name);
- });
- document.querySelectorAll("#subnav button").forEach((b) => b.classList.toggle("on", b.dataset.view === name));
- setCrumb("车辆管理", labels[name]);
- markNav(`[data-fleet-view="${fleetMenuView[name] || "etc"}"]`);
- if (name === "dash") renderDash();
- if (name === "fleet") renderFleet();
- if (name === "fuel") renderFuel();
- if (name === "fuelwarn") renderFuelWarn();
- if (name === "etc") renderEtc();
- if (name === "oilcard") renderOilCard();
- if (name === "maint") renderMaint();
- if (name === "stats") renderStats();
- }
- function toast(msg) {
- const el = document.getElementById("toast");
- el.textContent = msg;
- el.classList.add("show");
- setTimeout(() => el.classList.remove("show"), 1800);
- }
- function fuelSegments() {
- const byPlate = {};
- fuels.forEach((f) => {
- (byPlate[f.plate] ||= []).push(f);
- });
- const rows = [];
- vehicles.forEach((v) => {
- const list = (byPlate[v.plate] || []).slice().sort((a, b) => a.km - b.km || a.date.localeCompare(b.date));
- if (list.length < 2) {
- rows.push({
- plate: v.plate,
- driver: v.driver,
- fromDate: list[0]?.date || "—",
- toDate: list[0]?.date || "—",
- fromKm: list[0]?.km || v.km,
- toKm: list[0]?.km || v.km,
- km: 0,
- liters: list[0]?.liters || 0,
- rate: null,
- per100: null,
- status: list.length ? "不足" : "无记录"
- });
- return;
- }
- for (let i = 1; i < list.length; i++) {
- const prev = list[i - 1];
- const cur = list[i];
- const km = cur.km - prev.km;
- const liters = Number(cur.liters);
- const rate = km > 0 ? liters / km : null;
- let status = "正常";
- if (km <= 0) status = "异常";
- else if (rate > FUEL_NORM) status = "预警";
- rows.push({
- plate: v.plate,
- driver: v.driver,
- fromDate: prev.date,
- toDate: cur.date,
- fromKm: prev.km,
- toKm: cur.km,
- km,
- liters,
- rate,
- per100: rate == null ? null : rate * 100,
- status
- });
- }
- });
- const order = { 预警: 0, 异常: 1, 不足: 2, 无记录: 3, 正常: 4 };
- return rows.sort((a, b) => order[a.status] - order[b.status] || a.plate.localeCompare(b.plate));
- }
- function fuelWarnCount() {
- return fuelSegments().filter((r) => r.status === "预警").length;
- }
- function renderDash() {
- const due = vehicles.filter((v) => daysTo(v.tankDue) <= 20 || daysTo(v.insDue) <= 20);
- const fixing = vehicles.filter((v) => v.status === "维保中").length;
- const warnN = fuelWarnCount();
- document.getElementById("dashAlerts").innerHTML = `
- <div class="alert red" style="cursor:pointer" onclick="showView('fuelwarn')">油耗超标预警 ${warnN} 条(标准 0.33 L/km)</div>
- <div class="alert red">罐检 / 保险 20 日内到期 ${due.length} 辆</div>
- <div class="alert">维保中 ${fixing} 辆,注意出厂登记</div>
- <div class="alert ok">本月已登加油 ${fuels.length} 笔</div>`;
- const fuelSum = fuels.reduce((s, x) => s + x.amount, 0);
- const maintOpen = maints.filter((m) => !m.outDate).length;
- document.getElementById("dashStats").innerHTML = [
- ["在册车辆", vehicles.length, `运行 ${vehicles.filter(v=>v.status==="运行").length} · 维保 ${fixing}`],
- ["本月加油(元)", money(fuelSum), `${fuels.reduce((s,x)=>s+x.liters,0)} 升`],
- ["未出厂维保", maintOpen, `记录 ${maints.length} 条`],
- ["本月 ETC 申请", etcs.length, `已提交 ${etcs.filter(x=>x.status==="已提交").length} · 草稿 ${etcs.filter(x=>x.status==="草稿").length}`]
- ].map(([k,v,s]) => `<div class="stat"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");
- document.getElementById("dashFleet").innerHTML = `<table><thead><tr><th>车牌</th><th>介质</th><th>驾驶员</th><th>状态</th><th>罐检到期</th></tr></thead><tbody>${
- 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("")
- }</tbody></table>`;
- const events = [
- ...fuels.map((x) => ({ date: x.date, title: "加油 " + x.plate, d: x.liters + " 升 · " + x.station })),
- ...etcs.map((x) => ({ date: x.date, title: "ETC申请 " + x.plate, d: x.applicant + " · 期初 " + x.km + " km" })),
- ...maints.map((x) => ({ date: x.inDate, title: x.type + " " + x.plate, d: x.item }))
- ].sort((a, b) => b.date.localeCompare(a.date)).slice(0, 6);
- 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("");
- const byPlate = {};
- fuels.forEach((f) => { byPlate[f.plate] = (byPlate[f.plate] || 0) + f.liters; });
- const max = Math.max(...Object.values(byPlate), 1);
- document.getElementById("dashBars").innerHTML = Object.entries(byPlate).map(([p, n]) =>
- `<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>`
- ).join("");
- const sb = document.getElementById("sideBadge");
- if (sb) sb.textContent = due.length + maintOpen + warnN;
- }
- function renderFleet() {
- const q = (document.getElementById("fleetQ").value || "").trim();
- const st = document.getElementById("fleetSt").value;
- const rows = vehicles.filter((v) => (!q || v.plate.includes(q) || v.driver.includes(q)) && (!st || v.status === st));
- 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>${
- 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("")
- }</tbody></table>`;
- }
- function attachListOf(source) {
- if (source === "etc") return etcs;
- if (source === "oilcard") return oilcards;
- if (source === "maint") return maints;
- return fuels;
- }
- function attachChips(source, rowIndex) {
- const items = (attachListOf(source)[rowIndex] || {}).attachments || [];
- if (!items.length) return `<span class="st st-due">缺附件</span>`;
- return `<div class="chips">${items.map((a, i) =>
- a.url
- ? `<button class="chip" type="button" onclick="openAttach('${source}',${rowIndex},${i})">${a.name}</button>`
- : `<span class="chip ghost" title="历史存档">${a.name}</span>`
- ).join("")}</div>`;
- }
- function ymShift(ym, n) {
- const [y, m] = String(ym).split("-").map(Number);
- const d = new Date(y, m - 1 + n, 1);
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
- }
- function fuelsInMonth(plate, ym) {
- return fuels.filter((f) => f.plate === plate && String(f.date).slice(0, 7) === ym);
- }
- function lastFuelKm(rows) {
- const list = rows.filter((f) => f.km != null && f.km !== "").slice().sort((a, b) => String(a.date).localeCompare(String(b.date)));
- return list.length ? Number(list[list.length - 1].km) : null;
- }
- function monthRunKm(plate, ym) {
- const cur = lastFuelKm(fuelsInMonth(plate, ym));
- const prev = lastFuelKm(fuelsInMonth(plate, ymShift(ym, -1)));
- if (cur != null && prev != null) return Math.max(0, cur - prev);
- const rows = fuelsInMonth(plate, ym).filter((f) => f.km != null).slice().sort((a, b) => String(a.date).localeCompare(String(b.date)));
- if (rows.length >= 2) return Math.max(0, Number(rows[rows.length - 1].km) - Number(rows[0].km));
- return 0;
- }
- function litersMomHtml(cur, prev) {
- if (!prev) return `<em class="mom flat">环比 —</em>`;
- const pct = ((cur - prev) / prev) * 100;
- if (Math.abs(pct) < 0.05) return `<em class="mom flat">环比持平</em>`;
- const up = pct > 0;
- return `<em class="mom ${up ? "up" : "down"}">环比${up ? "↑" : "↓"}${Math.abs(pct).toFixed(1)}%</em>`;
- }
- function renderFuel() {
- const ym = String(today).slice(0, 7);
- const prevYm = ymShift(ym, -1);
- const box = document.getElementById("fuelPlateCards");
- if (box) {
- box.innerHTML = vehicles.map((v) => {
- const rows = fuelsInMonth(v.plate, ym);
- const prevRows = fuelsInMonth(v.plate, prevYm);
- const liters = rows.reduce((s, f) => s + Number(f.liters || 0), 0);
- const prevLiters = prevRows.reduce((s, f) => s + Number(f.liters || 0), 0);
- const runKm = monthRunKm(v.plate, ym);
- const prevKm = monthRunKm(v.plate, prevYm);
- return `<article class="fuel-plate-card" onclick="openFuelDlg('${v.plate}')">
- <div class="plate">${hazPlate(v)}</div>
- <div class="meta">${v.driver || "—"} · ${v.type || ""} · <span class="st ${stClass(v.status)}">${v.status}</span></div>
- <div class="nums">
- <div><b>${rows.length}</b><span>当月笔数</span>${litersMomHtml(rows.length, prevRows.length)}</div>
- <div><b>${money(runKm)}</b><span>当月公里</span>${litersMomHtml(runKm, prevKm)}</div>
- <div><b>${fmtTon(liters)}</b><span>升</span>${litersMomHtml(liters, prevLiters)}</div>
- </div>
- </article>`;
- }).join("");
- }
- 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>${
- 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("")
- }</tbody></table>`;
- }
- function openFuelDlg(plate) {
- const form = document.getElementById("fuelForm");
- form.reset();
- document.getElementById("fuelFileChips").innerHTML = "";
- fillPlates();
- if (plate) form.plate.value = plate;
- const v = vehicles.find((x) => x.plate === form.plate.value);
- if (v && form.km) form.km.value = v.km;
- document.getElementById("fuelDialog").showModal();
- }
- function closeFuelDlg() {
- document.getElementById("fuelDialog").close();
- }
- function fmtRate(n, digits) {
- return n == null ? "—" : n.toFixed(digits);
- }
- function renderFuelWarn() {
- const q = (document.getElementById("fuelwarnQ").value || "").trim();
- const st = document.getElementById("fuelwarnSt").value;
- const all = fuelSegments();
- const warnN = all.filter((r) => r.status === "预警").length;
- const okN = all.filter((r) => r.status === "正常").length;
- const lackN = all.filter((r) => r.status === "不足" || r.status === "无记录" || r.status === "异常").length;
- const rates = all.filter((r) => r.rate != null).map((r) => r.rate);
- const maxRate = rates.length ? Math.max(...rates) : 0;
- document.getElementById("fuelwarnKpis").innerHTML = [
- ["标准油耗", "0.33 L/km", "33 ÷ 100"],
- ["超标预警", warnN, "相邻加油区间"],
- ["正常区间", okN, "未超过标准"],
- ["数据不足 / 异常", lackN, "少于两次加油或里程未增"]
- ].map(([k, v, s]) => `<div class="stat"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");
- const latestByPlate = {};
- all.filter((r) => r.rate != null).forEach((r) => {
- const prev = latestByPlate[r.plate];
- if (!prev || r.toDate > prev.toDate) latestByPlate[r.plate] = r;
- });
- const barMax = Math.max(maxRate, FUEL_NORM, 0.01);
- document.getElementById("fuelwarnBars").innerHTML = Object.values(latestByPlate).map((r) => {
- const over = r.rate > FUEL_NORM;
- 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>`;
- }).join("") || `<div style="color:var(--muted)">暂无可对照的加油区间</div>`;
- const rows = all.filter((r) => (!q || r.plate.includes(q) || hazPlate(r.plate).includes(q)) && (!st || (st === "不足" ? ["不足", "无记录", "异常"].includes(r.status) : r.status === st)));
- 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>`;
- 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>${
- rows.map((r) => {
- const over = r.status === "预警";
- const delta = r.rate == null ? "—" : (r.rate - FUEL_NORM >= 0 ? "+" : "") + (r.rate - FUEL_NORM).toFixed(3);
- 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>`;
- }).join("") || `<tr><td colspan="10" style="color:var(--muted)">没有符合条件的记录</td></tr>`
- }</tbody></table>`;
- }
- function previewFuelFiles(input) {
- const box = document.getElementById("fuelFileChips");
- const files = [...input.files];
- box.innerHTML = files.length
- ? files.map((f) => `<span class="chip ghost">${f.name}</span>`).join("")
- : "";
- }
- function readFiles(fileList) {
- return Promise.all([...fileList].map((file) => new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => resolve({ name: file.name, type: file.type, url: reader.result });
- reader.onerror = reject;
- reader.readAsDataURL(file);
- })));
- }
- function openAttach(source, rowIndex, fileIndex) {
- const file = source === "task"
- ? ((TASKS.find((x) => x.id === rowIndex) || {}).sheetPhotos || [])[fileIndex]
- : (attachListOf(source)[rowIndex].attachments || [])[fileIndex];
- if (!file) return;
- document.getElementById("attachTitle").textContent = file.name;
- const body = document.getElementById("attachBody");
- if (file.type && file.type.startsWith("image/") && file.url) {
- body.innerHTML = `<img alt="${file.name}" src="${file.url}" />`;
- } else if (file.url) {
- 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>`;
- } else {
- body.innerHTML = `<p>该附件已随历史记录存档,当前演示环境未保留原文件内容。</p>`;
- }
- document.getElementById("attachDialog").showModal();
- }
- function closeAttach() {
- document.getElementById("attachDialog").close();
- }
- function etcsInMonth(plate, ym) {
- return etcs.filter((x) =>
- x.plate === plate &&
- String(x.date).slice(0, 7) === ym &&
- x.status !== "草稿" &&
- (x.type === "ETC" || !x.type)
- );
- }
- function renderEtc() {
- const ym = String(today).slice(0, 7);
- const prevYm = ymShift(ym, -1);
- const box = document.getElementById("etcPlateCards");
- if (box) {
- box.innerHTML = vehicles.map((v) => {
- const rows = etcsInMonth(v.plate, ym);
- const prevRows = etcsInMonth(v.plate, prevYm);
- const pay = rows.reduce((s, x) => s + Number(x.amount || 0), 0);
- const prevPay = prevRows.reduce((s, x) => s + Number(x.amount || 0), 0);
- const used = rows.reduce((s, x) => s + Number(x.used || 0), 0);
- const prevUsed = prevRows.reduce((s, x) => s + Number(x.used || 0), 0);
- const runKm = monthRunKm(v.plate, ym);
- const prevKm = monthRunKm(v.plate, prevYm);
- return `<article class="fuel-plate-card" onclick="openEtcDlg('${v.plate}')">
- <div class="plate">${hazPlate(v)}</div>
- <div class="meta">${v.driver || "—"} · ${v.type || ""} · <span class="st ${stClass(v.status)}">${v.status}</span></div>
- <div class="nums four">
- <div><b>${rows.length}</b><span>充值次数</span>${litersMomHtml(rows.length, prevRows.length)}</div>
- <div><b>${money(runKm)}</b><span>当月公里</span>${litersMomHtml(runKm, prevKm)}</div>
- <div><b>${money(pay)}</b><span>充值金额</span>${litersMomHtml(pay, prevPay)}</div>
- <div><b>${money(used)}</b><span>使用金额</span>${litersMomHtml(used, prevUsed)}</div>
- </div>
- </article>`;
- }).join("");
- }
- 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>${
- 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("")
- }</tbody></table>`;
- }
- function previewMaintFiles(input) {
- const box = document.getElementById("maintFileChips");
- const files = [...input.files];
- box.innerHTML = files.length
- ? files.map((f) => `<span class="chip ghost">${f.name}</span>`).join("")
- : "";
- }
- function openMaintDlg(plate) {
- const form = document.getElementById("maintForm");
- form.reset();
- document.getElementById("maintFileChips").innerHTML = "";
- fillPlates();
- if (plate) form.plate.value = plate;
- document.getElementById("maintDialog").showModal();
- }
- function closeMaintDlg() {
- document.getElementById("maintDialog").close();
- }
- function focusMaintPlate(plate) {
- openMaintDlg(plate);
- }
- function renderMaint() {
- const box = document.getElementById("maintPlateCards");
- const yr = String(today).slice(0, 4);
- const ym = String(today).slice(0, 7);
- if (box) {
- box.innerHTML = vehicles.map((v) => {
- const yearRows = maints.filter((m) => m.plate === v.plate && String(m.inDate).slice(0, 4) === yr);
- const monthRows = yearRows.filter((m) => String(m.inDate).slice(0, 7) === ym);
- const yearAmt = yearRows.reduce((s, m) => s + Number(m.amount || 0), 0);
- const monthAmt = monthRows.reduce((s, m) => s + Number(m.amount || 0), 0);
- return `<article class="fuel-plate-card" onclick="focusMaintPlate('${v.plate}')">
- <div class="plate">${hazPlate(v)}</div>
- <div class="meta">${v.driver || "—"} · ${v.type || ""} · <span class="st ${stClass(v.status)}">${v.status}</span></div>
- <div class="nums">
- <div><b>${yearRows.length}</b><span>本年度维修次数</span></div>
- <div><b>${money(monthAmt)}</b><span>本月合计</span></div>
- <div><b>${money(monthAmt)} / ${money(yearAmt)}</b><span>本月 / 本年度合计</span></div>
- </div>
- </article>`;
- }).join("");
- }
- 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>${
- 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("")
- }</tbody></table>`;
- }
- function renderStats() {
- const fuelAmt = fuels.reduce((s, x) => s + Number(x.amount), 0);
- const maintAmt = maints.reduce((s, x) => s + Number(x.amount), 0);
- const liters = fuels.reduce((s, x) => s + Number(x.liters), 0);
- document.getElementById("statKpis").innerHTML = [
- ["加油升数", liters, "本月已登记"],
- ["加油费用", money(fuelAmt) + " 元", ""],
- ["维保费用", money(maintAmt) + " 元", "含未出厂"],
- ["ETC 申请", etcs.length, `已提交 ${etcs.filter(x=>x.status==="已提交").length} 笔`]
- ].map(([k,v,s]) => `<div class="stat"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s || " "}</div></div>`).join("");
- const byPlate = {};
- fuels.forEach((f) => { byPlate[f.plate] = (byPlate[f.plate] || 0) + f.liters; });
- const max = Math.max(...Object.values(byPlate), 1);
- document.getElementById("statBars").innerHTML = Object.entries(byPlate).map(([p, n]) =>
- `<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>`
- ).join("");
- const total = fuelAmt + maintAmt || 1;
- const segs = [
- { name: "加油", n: fuelAmt, c: "#148a3c" },
- { name: "维保", n: maintAmt, c: "#c2410c" }
- ];
- let acc = 0;
- const circ = 2 * Math.PI * 10;
- document.getElementById("pie").innerHTML = segs.map((s) => {
- const len = (s.n / total) * circ;
- const dash = `${len} ${circ}`;
- const rot = -90 + acc * 360;
- acc += s.n / total;
- return `<circle r="10" cx="16" cy="16" fill="none" stroke="${s.c}" stroke-width="4" stroke-dasharray="${dash}" transform="rotate(${rot} 16 16)"/>`;
- }).join("");
- document.getElementById("pieLegend").innerHTML = segs.map((s) =>
- `<span><i class="sw" style="background:${s.c}"></i>${s.name} ${money(s.n)} 元</span>`
- ).join("");
- document.getElementById("certTable").innerHTML = `<table><thead><tr><th>车牌</th><th>罐检到期</th><th>剩余</th><th>保险到期</th><th>剩余</th></tr></thead><tbody>${
- 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("")
- }</tbody></table>`;
- }
- function addFuel(e) {
- e.preventDefault();
- const input = e.target.querySelector("[name=attach]");
- if (!input.files || !input.files.length) {
- toast("请上传加油小票或发票");
- return false;
- }
- const fd = new FormData(e.target);
- const f = Object.fromEntries(fd);
- delete f.attach;
- readFiles(input.files).then((attachments) => {
- fuels.unshift({ ...f, liters: +f.liters, amount: +f.amount, km: +f.km, attachments });
- const v = vehicles.find((x) => x.plate === f.plate);
- if (v) v.km = +f.km;
- toast("加油已登记,附件已保存");
- e.target.reset();
- document.getElementById("fuelFileChips").innerHTML = "";
- fillPlates();
- closeFuelDlg();
- renderFuel();
- renderDash();
- if (!document.getElementById("view-fuelwarn").classList.contains("hidden")) renderFuelWarn();
- });
- return false;
- }
- function previewEtcFiles(input) {
- const box = document.getElementById("etcFileChips");
- const files = [...input.files];
- box.innerHTML = files.length
- ? files.map((f) => `<span class="chip ghost">${f.name}</span>`).join("")
- : "";
- }
- function stepKm(delta) {
- const el = document.querySelector("#etcForm [name=km]");
- el.value = Math.max(0, (+el.value || 0) + delta);
- }
- function syncEtcKm() {
- const plate = document.querySelector("#etcForm [name=plate]").value;
- const v = vehicles.find((x) => x.plate === plate);
- if (v) document.querySelector("#etcForm [name=km]").value = v.km;
- }
- function openEtcDlg(plate) {
- const form = document.getElementById("etcForm");
- form.reset();
- document.getElementById("etcFileChips").innerHTML = "";
- fillPlates();
- if (plate) form.plate.value = plate;
- syncEtcKm();
- document.getElementById("etcDialog").showModal();
- }
- function closeEtcDlg() {
- document.getElementById("etcDialog").close();
- }
- function saveEtc(submit) {
- const form = document.getElementById("etcForm");
- if (!form.reportValidity()) return;
- const input = form.querySelector("[name=attach]");
- if (!input.files || !input.files.length) {
- toast("请上传附件");
- return;
- }
- const f = Object.fromEntries(new FormData(form));
- delete f.attach;
- readFiles(input.files).then((attachments) => {
- etcs.unshift({ ...f, km: +f.km, amount: +f.amount || 0, used: +f.used || 0, attachments, status: submit ? "已提交" : "草稿" });
- toast(submit ? "ETC 申请已提交" : "ETC 申请已保存");
- closeEtcDlg();
- renderEtc();
- renderDash();
- });
- }
- function addMaint(e) {
- e.preventDefault();
- const form = document.getElementById("maintForm");
- if (!form.reportValidity()) return false;
- const f = Object.fromEntries(new FormData(form));
- delete f.attach;
- const input = form.querySelector("[name=attach]");
- const save = (attachments) => {
- maints.unshift({
- ...f,
- amount: +f.amount || 0,
- nextKm: f.nextKm ? +f.nextKm : "",
- attachments: attachments || []
- });
- const v = vehicles.find((x) => x.plate === f.plate);
- if (v && !f.outDate) v.status = "维保中";
- toast("维保申请已提交");
- closeMaintDlg();
- renderMaint();
- renderDash();
- };
- if (input && input.files && input.files.length) readFiles(input.files).then(save);
- else save([]);
- return false;
- }
- function transOf(c) { return c.transList || []; }
- function findTrans(c, transId) { return transOf(c).find((t) => t.id === transId) || null; }
- function isPlanDone(p) {
- return !!(p && (p.status === "已完成" || p.archived));
- }
- function deductQty(p) {
- if (!p || p.status === "已作废" || !isPlanDone(p)) return 0;
- return Number(p.realQty != null ? p.realQty : p.planQty || 0);
- }
- function holdQty(p) {
- if (!p || p.status === "已作废" || isPlanDone(p)) return 0;
- return Number(p.planQty || 0);
- }
- function occupyQty(p) {
- return deductQty(p) + holdQty(p);
- }
- function warnQtyOf(v, total) {
- if (v !== "" && v != null) {
- const n = Number(v);
- if (Number.isFinite(n) && n >= 0) return n;
- }
- return defaultWarnQty(total);
- }
- function buyWarnQty(c) { return warnQtyOf(c && c.warnQty, c && c.buyQty); }
- function transWarnQty(t) { return warnQtyOf(t && t.warnQty, t && t.transQty); }
- function warnQtyField(certId, transId, unit) {
- const c = certs.find((x) => x.id === certId);
- const v = transId == null ? buyWarnQty(c) : transWarnQty(c ? findTrans(c, transId) : null);
- const tid = transId == null ? "null" : transId;
- 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>`;
- }
- function ensureWarnQtyInputs(root) {
- if (!root) return;
- root.querySelectorAll("input[name=warnQty]").forEach((el) => {
- const box = el.closest(".form") || el.closest(".extra-trans") || el.closest("#buyFormCol") || root;
- const qtyEl = box.querySelector("[name=buyQty]") || box.querySelector("[name=transQty]");
- if (el.value === "" || el.value == null) el.value = defaultWarnQty(qtyEl && qtyEl.value);
- });
- }
- function setCertWarnQty(certId, transId, val) {
- const c = certs.find((x) => x.id === certId);
- if (!c) return;
- if (val === "" || val == null) return;
- const n = Number(val);
- if (!Number.isFinite(n) || n < 0) {
- toast("请输入有效预警量");
- if (currentCertId === certId) renderCertDetail();
- return;
- }
- if (transId == null || transId === "") {
- c.warnQty = n;
- toast(`购买证预警量已设为 ${fmtTon(n)}${c.unit || "吨"}`);
- } else {
- const t = findTrans(c, +transId);
- if (!t) return;
- t.warnQty = n;
- toast(`运输证 ${t.transNo || ""} 预警量已设为 ${fmtTon(n)}${c.unit || "吨"}`);
- }
- if (currentCertId === certId) renderCertDetail();
- renderCertList();
- updateBadges();
- }
- function lowRemain(g, warn) {
- const w = warnQtyOf(warn);
- return g > 0 && g <= w + 1e-9;
- }
- function chemKey(s) {
- const t = String(s || "");
- if (/硝酸铵|硝铵/.test(t)) return "硝酸铵";
- if (/盐酸/.test(t)) return "盐酸";
- if (/硫酸|试剂酸/.test(t)) return "硫酸";
- if (/槽车运输/.test(t)) return "硫酸";
- return t.replace(/\d+(\.\d+)?%/g, "").replace(/工业|试剂/g, "").trim();
- }
- function planMatchesCert(p, c) {
- if (!p || !c) return false;
- if (p.ctrl && c.ctrl && p.ctrl !== c.ctrl) return false;
- const a = chemKey(p.product);
- const b = chemKey(c.product);
- if (!a || !b) return true;
- return a === b || a.includes(b) || b.includes(a);
- }
- function normOrg(s) {
- return String(s || "").replace(/\s+/g, "").replace(/有限责任公司|股份有限公司|有限公司|集团|公司/g, "").toLowerCase();
- }
- function orgMatch(a, b) {
- const x = normOrg(a), y = normOrg(b);
- if (!x || !y) return false;
- return x === y || x.includes(y) || y.includes(x);
- }
- function isShanxiaOrg(s) {
- const t = String(s || "");
- return /山峡化工|山峡/.test(t) || orgMatch(t, "安庆市山峡化工有限公司");
- }
- function planCertScene(p) {
- if (isInboundPlan(p)) return "in";
- if (isDirectSale(p)) return "direct";
- return "out";
- }
- function planSceneLabel(scene) {
- return scene === "in" ? "入库单" : scene === "direct" ? "直销单" : "出库单";
- }
- function planLoadParty(p) {
- return (p && p.loadFrom) || "";
- }
- function planShipParty(p) {
- if (!p) return "";
- if (isInboundPlan(p)) return p.settler || p.customer || "安庆市山峡化工有限公司";
- return p.customer || p.settler || "";
- }
- function planCustName(p) {
- return planShipParty(p);
- }
- function certBuyerMatch(p, c) {
- return certIsShipBuy(p, c);
- }
- function certIsShipBuy(p, c) {
- if (!p || !c) return false;
- if (!planMatchesCert(p, c)) return false;
- return orgMatch(planShipParty(p), c.buyer);
- }
- function certIsProcure(p, c) {
- if (!p || !c) return false;
- if (!planMatchesCert(p, c)) return false;
- if (orgMatch(planLoadParty(p), c.buyer)) return true;
- return isShanxiaOrg(c.buyer) && orgMatch(planLoadParty(p), c.seller);
- }
- function planNeedProcureCert(p) {
- return isDirectSale(p);
- }
- function planCertLinked(p) {
- if (!p) return false;
- if (!p.certId) return false;
- if (planNeedProcureCert(p) && (!p.shipCertId || !p.procureCertId)) return false;
- return true;
- }
- function isWaitListPlan(p) {
- if (!p || p.status !== "已安排") return false;
- if (p.ctrl !== "易制毒") return false;
- return !!String(p.tractor || "").trim();
- }
- function isWaitLinkPlan(p) {
- if (!isWaitListPlan(p)) return false;
- return !p.certId || !p.transId || (planNeedProcureCert(p) && (!p.shipCertId || !p.procureCertId));
- }
- function waitPlanDay(p) {
- return String((p && (p.deliverDate || p.date)) || "");
- }
- function waitWeekStart(dayStr) {
- const d = new Date(String(dayStr) + "T00:00:00");
- if (Number.isNaN(+d)) return 0;
- const wd = d.getDay();
- d.setDate(d.getDate() - (wd === 0 ? 6 : wd - 1));
- d.setHours(0, 0, 0, 0);
- return +d;
- }
- function isWaitTodayPlan(p) {
- return waitPlanDay(p) === today;
- }
- function isWaitWeekPlan(p) {
- const day = waitPlanDay(p);
- if (!day) return false;
- const t = Date.parse(day + "T00:00:00");
- if (!Number.isFinite(t)) return false;
- const w0 = waitWeekStart(today);
- return t >= w0 && t < w0 + 7 * 86400000;
- }
- let waitLinkSceneTab = "";
- function waitListSort(a, b) {
- const sa = planCertScene(a), sb = planCertScene(b);
- const order = { in: 0, out: 1, direct: 2 };
- if (order[sa] !== order[sb]) return order[sa] - order[sb];
- const aToday = (a.date === today || a.deliverDate === today) ? 0 : 1;
- const bToday = (b.date === today || b.deliverDate === today) ? 0 : 1;
- if (aToday !== bToday) return aToday - bToday;
- return String(a.deliverDate || a.date).localeCompare(String(b.deliverDate || b.date));
- }
- function waitListPlans() {
- return plans.filter((p) => isWaitListPlan(p) && (!waitLinkSceneTab || planCertScene(p) === waitLinkSceneTab)).sort(waitListSort);
- }
- function waitLinkPlans() {
- return plans.filter((p) => isWaitLinkPlan(p) && (!waitLinkSceneTab || planCertScene(p) === waitLinkSceneTab)).sort(waitListSort);
- }
- function setWaitLinkScene(tab) {
- waitLinkSceneTab = tab || "";
- renderWaitPlans();
- }
- function platesOfCert(c) {
- const set = new Set();
- transOf(c).forEach((t) => platesOfTrans(t).forEach((pl) => set.add(pl)));
- return [...set];
- }
- function transForPlate(c, plate) {
- if (!c || !plate || certVoided(c)) return null;
- const hits = transOf(c).filter((t) => transSelectable(c, t) && (platesOfTrans(t).includes(plate) || String(t.plates || "").includes(plate)));
- return hits[0] || null;
- }
- function suggestVehicleFromCert(c, p) {
- if (!c) return "";
- const plates = platesOfCert(c);
- if (p && p.tractor && plates.includes(p.tractor)) return p.tractor;
- const ds = allDispatches(c);
- let best = "", bestLeft = -Infinity;
- ds.forEach((d) => {
- const left = Number(d.qty || 0) - vehicleHold(d.plate, d.transId);
- if (left > bestLeft) { bestLeft = left; best = d.plate; }
- });
- return best || plates[0] || (p && p.tractor) || "";
- }
- function rankCertForPlan(p, pred) {
- return certs.map((c) => {
- let score = 0;
- const hit = pred(p, c);
- const match = planMatchesCert(p, c);
- if (hit) score += 10;
- if (match) score += 3;
- if (p.ctrl && c.ctrl && p.ctrl === c.ctrl) score += 2;
- if (remainGap(c) + 1e-9 >= occupyQty(p)) score += 1;
- if (transOf(c).some((x) => transItemRemain(x) + 1e-9 >= occupyQty(p))) score += 1;
- if (transOf(c).some((x) => assignedSum(x) > 1e-9)) score += 4;
- if (transOf(c).some((x) => transAssignLeft(x) + 1e-9 >= occupyQty(p))) score += 5;
- if (p.tractor && transOf(c).some((t) => transSelectable(c, t) && platesOfTrans(t).includes(p.tractor))) score += 4;
- if (certVoided(c)) score -= 50;
- return { c, score, hit };
- }).sort((a, b) => b.score - a.score);
- }
- function suggestShipCert(p) {
- const hit = rankCertForPlan(p, certIsShipBuy).find((x) => x.hit && certSelectable(x.c));
- return (hit && hit.c) || null;
- }
- function suggestProcureCert(p) {
- const hit = rankCertForPlan(p, certIsProcure).find((x) => x.hit && certSelectable(x.c));
- return (hit && hit.c) || null;
- }
- function suggestCertForPlan(p) {
- if (planNeedProcureCert(p)) return suggestProcureCert(p) || suggestShipCert(p);
- return suggestShipCert(p);
- }
- function suggestTransForPlan(c, p) {
- const list = transOf(c).filter((t) => transSelectable(c, t));
- if (!list.length) return null;
- if (p && p.transId) {
- const hit = findTrans(c, p.transId);
- if (hit && transSelectable(c, hit)) return hit;
- }
- if (p && p.tractor) {
- const hit = transForPlate(c, p.tractor);
- if (hit) return hit;
- }
- const qty = occupyQty(p);
- return list.find((t) => transItemRemain(t) + 1e-9 >= qty) || list[0];
- }
- function suggestVehicleForPlan(t, p) {
- if (!t) return "";
- const plates = platesOfTrans(t);
- if (p && p.tractor && (plates.includes(p.tractor) || String(t.plates || "").includes(p.tractor))) return p.tractor;
- const ds = t.dispatches || [];
- let best = "", bestLeft = -Infinity;
- ds.forEach((d) => {
- const left = Number(d.qty || 0) - vehicleHold(d.plate, t.id);
- if (left > bestLeft) { bestLeft = left; best = d.plate; }
- });
- return best || plates[0] || (p && p.tractor) || "";
- }
- function waitPickOf(p) {
- const saved = waitLinkPicks[p.id] || {};
- const scene = planCertScene(p);
- const shipC = (saved.shipCertId && certs.find((x) => x.id === saved.shipCertId)) || suggestShipCert(p);
- let c = (saved.certId && certs.find((x) => x.id === saved.certId)) || null;
- if (!c) c = planNeedProcureCert(p) ? suggestProcureCert(p) : shipC;
- if (!c) return { c: null, t: null, plate: "", shipC, scene, matchCust: false, matchShip: !!shipC && certIsShipBuy(p, shipC), matchProcure: !planNeedProcureCert(p), matchVeh: false };
- const certPlates = platesOfCert(c);
- let plate = saved.plate || "";
- if (plate && !certPlates.includes(plate) && !transForPlate(c, plate)) plate = "";
- if (!plate) plate = suggestVehicleFromCert(c, p);
- let t = saved.transId ? findTrans(c, saved.transId) : null;
- if (plate) {
- const byPlate = transForPlate(c, plate);
- if (byPlate && !saved.transId) t = byPlate;
- if (t && plate && !platesOfTrans(t).includes(plate) && !String(t.plates || "").includes(plate)) t = byPlate || t;
- }
- if (!t) t = suggestTransForPlan(c, p);
- if (t && plate && !platesOfTrans(t).includes(plate) && !String(t.plates || "").includes(plate) && saved.plate == null) {
- plate = suggestVehicleForPlan(t, p);
- }
- const matchShip = !!(shipC && certIsShipBuy(p, shipC));
- const matchProcure = !planNeedProcureCert(p) || !!(c && certIsProcure(p, c));
- return {
- c, t, plate, shipC, scene,
- matchCust: matchShip,
- matchShip, matchProcure,
- matchVeh: !!(plate && t && (platesOfTrans(t).includes(plate) || String(t.plates || "").includes(plate)))
- };
- }
- function afterLinkCalc(c, t, plate, qty, skipId, shipC) {
- const buyAfter = remainGap(c, skipId) - qty;
- const transAfter = t ? transItemGap(t, skipId) - qty : 0;
- const shipAfter = shipC && shipC.id !== (c && c.id) ? remainGap(shipC, skipId) - qty : buyAfter;
- const cap = t && plate ? Number(((t.dispatches || []).find((d) => d.plate === plate) || {}).qty || 0) : 0;
- const vehAfter = plate ? cap - vehicleHold(plate, t && t.id, skipId) - qty : 0;
- const room = vehicleRoom(c, t, plate, skipId, shipC);
- const take = Math.min(qty, room);
- const over = buyAfter < -1e-6 || transAfter < -1e-6 || shipAfter < -1e-6 || (cap > 0 && vehAfter < -1e-6);
- const warn = !over && (
- lowRemain(buyAfter, buyWarnQty(c)) ||
- lowRemain(transAfter, transWarnQty(t)) ||
- (shipC ? lowRemain(shipAfter, buyWarnQty(shipC)) : false) ||
- lowRemain(remainAfterDone(c), buyWarnQty(c))
- );
- return { buyAfter, transAfter, shipAfter, vehAfter, cap, over, warn, room, take };
- }
- const waitLinkPicks = {};
- function fmtTon(n) {
- const x = Number(n || 0);
- return Math.abs(x - Math.round(x)) < 1e-6 ? String(Math.round(x)) : x.toFixed(2);
- }
- function linkedPlansOf(c) {
- return plans.filter((p) => p.status !== "已作废" && (p.certId === c.id || p.shipCertId === c.id || p.procureCertId === c.id));
- }
- function openLinkedPlans(c) {
- return linkedPlansOf(c).filter((p) => !p.archived);
- }
- function reserved(c, skipId) {
- return linkedPlansOf(c).reduce((s, p) => s + (p.id === skipId ? 0 : occupyQty(p)), 0);
- }
- function deductedOf(c, skipId) {
- return linkedPlansOf(c).reduce((s, p) => s + (p.id === skipId ? 0 : deductQty(p)), 0);
- }
- function holdingOf(c, skipId) {
- return linkedPlansOf(c).reduce((s, p) => s + (p.id === skipId ? 0 : holdQty(p)), 0);
- }
- function remainGap(c, skipId) {
- return Number(c.buyQty || 0) - reserved(c, skipId);
- }
- function remainAfterDone(c, skipId) {
- return Number(c.buyQty || 0) - deductedOf(c, skipId);
- }
- function remain(c, skipId) { return Math.max(0, remainGap(c, skipId)); }
- function transItemHold(t, skipId) {
- return plans.filter((p) => p.transId === t.id && p.status !== "已作废" && p.id !== skipId)
- .reduce((s, p) => s + occupyQty(p), 0);
- }
- function transItemDeduct(t, skipId) {
- return plans.filter((p) => p.transId === t.id && p.status !== "已作废" && p.id !== skipId)
- .reduce((s, p) => s + deductQty(p), 0);
- }
- function transItemPlan(t, skipId) {
- return plans.filter((p) => p.transId === t.id && p.status !== "已作废" && p.id !== skipId)
- .reduce((s, p) => s + holdQty(p), 0);
- }
- function transItemGap(t, skipId) {
- return Number(t.transQty || 0) - transItemHold(t, skipId);
- }
- function transItemRemain(t, skipId) { return Math.max(0, transItemGap(t, skipId)); }
- function transItemWarn(t, skipId) {
- if (!t) return false;
- const g = transItemGap(t, skipId);
- const afterDone = Number(t.transQty || 0) - transItemDeduct(t, skipId);
- return g < -1e-6 || lowRemain(g, transWarnQty(t)) || lowRemain(afterDone, transWarnQty(t));
- }
- function transTotal(c) { return transOf(c).reduce((s, t) => s + Number(t.transQty || 0), 0); }
- function transShipped(c) { return transOf(c).reduce((s, t) => s + Number(t.shipped || 0), 0); }
- function transRemain(c, skipId) { return transOf(c).reduce((s, t) => s + transItemRemain(t, skipId), 0); }
- function assignedSum(t) { return (t.dispatches || []).reduce((s, d) => s + Number(d.qty || 0), 0); }
- function assignedSumAll(c) { return transOf(c).reduce((s, t) => s + assignedSum(t), 0); }
- function unassignedTrans(t) { return Math.max(0, Number(t.transQty || 0) - assignedSum(t)); }
- function vehicleHold(plate, transId, skipId) {
- return plans.filter((p) => p.transId === transId && p.tractor === plate && p.status !== "已作废" && p.id !== skipId)
- .reduce((s, p) => s + occupyQty(p), 0);
- }
- function vehiclePlanHold(plate, transId, skipId) {
- return plans.filter((p) => p.transId === transId && p.tractor === plate && p.status !== "已作废" && p.id !== skipId)
- .reduce((s, p) => s + holdQty(p), 0);
- }
- function vehicleDeduct(plate, transId, skipId) {
- return plans.filter((p) => p.transId === transId && p.tractor === plate && p.status !== "已作废" && p.id !== skipId)
- .reduce((s, p) => s + deductQty(p), 0);
- }
- function vehicleBoundQty(c, t, plate, skipId) {
- if (!c || !t) return 0;
- const cap = Number(((t.dispatches || []).find((d) => d.plate === plate) || {}).qty || 0);
- const self = plate ? vehicleHold(plate, t.id, skipId) : 0;
- const buyRoom = remainGap(c, skipId) + self;
- const transRoom = transItemGap(t, skipId) + self;
- const rooms = [buyRoom, transRoom];
- if (cap > 0) rooms.push(cap);
- return Math.max(0, Math.min(...rooms));
- }
- function vehicleRoom(c, t, plate, skipId, shipC) {
- if (!c) return 0;
- const rooms = [remainGap(c, skipId)];
- if (t) rooms.push(transItemGap(t, skipId));
- if (shipC && shipC.id !== c.id) rooms.push(remainGap(shipC, skipId));
- const cap = t && plate ? Number(((t.dispatches || []).find((d) => d.plate === plate) || {}).qty || 0) : 0;
- const used = t && plate ? vehicleHold(plate, t.id, skipId) : 0;
- if (cap > 0) rooms.push(cap - used);
- return Math.max(0, Math.min(...rooms));
- }
- function calcVehPlanQty(c, t, plate, p) {
- const want = Number((p && p.planQty) || 0);
- const shipC = p && p.shipCertId ? certs.find((x) => x.id === p.shipCertId) : null;
- const room = vehicleRoom(c, t, plate, p && p.id, shipC);
- return { want, room, qty: Math.min(want, room) };
- }
- function syncCertShipped(c) {
- if (!c) return;
- c.shipped = deductedOf(c);
- transOf(c).forEach((t) => {
- t.shipped = transItemDeduct(t);
- (t.dispatches || []).forEach((d) => {
- d.shipped = vehicleDeduct(d.plate, t.id);
- });
- });
- }
- function settleCertOnComplete(p) {
- if (!p || !p.certId) return null;
- const n = deductQty(p);
- p.certDeducted = true;
- p.deductedQty = n;
- const cert = certs.find((c) => c.id === p.certId);
- if (!cert) return null;
- syncCertShipped(cert);
- const shipC = p.shipCertId ? certs.find((x) => x.id === p.shipCertId) : null;
- if (shipC && shipC.id !== cert.id) syncCertShipped(shipC);
- const t = findTrans(cert, p.transId);
- const buyLeft = remainAfterDone(cert);
- const transLeft = t ? Number(t.transQty || 0) - transItemDeduct(t) : 0;
- return {
- cert, t, n, buyLeft, transLeft,
- availBuy: remainGap(cert),
- availTrans: t ? transItemGap(t) : 0
- };
- }
- function certQtyWarn(c) {
- return lowRemain(remainGap(c), buyWarnQty(c)) || lowRemain(remainAfterDone(c), buyWarnQty(c));
- }
- function transQtyWarn(t) {
- if (!t) return false;
- const afterDone = Number(t.transQty || 0) - transItemDeduct(t);
- return transItemGap(t) < -1e-6 || lowRemain(transItemGap(t), transWarnQty(t)) || lowRemain(afterDone, transWarnQty(t));
- }
- function vehicleOver(d, t) {
- const cap = Number(d.qty || 0);
- if (cap <= 0) return vehicleHold(d.plate, t.id) > 1e-6;
- return vehicleHold(d.plate, t.id) - cap > 1e-6;
- }
- function platesOfTrans(t) {
- const set = new Set();
- (t.dispatches || []).forEach((d) => { if (d.plate) set.add(d.plate); });
- plans.forEach((p) => {
- if (p.transId === t.id && p.status !== "已作废" && p.tractor) set.add(p.tractor);
- });
- return [...set];
- }
- function unplatedHold(c) {
- return linkedPlansOf(c).filter((p) => !p.tractor).reduce((s, p) => s + occupyQty(p), 0);
- }
- function vehicleAlertOf(c, t, hold, cap, planHold, shipped) {
- const unit = c.unit || "吨";
- const buyG = remainGap(c);
- const transG = transItemGap(t);
- const buyDone = remainAfterDone(c);
- const transDone = Number(t.transQty || 0) - transItemDeduct(t);
- if (transG < -1e-6) return { level: "over", text: `本车计划 ${fmtTon(planHold || hold)}${unit},运输证已超证载 ${fmtTon(-transG)}${unit}` };
- if (buyG < -1e-6) return { level: "over", text: `本车计划 ${fmtTon(planHold || hold)}${unit},购买证已超证载 ${fmtTon(-buyG)}${unit}` };
- if (cap > 0 && hold - cap > 1e-6) return { level: "over", text: `本车计划 ${fmtTon(hold)}${unit} 超过分派 ${fmtTon(cap)}${unit}` };
- if (cap <= 0 && hold > 1e-6) return { level: "warn", text: `本车计划 ${fmtTon(planHold || hold)}${unit},运输证尚未分派该车` };
- if (lowRemain(transG, transWarnQty(t)) || lowRemain(transDone, transWarnQty(t))) return { level: "warn", text: `运输证余量 ${fmtTon(Math.min(transG, transDone))}${unit} ≤ ${fmtTon(transWarnQty(t))} 吨` };
- if (lowRemain(buyG, buyWarnQty(c)) || lowRemain(buyDone, buyWarnQty(c))) return { level: "warn", text: `购买证余量 ${fmtTon(Math.min(buyG, buyDone))}${unit} ≤ ${fmtTon(buyWarnQty(c))} 吨` };
- return { level: "", text: (planHold || shipped) ? `计划 ${fmtTon(planHold || 0)}${unit} · 已完成扣减 ${fmtTon(shipped || 0)}${unit}` : "正常" };
- }
- function vehicleRowsOf(c) {
- const rows = transOf(c).flatMap((t) => platesOfTrans(t).map((plate) => {
- const d = (t.dispatches || []).find((x) => x.plate === plate) || { plate, qty: 0, driver: "", shipped: 0 };
- const hold = vehicleHold(plate, t.id);
- const planHold = vehiclePlanHold(plate, t.id);
- const shipped = vehicleDeduct(plate, t.id);
- const cap = Number(d.qty || 0);
- const bound = vehicleBoundQty(c, t, plate);
- const alert = vehicleAlertOf(c, t, hold, cap, planHold, shipped);
- 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 };
- }));
- const loose = unplatedHold(c);
- if (loose > 1e-6) {
- const t = transOf(c)[0] || { transNo: "—", carrier: "", transQty: 0 };
- rows.push({
- plate: "未派车计划", driver: "—", transId: t.id, transNo: t.transNo || "—", carrier: t.carrier || "",
- cap: 0, hold: loose, planHold: loose, shipped: 0, bound: 0, level: "warn", text: `有 ${fmtTon(loose)}${c.unit} 计划已关联证件但未指定车辆`
- });
- }
- return rows;
- }
- function vehLinkedPlans(plate, transId) {
- const tid = Number(transId);
- if (plate === "未派车计划") {
- return plans.filter((p) => p.status !== "已作废" && Number(p.transId) === tid && !String(p.tractor || "").trim());
- }
- const plateN = String(plate || "").trim();
- return plans.filter((p) => p.status !== "已作废" && Number(p.transId) === tid && String(p.tractor || "").trim() === plateN);
- }
- function openVehPlanDlg(certId, transId, plate) {
- const c = certs.find((x) => x.id === certId);
- const t = c ? findTrans(c, transId) : null;
- const rows = vehLinkedPlans(plate, transId);
- const unit = (c && c.unit) || "吨";
- document.getElementById("vehPlanTitle").textContent =
- `${plate} · ${t ? t.transNo : "运输证"} · 关联计划 ${rows.length} 条`;
- const sumPlan = rows.reduce((s, p) => s + Number(p.planQty || 0), 0);
- const sumDone = rows.reduce((s, p) => s + Number(deductQty(p) || 0), 0);
- document.getElementById("vehPlanBody").innerHTML = rows.length ? `<table>
- <thead><tr><th>计划编号</th><th>状态</th><th>单据类型</th><th>品名</th><th>车牌</th><th>计划量</th><th>已完成量</th><th>送货时间</th></tr></thead>
- <tbody>${rows.map((p) => `<tr>
- <td class="cell-no">${p.no}</td>
- <td>${planStHtml(p)}</td>
- <td>${planTypeName(p)}</td>
- <td>${p.product || "—"}${p.ctrl === "易制毒" ? `<span class="prod-tag red">易制毒</span>` : ""}</td>
- <td>${p.tractor || "未派车"}</td>
- <td class="num">${fmtTon(p.planQty)} ${p.unit || unit}</td>
- <td class="num">${fmtTon(deductQty(p))} ${p.unit || unit}</td>
- <td>${planDeliverAt(p)}</td>
- </tr>`).join("")}</tbody>
- <tfoot><tr>
- <td colspan="5">合计</td>
- <td class="num">${fmtTon(sumPlan)} ${unit}</td>
- <td class="num">${fmtTon(sumDone)} ${unit}</td>
- <td></td>
- </tr></tfoot>
- </table>` : `<div class="empty">该车辆在此运输证下暂无关联计划。</div>`;
- const dlg = document.getElementById("vehPlanDialog");
- if (dlg.open) dlg.close();
- dlg.showModal();
- }
- function closeVehPlanDlg() {
- document.getElementById("vehPlanDialog").close();
- }
- function vehicleWarnCount(c) {
- return vehicleRowsOf(c).filter((r) => r.level).length;
- }
- function certOver(c) {
- if (remainGap(c) < -1e-6) return true;
- return transOf(c).some((t) => transItemGap(t) < -1e-6);
- }
- function transWarn(c) { return transOf(c).some((t) => transItemWarn(t)); }
- function transCount(c) { return transOf(c).length; }
- function transNos(c) { return transOf(c).map((t) => t.transNo).filter(Boolean); }
- function transDueMin(c) {
- const dates = [c.buyDue, ...transOf(c).map((t) => t.transDue)].filter(Boolean);
- return dates.length ? dates.reduce((a, b) => a < b ? a : b) : today;
- }
- function allDispatches(c) {
- return transOf(c).flatMap((t) => (t.dispatches || []).map((d) => ({ ...d, transId: t.id, transNo: t.transNo, carrier: t.carrier })));
- }
- function buyWarn(c) {
- return remainGap(c) < -1e-6 || certQtyWarn(c);
- }
- function sameCompanyCars(company) {
- return carrierFleet.filter((v) => v.company === company);
- }
- function fieldsOf(root) {
- const o = {};
- if (!root) return o;
- root.querySelectorAll("input, select, textarea").forEach((el) => {
- if (el.name && el.type !== "file") o[el.name] = el.value;
- });
- return o;
- }
- function fillFields(root, data) {
- if (!root || !data) return;
- Object.entries(data).forEach(([k, v]) => {
- const el = root.querySelector(`[name="${k}"]`);
- if (el) el.value = v;
- });
- }
- function vehicleWarnText(d, t, unit) {
- const hold = vehicleHold(d.plate, t.id);
- const cap = Number(d.qty || 0);
- if (cap > 0 && hold - cap > 1e-6) return `计划 ${hold}${unit} 超过分派 ${cap}${unit}`;
- if (cap <= 0 && hold > 1e-6) return `计划 ${hold}${unit},尚未分派该车`;
- if (transItemWarn(t)) return `运输证余量 ≤${fmtTon(transWarnQty(t))}吨`;
- return "";
- }
- function certVoided(c) { return !!(c && c.voided); }
- function transVoided(t) { return !!(t && t.voided); }
- function certSelectable(c) { return !!c && !certVoided(c); }
- function transSelectable(c, t) { return certSelectable(c) && !!t && !transVoided(t); }
- function openVoidCertDlg(kind, certId, transId) {
- const c = certs.find((x) => x.id === certId);
- if (!c) return;
- if (kind === "buy") {
- if (certVoided(c)) { toast("该购买证已核销"); return; }
- voidCertTarget = { kind, certId };
- document.getElementById("voidCertTitle").textContent = "核销购买证";
- document.getElementById("voidCertHint").textContent =
- `购买证 ${c.buyNo} 当前余量 ${fmtTon(remain(c))}${c.unit || "吨"}。核销后不可再关联运输计划,已关联在途计划不受影响。`;
- } else {
- const t = findTrans(c, transId);
- if (!t) return;
- if (transVoided(t)) { toast("该运输证已核销"); return; }
- voidCertTarget = { kind, certId, transId };
- document.getElementById("voidCertTitle").textContent = "核销运输证";
- document.getElementById("voidCertHint").textContent =
- `运输证 ${t.transNo} 当前余量 ${fmtTon(transItemRemain(t))}${c.unit || "吨"}。核销后不可再关联运输计划,已关联在途计划不受影响。`;
- }
- document.getElementById("voidCertDialog").showModal();
- }
- function closeVoidCertDlg() {
- document.getElementById("voidCertDialog").close();
- voidCertTarget = null;
- }
- function confirmVoidCert() {
- const tgt = voidCertTarget;
- if (!tgt) return;
- const c = certs.find((x) => x.id === tgt.certId);
- if (!c) return;
- if (tgt.kind === "buy") {
- c.voided = true;
- c.voidedAt = today;
- c.voidQty = remain(c);
- toast(`购买证 ${c.buyNo} 已核销,不可再关联`);
- } else {
- const t = findTrans(c, tgt.transId);
- if (!t) return;
- t.voided = true;
- t.voidedAt = today;
- t.voidQty = transItemRemain(t);
- toast(`运输证 ${t.transNo} 已核销,不可再关联`);
- }
- closeVoidCertDlg();
- refreshCertViews();
- }
- function certStatusOf(c) {
- if (certVoided(c)) return "已核销";
- const r = remainGap(c);
- const tr = transRemain(c);
- const due = daysTo(transDueMin(c));
- if (certOver(c)) return "超证";
- if (r <= 0 && tr <= 0) return "已用尽";
- if (vehicleWarnCount(c)) return "车辆预警";
- if (transWarn(c)) return "运输证预警";
- if (due <= 15) return "即将到期";
- if (buyWarn(c)) return "余量预警";
- return "有余量";
- }
- function certStHtml(s) {
- if (s === "有余量") return `<span class="st st-run">${s}</span>`;
- if (s === "余量预警" || s === "即将到期") return `<span class="st st-fix">${s}</span>`;
- if (s === "运输证预警") return `<span class="st st-due">运输证预警</span>`;
- if (s === "车辆预警") return `<span class="st st-due">车辆计划预警</span>`;
- if (s === "超证") return `<span class="st st-due">超证</span>`;
- return `<span class="st st-due">${s}</span>`;
- }
- function remainHtml(c) {
- const g = remainGap(c);
- const r = remain(c);
- const cls = g < -1e-6 ? "bad" : (r <= 0 ? "bad" : (buyWarn(c) || transWarn(c) ? "warn" : ""));
- return `<span class="remain ${cls}">${g < -1e-6 ? g.toFixed(2) : r} ${c.unit || "吨"}</span><div class="muted">预警 ${fmtTon(buyWarnQty(c))}${c.unit || "吨"}</div>`;
- }
- function transRemainHtml(c) {
- const gaps = transOf(c).map((t) => transItemGap(t));
- const over = gaps.reduce((s, g) => s + Math.min(0, g), 0);
- const r = transRemain(c);
- const cls = over < -1e-6 || r <= 0 ? "bad" : (transWarn(c) ? "warn" : "");
- if (over < -1e-6) return `<span class="remain ${cls}">超 ${(-over).toFixed(2)} ${c.unit || "吨"}</span>`;
- const th = transOf(c).map((t) => transWarnQty(t));
- const uniq = [...new Set(th.map((n) => fmtTon(n)))];
- return `<span class="remain ${cls}">${r} ${c.unit || "吨"}</span><div class="muted">预警 ${uniq.join(" / ")}${c.unit || "吨"}</div>`;
- }
- function certWarnCount() {
- return certs.filter((c) => ["余量预警", "运输证预警", "车辆预警", "即将到期", "已用尽", "超证"].includes(certStatusOf(c))).length;
- }
- function updateBadges() {
- const needLink = plans.filter((p) => p.ctrl && !p.certId && p.status !== "已完成" && p.status !== "已作废").length;
- const waitPrecursor = plans.filter(isWaitPrecursorPlan).length;
- document.getElementById("planBadge").textContent = waitPrecursor || needLink;
- document.getElementById("planBadge").style.display = (waitPrecursor || needLink) ? "" : "none";
- const waitArch = plans.filter((p) => p.status === "已完成" && !p.archived).length;
- const ab = document.getElementById("archiveBadge");
- if (ab) ab.textContent = waitArch;
- document.getElementById("certBadge").textContent = certWarnCount();
- const expN = expenses.filter((x) => ["待财务初审", "待终审", "待付款", "待归档", "已驳回"].includes(x.status)).length
- + preExps.filter((p) => p.status === "待财务初审" || p.status === "待批准").length;
- document.getElementById("expBadge").textContent = expN;
- const tb = document.getElementById("taskBadge");
- if (tb) tb.textContent = TASKS.filter((t) => t.status === "待执行" || t.status === "执行中").length;
- const cb = document.getElementById("ctBadge");
- if (cb) {
- const n = contracts.filter((x) => x.status === "待审批" || x.status === "草稿").length;
- cb.textContent = n;
- cb.style.display = n ? "" : "none";
- }
- }
- const WH_TREE = [
- {
- id: "h2so4", name: "硫酸库", code: "WH-H2SO4", dot: "acid", cap: 3600, unit: "吨",
- manager: "刘洋", area: "高新库区酸罐区",
- ops: ["采购入库", "销货出库", "稀释加工", "精制加工"],
- desc: "98% 工业硫酸可直销出库、加水稀释。试剂硫酸单独成线。工业 / 试剂分罐,不得混兑。",
- children: [
- { 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% 或其他定制浓度。" },
- { id: "h2so4-sul", name: "硫磺酸库", code: "WH-H2SO4-S", cap: 80, unit: "吨", manager: "刘洋", area: "V0702C", kind: "raw", line: "industrial", ops: ["采购入库", "销货出库", "稀释加工", "精制加工"], desc: "98%硫磺酸存放于 V0702C。可直销出库,也可稀释加工。" },
- { 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% 试剂酸或其他定制浓度。不与工业酸混用。" },
- { id: "h2so4-30", name: "30%工业硫酸库", code: "WH-H2SO4-30", cap: 800, unit: "吨", manager: "郑浩", area: "V0705A", kind: "product", line: "both", ops: ["销货出库"], desc: "30% 工业硫酸存放于 V0705A。试剂酸分罐存放,可分别出库。" },
- { 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,分罐出库。" },
- { id: "h2so4-custom", name: "定制浓度库", code: "WH-H2SO4-DX", cap: 600, unit: "吨", manager: "郑浩", area: "成品罐区 3#", kind: "product", line: "both", ops: ["销货出库"], desc: "承接订单定制浓度(如 40%、70%、80% 等)。工业酸、试剂酸分罐。" }
- ]
- },
- { id: "naoh", name: "液碱库", code: "WH-NAOH", dot: "alkali", cap: 450, unit: "吨", manager: "陈伟", area: "32%液碱 V0801 V0802 V0803", ops: ["进货入库", "销货出库"], desc: "32% 液碱存放于 V0801、V0802、V0803。", children: [
- { 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。" }
- ] },
- { id: "hcl", name: "盐酸库", code: "WH-HCL", dot: "hcl", cap: 520, unit: "吨", manager: "刘洋", area: "盐酸罐区", ops: ["采购入库", "销货出库"], desc: "盐酸库分仓:31% 工业盐酸、31% 合成盐酸、36% 分析纯盐酸。", children: [
- { id: "hcl-31-ind", name: "31% 工业盐酸库", code: "WH-HCL-31I", cap: 200, unit: "吨", manager: "刘洋", area: "V0501 V0502", kind: "product", ops: ["采购入库", "销货出库"], desc: "31% 工业盐酸存放于 V0501、V0502。" },
- { id: "hcl-31", name: "31% 合成盐酸库", code: "WH-HCL-31", cap: 200, unit: "吨", manager: "刘洋", area: "V0601 V0602", kind: "product", ops: ["采购入库", "销货出库"], desc: "31% 合成盐酸存放于 V0601、V0602。" },
- { id: "hcl-36", name: "36% 分析纯盐酸库", code: "WH-HCL-36", cap: 120, unit: "吨", manager: "刘洋", area: "V0901 V0902", kind: "product", ops: ["采购入库", "销货出库"], desc: "36% 分析纯盐酸存放于 V0901、V0902。" }
- ] },
- {
- id: "pack", name: "分装库", code: "WH-PACK", dot: "pack", cap: 400, unit: "吨", manager: "胡明", area: "分装车间库",
- ops: ["形态转换入库", "销货出库"], desc: "分装成品按品号管理。",
- children: [
- { id: "pack-s98-25", name: "S-98-25PL", code: "S-98-25PL", cap: 40, unit: "吨", manager: "胡明", area: "分装库", desc: "98%工业硫酸 · 25kg塑桶" },
- { id: "pack-s98-5", name: "S-98-5PL", code: "S-98-5PL", cap: 20, unit: "吨", manager: "胡明", area: "分装库", desc: "98%工业硫酸 · 5kg塑桶" },
- { id: "pack-cp97-25", name: "CP-97-25PL", code: "CP-97-25PL", cap: 30, unit: "吨", manager: "胡明", area: "分装库", desc: "试剂硫酸 97% · 25kg塑桶" },
- { id: "pack-cp97-5", name: "CP-97-5PL", code: "CP-97-5PL", cap: 15, unit: "吨", manager: "胡明", area: "分装库", desc: "试剂硫酸 97% · 5kg塑桶" },
- { id: "pack-h31", name: "H-31-25PL", code: "H-31-25PL", cap: 30, unit: "吨", manager: "胡明", area: "分装库", desc: "31%盐酸 · 25kg塑桶" },
- { id: "pack-ah36", name: "AH-36-25PL", code: "AH-36-25PL", cap: 25, unit: "吨", manager: "胡明", area: "分装库", desc: "36%醋酸 · 25kg塑桶" },
- { id: "pack-c10", name: "C-10-25PL", code: "C-10-25PL", cap: 30, unit: "吨", manager: "胡明", area: "分装库", desc: "10%次氯酸钠 · 25kg塑桶" },
- { id: "pack-c13", name: "C-13-25PL", code: "C-13-25PL", cap: 25, unit: "吨", manager: "胡明", area: "分装库", desc: "13%次氯酸钠 · 25kg塑桶" }
- ]
- },
- {
- id: "solid", name: "固体库", code: "WH-SOLID", dot: "solid", cap: 600, unit: "吨", manager: "吴涛", area: "固体危化库",
- ops: ["进货入库", "销货出库"], desc: "固体库按货源分仓:片碱库(君正)、片碱库(红四方)、纯碱库(红四方)。",
- children: [
- { id: "solid-jz", name: "片碱库(君正)", code: "WH-SOLID-JZ", cap: 200, unit: "吨", manager: "吴涛", area: "固体库 · 君正片碱区", ops: ["进货入库", "销货出库"], desc: "存放君正货源片碱。" },
- { id: "solid-hsf", name: "片碱库(红四方)", code: "WH-SOLID-HSF", cap: 200, unit: "吨", manager: "吴涛", area: "固体库 · 红四方片碱区", ops: ["进货入库", "销货出库"], desc: "存放红四方货源片碱。" },
- { id: "solid-soda", name: "纯碱库(红四方)", code: "WH-SOLID-SODA", cap: 200, unit: "吨", manager: "吴涛", area: "固体库 · 红四方纯碱区", ops: ["进货入库", "销货出库"], desc: "存放红四方货源纯碱。" }
- ]
- }
- ];
- const WH_STOCK = [
- { 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: [
- { id: "V0701A", qty: 58, cap: 80 }, { id: "V0701B", qty: 54, cap: 80 }, { id: "V0701C", qty: 52, cap: 80 },
- { id: "V0701D", qty: 56, cap: 80 }, { id: "V070E", qty: 50, cap: 80 }, { id: "V0701F", qty: 56, cap: 80 }
- ] },
- { wh: "h2so4-sul", name: "98%硫磺酸", grade: "industrial", conc: 98, qty: 42.0, cost: 390.0, unit: "吨", loc: "V0702C", tankCap: 80, tanks: [
- { id: "V0702C", qty: 42, cap: 80 }
- ] },
- { wh: "h2so4-rg", name: "试剂硫酸", grade: "reagent", conc: 98, qty: 86.5, cost: 1280.0, unit: "吨", loc: "V0702A / V0702B / V0705B", tankCap: 240, tanks: [
- { id: "V0702A", qty: 32, cap: 80 }, { id: "V0702B", qty: 30, cap: 80 }, { id: "V0705B", qty: 24.5, cap: 80 }
- ] },
- { wh: "h2so4-30", name: "30%工业硫酸", grade: "industrial", conc: 30, qty: 186.4, cost: 168.0, unit: "吨", loc: "V0705A", tankCap: 400, tanks: [
- { id: "V0705A", qty: 186.4, cap: 400 }
- ] },
- { wh: "h2so4-30", name: "30%试剂酸", grade: "reagent", conc: 30, qty: 42.0, cost: 460.0, unit: "吨", loc: "30%罐 2#", tankCap: 200 },
- { wh: "h2so4-50", name: "50%工业硫酸", grade: "industrial", conc: 50, qty: 312.8, cost: 248.6, unit: "吨", loc: "V0704", tankCap: 600, tanks: [
- { id: "V0704", qty: 312.8, cap: 600 }
- ] },
- { wh: "h2so4-50", name: "50%试剂酸", grade: "reagent", conc: 50, qty: 58.0, cost: 720.0, unit: "吨", loc: "V0301A / V0301B", tankCap: 250, tanks: [
- { id: "V0301A", qty: 30, cap: 125 }, { id: "V0301B", qty: 28, cap: 125 }
- ] },
- { wh: "h2so4-custom", name: "40%工业硫酸", grade: "industrial", conc: 40, qty: 36.0, cost: 210.0, unit: "吨", loc: "定制罐 1#", tankCap: 300 },
- { wh: "h2so4-custom", name: "70%试剂酸", grade: "reagent", conc: 70, qty: 18.0, cost: 980.0, unit: "吨", loc: "定制罐 2#", tankCap: 200 },
- { wh: "h2so4-custom", name: "32%工业硫酸", grade: "industrial", conc: 32, qty: 24.0, cost: 185.0, unit: "吨", loc: "定制罐 3#", tankCap: 200 },
- { wh: "hcl-31-ind", name: "31%工业盐酸", conc: 31, qty: 118.0, cost: 185.0, unit: "吨", loc: "V0501 / V0502", tankCap: 200, tanks: [
- { id: "V0501", qty: 62, cap: 100 }, { id: "V0502", qty: 56, cap: 100 }
- ] },
- { wh: "hcl-31", name: "31%合成盐酸", conc: 31, qty: 86.0, cost: 220.0, unit: "吨", loc: "V0601 / V0602", tankCap: 200, tanks: [
- { id: "V0601", qty: 44, cap: 100 }, { id: "V0602", qty: 42, cap: 100 }
- ] },
- { wh: "hcl-36", name: "36%分析纯盐酸", conc: 36, qty: 28.0, cost: 860.0, unit: "吨", loc: "V0901 / V0902", tankCap: 120, tanks: [
- { id: "V0901", qty: 16, cap: 60 }, { id: "V0902", qty: 12, cap: 60 }
- ] },
- { wh: "naoh-32", name: "32%液碱", qty: 420.0, cost: 980.0, unit: "吨", loc: "V0801 / V0802 / V0803", tankCap: 450, tanks: [
- { id: "V0801", qty: 145, cap: 150 }, { id: "V0802", qty: 140, cap: 150 }, { id: "V0803", qty: 135, cap: 150 }
- ] },
- { 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 },
- { 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 },
- { 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 },
- { 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 },
- { wh: "pack-h31", sku: "H-31-25PL", name: "31%盐酸", packSpec: "25kg塑桶", qty: 9.5, cost: 280.0, unit: "吨", loc: "H-31-25PL", tankCap: 30 },
- { wh: "pack-ah36", sku: "AH-36-25PL", name: "36%醋酸", packSpec: "25kg塑桶", qty: 6.1, cost: 420.0, unit: "吨", loc: "AH-36-25PL", tankCap: 25 },
- { wh: "pack-c10", sku: "C-10-25PL", name: "10%次氯酸钠", packSpec: "25kg塑桶", qty: 7.3, cost: 310.0, unit: "吨", loc: "C-10-25PL", tankCap: 30 },
- { wh: "pack-c13", sku: "C-13-25PL", name: "13%次氯酸钠", packSpec: "25kg塑桶", qty: 5.8, cost: 340.0, unit: "吨", loc: "C-13-25PL", tankCap: 25 },
- { wh: "solid-jz", name: "片碱", supplier: "君正", qty: 48.0, cost: 3050.0, unit: "吨", loc: "片碱库(君正)" },
- { wh: "solid-hsf", name: "片碱", supplier: "红四方", qty: 37.0, cost: 3120.0, unit: "吨", loc: "片碱库(红四方)" },
- { wh: "solid-soda", name: "纯碱", supplier: "红四方", qty: 52.0, cost: 1680.0, unit: "吨", loc: "纯碱库(红四方)" }
- ];
- const WH_LEDGER = [
- { 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 吨" },
- { 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%工业硫酸加水" },
- { 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 },
- { 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: "原料酸直接出库" },
- { 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 吨" },
- { 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: "由试剂硫酸加工" },
- { 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 },
- { 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 吨" },
- { 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: "定制浓度" },
- { 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 },
- { 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塑桶" },
- { 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塑桶" },
- { 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 吨" },
- { 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%工业硫酸加水" },
- { 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塑桶" },
- { 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塑桶" },
- { 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 },
- { 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" },
- { 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: "君正货源" },
- { 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: "红四方货源" },
- { 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 },
- { 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塑桶" },
- { 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塑桶" },
- { 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塑桶" },
- { 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塑桶" },
- { 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: "红四方纯碱出库" },
- { 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: "原料试剂酸直接出库" },
- { 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 },
- { 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 },
- { 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 },
- ];
- let currentWh = "h2so4";
- let currentWhDoc = "库存成本明细账";
- let currentWhView = "info";
- let whDocSeq = 10;
- const WH_PROC_PARENTS = ["h2so4", "hcl", "pack", "naoh"];
- const WH_PROCESS = [
- { 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 },
- { 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 },
- { 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 },
- { 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 },
- { 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 },
- { 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 },
- { 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 },
- { 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 }
- ];
- function whIdsOf(id) {
- const node = WH_TREE.find((w) => w.id === id);
- if (node) return [id, ...node.children.map((c) => c.id)];
- return [id];
- }
- function whNameOf(id) {
- for (const w of WH_TREE) {
- if (w.id === id) return w.name;
- const c = w.children.find((x) => x.id === id);
- if (c) return w.name + " / " + c.name;
- }
- return id;
- }
- function warehouseSelectHtml(selected) {
- return WH_TREE.map((w) => {
- if (w.children.length) {
- return `<optgroup label="${w.name}">${w.children.map((c) =>
- `<option value="${c.id}" ${selected === c.id ? "selected" : ""}>${c.name}</option>`
- ).join("")}</optgroup>`;
- }
- return `<option value="${w.id}" ${selected === w.id ? "selected" : ""}>${w.name}</option>`;
- }).join("");
- }
- function planTypeName(p) {
- return p.type === "入直单" ? "入库单" : p.type;
- }
- function isInboundPlan(p) {
- return p.type === "入直单" || p.type === "入库单";
- }
- function isDirectSale(p) {
- return p.type === "直销单";
- }
- function warehouseForPlan(p) {
- if (isDirectSale(p)) return "";
- if (p.archived && p.warehouseId) return p.warehouseId;
- return suggestWarehouse(p.product, p.spec) || p.warehouseId || "";
- }
- function parseConc(text) {
- const m = String(text || "").match(/(\d+(?:\.\d+)?)\s*[%%]/);
- return m ? Number(m[1]) : null;
- }
- function acidGradeOf(text) {
- const p = String(text || "");
- if (p.includes("试剂") || p.includes("精制")) return "reagent";
- return "industrial";
- }
- function gradeName(g) {
- return g === "reagent" ? "试剂酸" : "工业酸";
- }
- function suggestWarehouse(product, spec) {
- const p = `${product || ""}${spec || ""}`.replace(/\s/g, "");
- if (p.includes("分析纯") && p.includes("盐酸") || (p.includes("36%") && p.includes("盐酸") && !/塑桶|醋酸/.test(p))) return "hcl-36";
- if (p.includes("合成盐酸")) return "hcl-31";
- if (p.includes("工业盐酸") || (p.includes("31%") && p.includes("盐酸") && !/塑桶|H-31/.test(p))) return "hcl-31-ind";
- if (p.includes("烧碱") || p.includes("液碱")) return "naoh-32";
- if (p.includes("纯碱")) return "solid-soda";
- if (p.includes("片碱") && (p.includes("君正") || p.includes("JZ"))) return "solid-jz";
- if (p.includes("片碱") && (p.includes("红四方") || p.includes("HSF"))) return "solid-hsf";
- if (p.includes("片碱")) return "solid";
- if (/S-98|CP-97|H-31|AH-36|C-10|C-13|盐酸|醋酸|次氯|塑桶/.test(p)) return "pack";
- if (p.includes("硫磺酸")) return "h2so4-sul";
- const isAcid = /硫酸|试剂酸|硫磺酸/.test(p);
- if (!isAcid) return "";
- const conc = parseConc(p);
- const grade = acidGradeOf(p);
- if (conc != null) {
- if (conc >= 95) {
- if (grade === "reagent") return "h2so4-rg";
- return "h2so4-98";
- }
- if (Math.abs(conc - 30) < 0.6) return "h2so4-30";
- if (Math.abs(conc - 50) < 0.6) return "h2so4-50";
- return "h2so4-custom";
- }
- if (grade === "reagent") return "h2so4-rg";
- return "h2so4-98";
- }
- function warehouseCell(p) {
- if (isDirectSale(p)) return `<span class="muted">无中转仓库</span>`;
- const wh = warehouseForPlan(p);
- if (!wh) return `<span class="muted">未匹配品名</span>`;
- const stock = matchStock(wh, `${p.product || ""}${p.spec || ""}`);
- const tank = stock ? `<div class="muted">${stock.loc}${stock.grade ? " · " + gradeName(stock.grade) : ""}</div>` : "";
- return `<b>${whNameOf(wh)}</b>${tank}`;
- }
- function stockActCell(p) {
- if (isDirectSale(p)) return `<span class="wh-act none">不记库存</span>`;
- if (isInboundPlan(p)) return `<span class="wh-act in">入库 · 进货</span>`;
- return `<span class="wh-act out">出库 · 销货</span>`;
- }
- function gradeHtml(s) {
- if (s.grade === "industrial") return `<span class="grade ind">工业酸</span>`;
- if (s.grade === "reagent") return `<span class="grade rg">试剂酸</span>`;
- return "";
- }
- function matchStock(whId, product) {
- const list = WH_STOCK.filter((s) => s.wh === whId);
- if (!list.length) return null;
- const compact = String(product || "").replace(/\s/g, "");
- const exact = list.find((s) => {
- const n = s.name.replace(/\s/g, "");
- return compact.includes(n) || n.includes(compact);
- });
- if (exact) return exact;
- const conc = parseConc(compact);
- const grade = acidGradeOf(compact);
- if (list.some((s) => s.grade)) {
- return list.find((s) => s.grade === grade && (conc == null || Number(s.conc) === conc))
- || list.find((s) => s.grade === grade && conc != null && Math.abs(Number(s.conc) - conc) < 0.6)
- || list.find((s) => s.grade === grade)
- || list.find((s) => conc != null && Number(s.conc) === conc)
- || list[0];
- }
- if (conc != null) {
- const byc = list.find((s) => parseConc(s.name) === conc);
- if (byc) return byc;
- }
- return list[0];
- }
- function assignTankLoc(wh, grade) {
- const map = {
- "h2so4-30": { industrial: "V0705A", reagent: "30%罐 2#" },
- "h2so4-50": { industrial: "V0704", reagent: "V0301A / V0301B" },
- "h2so4-custom": { industrial: "定制罐 1#", reagent: "定制罐 2#" },
- "h2so4-98": { industrial: "V0701A" },
- "h2so4-sul": { industrial: "V0702C" },
- "h2so4-rg": { reagent: "V0702A" }
- };
- return (map[wh] && map[wh][grade]) || "储罐";
- }
- function stockTankRows(s) {
- if (s.tanks && s.tanks.length) {
- return s.tanks.map((t) => ({
- id: t.id,
- qty: t.qty,
- cap: t.cap || 80,
- note: t.note || "",
- label: t.note ? `${t.id}(${t.note})` : t.id
- }));
- }
- return [{ id: s.loc, qty: s.qty, cap: s.tankCap || 200, note: "", label: s.loc }];
- }
- function tankChipList(stock) {
- const labels = [];
- stock.forEach((s) => {
- stockTankRows(s).forEach((t) => {
- String(t.label || "").split(/\s*[/、]\s*/).forEach((part) => {
- const v = part.trim();
- if (v && !labels.includes(v)) labels.push(v);
- });
- });
- });
- const numbered = labels.filter((x) => /V\d/i.test(x));
- const rest = labels.filter((x) => !/V\d/i.test(x));
- return numbered.concat(rest);
- }
- function tankGroupHtml(whId, stock) {
- const chip = (id) => `<span class="chip ghost">${id}</span>`;
- const all = whId === "h2so4";
- const groups = [];
- if (all || whId === "h2so4-98") {
- groups.push(["98%工业硫酸库", ["V0701A", "V0701B", "V0701C", "V0701D", "V070E", "V0701F"]]);
- }
- if (all || whId === "h2so4-sul") {
- groups.push(["硫磺酸库", ["V0702C"]]);
- }
- if (all || whId === "h2so4-rg") {
- groups.push(["试剂硫酸库", ["V0702A", "V0702B", "V0705B"]]);
- }
- if (all || whId === "h2so4-30") {
- groups.push(["30%工业硫酸库", ["V0705A"]]);
- }
- if (all || whId === "h2so4-50") {
- groups.push(["50%工业硫酸库", ["V0704"]]);
- groups.push(["50%试剂酸", ["V0301A", "V0301B"]]);
- }
- if (whId === "hcl" || whId === "hcl-31-ind") {
- groups.push(["31% 工业盐酸库", ["V0501", "V0502"]]);
- }
- if (whId === "hcl" || whId === "hcl-31") {
- groups.push(["31% 合成盐酸库", ["V0601", "V0602"]]);
- }
- if (whId === "hcl" || whId === "hcl-36") {
- groups.push(["36% 分析纯盐酸库", ["V0901", "V0902"]]);
- }
- if (whId === "naoh" || whId === "naoh-32") {
- groups.push(["32%液碱库", ["V0801", "V0802", "V0803"]]);
- }
- if (all === false && (whId === "pack" || String(whId).startsWith("pack"))) {
- 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"]]);
- }
- if (whId === "solid" || String(whId).startsWith("solid-")) {
- const solidBins = [];
- if (whId === "solid" || whId === "solid-jz") solidBins.push("片碱库(君正)");
- if (whId === "solid" || whId === "solid-hsf") solidBins.push("片碱库(红四方)");
- if (whId === "solid" || whId === "solid-soda") solidBins.push("纯碱库(红四方)");
- groups.push(["固体分仓", solidBins]);
- }
- if (!groups.length) {
- const flat = tankChipList(stock);
- return flat.length ? `<div class="wh-tank">${flat.map(chip).join("")}</div>` : "";
- }
- return groups.map(([lab, ids]) => `
- <div class="tank-group">
- <div class="tg-lab">${lab}</div>
- <div class="wh-tank">${ids.map(chip).join("")}</div>
- </div>`).join("");
- }
- function ensureStock(whId, product) {
- let stock = matchStock(whId, product);
- const compact = String(product || "").replace(/\s/g, "");
- const conc = parseConc(compact);
- const grade = acidGradeOf(compact);
- if (stock && stock.grade && conc != null && Number(stock.conc) !== conc && Math.abs(Number(stock.conc) - conc) >= 0.6) {
- stock = null;
- }
- if (stock) return stock;
- const name = acidProductName(grade, conc == null ? 98 : conc, { wh: whId });
- stock = {
- wh: whId, name, grade, conc: conc == null ? 98 : conc, qty: 0, cost: 0, unit: "吨",
- loc: assignTankLoc(whId, grade), tankCap: 200
- };
- WH_STOCK.push(stock);
- return stock;
- }
- function whMeta(id) {
- for (const w of WH_TREE) {
- if (w.id === id) return w;
- const c = w.children.find((x) => x.id === id);
- if (c) return { ...c, parent: w.name, dot: w.dot };
- }
- return { name: id, cap: 0, unit: "吨" };
- }
- function stockOf(ids) {
- return WH_STOCK.filter((s) => ids.includes(s.wh));
- }
- function ledgerOf(ids) {
- return WH_LEDGER.filter((r) => ids.includes(r.wh));
- }
- function fmtT(n) {
- return Number(n).toLocaleString("zh-CN", { maximumFractionDigits: 1 });
- }
- function fmtY(n) {
- return Number(n).toLocaleString("zh-CN", { maximumFractionDigits: 0 });
- }
- function whLineLabel(line) {
- if (line === "industrial") return `<span class="grade ind">工业酸</span>`;
- if (line === "reagent") return `<span class="grade rg">试剂酸</span>`;
- if (line === "both") return `<span class="grade ind">工业酸</span> <span class="grade rg">试剂酸</span>`;
- return "";
- }
- function whKindLabel(kind) {
- if (kind === "raw") return `<span class="grade raw">原料库</span>`;
- if (kind === "product") return `<span class="grade fin">成品库</span>`;
- return `<span class="grade fin">仓库</span>`;
- }
- function whOpsHtml(ops) {
- return (ops || []).map((o) => `<span class="op-chip ${o.includes("出库") ? "out" : ""}">${o}</span>`).join("");
- }
- function parentWhOf(id) {
- const node = WH_TREE.find((w) => w.id === id);
- if (node) return node;
- return WH_TREE.find((w) => w.children.some((c) => c.id === id));
- }
- function renderWhNav() {
- const parent = parentWhOf(currentWh);
- const tree = currentWhView === "proc" ? WH_TREE.filter((w) => WH_PROC_PARENTS.includes(w.id)) : WH_TREE;
- const mains = tree.map((w) => {
- const on = parent?.id === w.id;
- return `
- <button class="wh-nav-item ${w.dot} ${on ? "on" : ""}" type="button" onclick="selectWarehouse('${w.id}')">
- <span class="wh-dot ${w.dot}"></span>${w.name}
- </button>`;
- }).join("");
- let subs = "";
- if (parent?.children?.length && (currentWhView !== "proc" || WH_PROC_PARENTS.includes(parent.id))) {
- subs = `<div class="wh-nav-subs"><span class="wh-nav-subs-lab">子仓库</span>${parent.children.map((c) => `
- <button class="wh-nav-child ${parent.dot} ${currentWh === c.id ? "on" : ""}" type="button" onclick="selectWarehouse('${c.id}')">
- <span class="wh-dot ${parent.dot}"></span>${c.name}
- </button>`).join("")}</div>`;
- }
- document.getElementById("whNav").innerHTML = `<div class="wh-nav-mains"><span class="wh-nav-lab">仓库</span>${mains}</div>${subs}`;
- }
- function renderWhKpis() {
- const ids = whIdsOf(currentWh);
- const stock = stockOf(ids);
- const qty = stock.reduce((a, s) => a + s.qty, 0);
- const cost = stock.reduce((a, s) => a + s.qty * s.cost, 0);
- const cap = ids.reduce((a, id) => {
- const m = whMeta(id);
- return a + (m.children ? 0 : (m.cap || 0));
- }, 0) || (whMeta(currentWh).cap || 0);
- const use = cap ? Math.min(100, Math.round(qty / cap * 100)) : 0;
- const inQty = ledgerOf(ids).reduce((a, r) => a + r.inQty, 0);
- const outQty = ledgerOf(ids).reduce((a, r) => a + r.outQty, 0);
- const low = stock.filter((s) => s.qty < 80);
- document.getElementById("whKpis").innerHTML = `
- <div class="wh-kpi"><div class="k">结存数量</div><div class="v">${fmtT(qty)}</div><div class="s">${whMeta(currentWh).unit || "吨"} · ${stock.length} 个品名</div></div>
- <div class="wh-kpi"><div class="k">结存成本</div><div class="v">${fmtY(cost)}</div><div class="s">元 · 加权平均</div></div>
- <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>
- <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>`;
- }
- function fmtN(n, d) {
- return Number(n).toLocaleString("zh-CN", { minimumFractionDigits: d, maximumFractionDigits: d });
- }
- function ledgerView(r) {
- const inQty = Number(r.inQty || 0);
- const outQty = Number(r.outQty || 0);
- const inCost = Number(r.inCost || 0);
- const outCost = Number(r.outCost || 0);
- let docType = r.docType || r.doc;
- if (r.doc === "形态转换单") docType = outQty ? "其他出库单" : "其他入库单";
- if (r.doc === "组装单") docType = "其他入库单";
- const bizMap = {
- "销货单": "普通销售", "进货单": "采购入库", "材料出库单": "生产领料",
- "其他出库单": "组装", "其他入库单": "组装", "形态转换单": "组装", "组装单": "组装",
- "产成品入库单": "完工入库", "盘点单": "盘点"
- };
- return {
- whName: whMeta(r.wh).name || whNameOf(r.wh),
- product: r.product,
- unit: r.unit || "吨",
- no: r.no,
- date: r.date,
- postDate: r.postDate || r.date,
- docType,
- bizType: r.bizType || bizMap[r.doc] || bizMap[docType] || "",
- partner: r.partner || "",
- inQty, inCost, outQty, outCost,
- inPrice: inQty ? inCost / inQty : 0,
- outPrice: outQty ? outCost / outQty : 0,
- balQty: Number(r.balQty || 0),
- price: Number(r.price || 0),
- balCost: Number(r.balCost || 0)
- };
- }
- function renderWhTable() {
- const ids = whIdsOf(currentWh);
- const q = (document.getElementById("whQ").value || "").trim().toLowerCase();
- const docSel = document.getElementById("whDocFilter").value;
- let rows = ledgerOf(ids);
- if (currentWhDoc && currentWhDoc !== "库存成本明细账") {
- rows = rows.filter((r) => r.doc === currentWhDoc);
- }
- if (docSel) rows = rows.filter((r) => r.doc === docSel);
- if (q) rows = rows.filter((r) => (r.no + r.product + r.doc + (r.partner || "") + (r.note || "")).toLowerCase().includes(q));
- document.getElementById("whTableTitle").textContent = currentWhDoc === "库存成本明细账"
- ? "库存成本明细账 · " + whNameOf(currentWh)
- : currentWhDoc + " · " + whNameOf(currentWh);
- document.getElementById("whTableCount").textContent = rows.length + " 条";
- if (currentWhDoc === "库存成本明细账") {
- const views = rows.map(ledgerView);
- const sumInQty = views.reduce((a, r) => a + r.inQty, 0);
- const sumInAmt = views.reduce((a, r) => a + r.inCost, 0);
- const sumOutQty = views.reduce((a, r) => a + r.outQty, 0);
- const sumOutAmt = views.reduce((a, r) => a + r.outCost, 0);
- const dash = `<td class="dash">—</td>`;
- const qtyCell = (n, cls) => n ? `<td class="${cls}">${fmtT(n)}</td>` : dash;
- const priceCell = (n, show) => show ? `<td>${fmtN(n, 2)}</td>` : dash;
- const amtCell = (n, show) => show ? `<td class="amt">${fmtN(n, 2)}</td>` : dash;
- document.getElementById("whTable").innerHTML = views.length ? `
- <table class="proc-table wide">
- <thead>
- <tr>
- <th>序号</th>
- <th class="col-name">仓库名称</th>
- <th class="col-name">物料名称</th>
- <th>单号</th>
- <th>日期</th>
- <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>
- <tbody>${views.map((r, i) => `<tr>
- <td>${i + 1}</td>
- <td class="col-name">${r.whName}</td>
- <td class="col-name"><b>${r.product}</b></td>
- <td class="cell-no">${r.no}</td>
- <td>${r.date}</td>
- <td>${r.docType}</td>
- <td>${r.bizType || "—"}</td>
- <td>${r.partner || "—"}</td>
- ${qtyCell(r.inQty, "qty-in")}
- ${priceCell(r.inPrice, r.inQty)}
- ${amtCell(r.inCost, r.inQty)}
- ${qtyCell(r.outQty, "qty-out")}
- ${priceCell(r.outPrice, r.outQty)}
- ${amtCell(r.outCost, r.outQty)}
- <td class="qty-bal">${fmtT(r.balQty)}</td>
- <td>${fmtN(r.price, 2)}</td>
- <td class="amt">${fmtN(r.balCost, 2)}</td>
- </tr>`).join("")}</tbody>
- <tfoot>
- <tr>
- <td colspan="8">合计</td>
- <td class="qty-in">${fmtT(sumInQty)}</td>
- <td class="dash">—</td>
- <td class="amt">${fmtN(sumInAmt, 2)}</td>
- <td class="qty-out">${fmtT(sumOutQty)}</td>
- <td class="dash">—</td>
- <td class="amt">${fmtN(sumOutAmt, 2)}</td>
- <td class="dash">—</td>
- <td class="dash">—</td>
- <td class="dash">—</td>
- </tr>
- </tfoot>
- </table>` : `<div class="empty">没有符合条件的明细。</div>`;
- } else {
- document.getElementById("whTable").innerHTML = rows.length ? `
- <table class="proc-table">
- <thead><tr><th>日期</th><th>单号</th><th>仓库</th><th>品名</th><th>入库</th><th>出库</th><th>金额</th><th>说明</th></tr></thead>
- <tbody>${rows.map((r) => `<tr>
- <td>${r.date}</td><td class="cell-no">${r.no}</td>
- <td class="wh-path">${whNameOf(r.wh)}</td><td>${r.product}</td>
- <td class="num">${r.inQty ? fmtT(r.inQty) : "—"}</td>
- <td class="num">${r.outQty ? fmtT(r.outQty) : "—"}</td>
- <td class="num">${fmtY(r.inCost || r.outCost)}</td>
- <td class="muted">${r.note || "—"}</td>
- </tr>`).join("")}</tbody>
- </table>` : `<div class="empty">当前仓库没有「${currentWhDoc}」。</div>`;
- }
- }
- function selectWarehouse(id) {
- currentWh = id;
- renderWarehouse();
- }
- function showWhView(view) {
- currentWhView = view || "info";
- document.querySelectorAll("#whSubnav button").forEach((b) => b.classList.toggle("on", b.dataset.view === currentWhView));
- document.getElementById("view-wh-info").classList.toggle("hidden", currentWhView !== "info");
- document.getElementById("view-wh-mode").classList.toggle("hidden", currentWhView !== "mode");
- document.getElementById("view-wh-proc").classList.toggle("hidden", currentWhView !== "proc");
- document.getElementById("view-wh-stock").classList.toggle("hidden", currentWhView !== "stock");
- const titles = { info: "库存信息表", mode: "仓库信息表", proc: "加工单明细表", stock: "库存成本明细表" };
- const descs = {
- info: "查看当前仓库在库库存与储罐库存明细表。",
- mode: "仓库档案与作业说明:出入库、稀释加工、精制加工。工业硫酸与试剂硫酸分线,不得混兑。",
- proc: "仓库内加水 / 形态转换形成的加工明细。现场作业请用运输计划下发的任务单。",
- stock: "按仓库查看库存成本明细账。稀释加工走形态转换:原料酸出库、成品入库。"
- };
- document.getElementById("whPageTitle").textContent = titles[currentWhView] || "库存信息表";
- document.getElementById("whPageDesc").textContent = descs[currentWhView] || descs.info;
- const acts = document.getElementById("whPageActions");
- if (currentWhView === "stock") {
- acts.innerHTML = `<button class="btn" type="button" onclick="whSelectDoc('形态转换单')">形态转换</button>
- <button class="btn" type="button" onclick="whSelectDoc('盘点单')">盘点</button>`;
- } else if (currentWhView === "mode") {
- acts.innerHTML = "";
- } else {
- acts.innerHTML = `<button class="btn" type="button" onclick="showWhView('stock')">查看库存</button>`;
- }
- setCrumb("仓储管理", titles[currentWhView] || "库存信息表");
- markNav(`[data-wh-view="${currentWhView}"]`);
- document.querySelector('[data-group="warehouse"]')?.classList.add("open");
- if (currentWhView === "proc") {
- const p = parentWhOf(currentWh);
- if (!WH_PROC_PARENTS.includes(p?.id)) {
- currentWh = "h2so4";
- renderWhNav();
- renderWhProc();
- } else {
- renderWhNav();
- }
- } else {
- renderWhNav();
- }
- }
- function subWhDirHtml() {
- const node = WH_TREE.find((w) => w.id === currentWh);
- const children = node?.children || [];
- if (!children.length) return "";
- return `
- <article class="card">
- <div class="card-h"><h2>子库档案</h2><span class="muted">${children.length} 个仓</span></div>
- <div class="card-b" style="overflow:auto">
- <table class="wh-dir">
- <thead><tr><th>仓库编码</th><th>名称</th><th>类型</th><th>产线</th><th>允许作业</th><th>结存</th><th>仓管</th></tr></thead>
- <tbody>${children.map((c) => {
- const qty = stockOf([c.id]).reduce((a, s) => a + s.qty, 0);
- return `<tr class="row-link" onclick="selectWarehouse('${c.id}')">
- <td class="cell-no">${c.code}</td>
- <td><b>${c.name}</b></td>
- <td>${whKindLabel(c.kind)}</td>
- <td>${whLineLabel(c.line)}</td>
- <td>${whOpsHtml(c.ops)}</td>
- <td class="num">${fmtT(qty)}</td>
- <td>${c.manager}</td>
- </tr>`;
- }).join("")}</tbody>
- </table>
- </div>
- </article>`;
- }
- const INFO_STOCK_ITEMS = [
- { wh: "h2so4-98", name: "98%工业硫酸", tone: "tone-ind" },
- { wh: "h2so4-sul", name: "98%硫磺酸", tone: "tone-ind" },
- { wh: "h2so4-rg", name: "试剂硫酸", tone: "tone-rg" },
- { wh: "h2so4-50", name: "50%工业硫酸", tone: "tone-ind" },
- { wh: "h2so4-30", name: "30%工业硫酸", tone: "tone-ind" },
- { wh: "hcl-31", name: "31%合成盐酸", tone: "tone-hcl" }
- ];
- function inStockCardHtml() {
- const cards = INFO_STOCK_ITEMS.map((it) => {
- const s = WH_STOCK.find((x) => x.wh === it.wh && x.name === it.name);
- if (!s) return "";
- const chips = stockTankRows(s).map((t) => `<span>${t.label}</span>`).join("");
- return `
- <article class="wh-mini ${it.tone}" onclick="selectWarehouse('${s.wh}')">
- <div class="wh-mini-top">
- <div class="wh-mini-name"><i></i><b>${s.name}</b></div>
- <div class="qty">${fmtT(s.qty)}<span>${s.unit}</span></div>
- </div>
- <div class="tanks">${chips}</div>
- </article>`;
- }).join("");
- return `<div class="wh-stock-cards">${cards}</div>`;
- }
- function renderWhInfo() {
- const ids = whIdsOf(currentWh);
- const html = `${inStockCardHtml()}${tankTableHtml(ids)}`;
- document.getElementById("view-wh-info").innerHTML = html || `<article class="card"><div class="card-b"><div class="empty">该仓暂无库存明细。</div></div></article>`;
- }
- function renderWhProc() {
- document.getElementById("view-wh-proc").innerHTML = processListHtml() + convertListHtml();
- }
- function renderWhMode() {
- const isAcid = currentWh === "h2so4" || String(currentWh).startsWith("h2so4-");
- const structHtml = isAcid ? `
- <div class="wh-tree-box">
- <div class="wh-branch">
- <h4>原料库 · 可出库,也可加工</h4>
- <button type="button" onclick="selectWarehouse('h2so4-98')"><span>98%工业硫酸库</span><span class="grade ind">工业酸</span></button>
- <button type="button" onclick="selectWarehouse('h2so4-sul')"><span>硫磺酸库</span><span class="grade ind">工业酸</span></button>
- <button type="button" onclick="selectWarehouse('h2so4-rg')"><span>试剂硫酸库</span><span class="grade rg">试剂酸</span></button>
- </div>
- <div class="wh-branch">
- <h4>成品库 · 加水浓度</h4>
- <button type="button" onclick="selectWarehouse('h2so4-30')"><span>30%工业硫酸库</span><span class="muted">V0705A</span></button>
- <button type="button" onclick="selectWarehouse('h2so4-50')"><span>50%工业硫酸库</span><span class="muted">V0704</span></button>
- <button type="button" onclick="selectWarehouse('h2so4-custom')"><span>定制浓度库</span><span class="muted">其他浓度</span></button>
- </div>
- </div>` : "";
- document.getElementById("view-wh-mode").innerHTML = `
- ${subWhDirHtml()}
- ${isAcid ? `
- <article class="card">
- <div class="card-h"><h2>出入库 · 稀释加工 · 精制加工</h2><span class="grade raw">工业 / 试剂分线,不得混兑</span></div>
- <div class="card-b">
- <div class="wh-mode" style="margin-bottom:12px">
- <h3>原料酸 · 98% 工业硫酸</h3>
- <p>存放于 98%工业硫酸库。可以仓库出库直销;也可以稀释加工成 30%、50% 工业硫酸或其他定制浓度。工业酸不得与试剂酸混用。</p>
- <div class="wh-proc">
- <div class="box"><b>98%工业硫酸</b><span>98%工业硫酸库 · 可直销出库</span></div>
- <div class="arr">→</div>
- <div class="box"><b>稀释加工</b><span>硫酸质量守恒</span></div>
- <div class="arr">→</div>
- <div class="box"><b>30% / 50% / 定制</b><span>工业硫酸成品库</span></div>
- </div>
- </div>
- <div class="wh-mode">
- <h3>原料酸 · 试剂硫酸</h3>
- <p>存放于试剂硫酸库。可以仓库出库直销;也可以稀释加工成 30%、50% 试剂酸,或其他定制浓度。试剂酸线独立储罐、独立转换,不与工业酸互兑。</p>
- <div class="wh-proc">
- <div class="box"><b>试剂硫酸</b><span>试剂硫酸库 · 可直销出库</span></div>
- <div class="arr">→</div>
- <div class="box"><b>稀释加工</b><span>浓度按订单定制</span></div>
- <div class="arr">→</div>
- <div class="box"><b>30% / 50% / 定制</b><span>试剂酸成品库</span></div>
- </div>
- </div>
- ${structHtml}
- </div>
- </article>` : ""}`;
- }
- function tankTableHtml(ids) {
- 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"));
- if (!stocks.length) return "";
- const rows = [];
- stocks.forEach((s) => {
- stockTankRows(s).forEach((t) => rows.push({ s, t }));
- });
- return `<article class="card">
- <div class="card-h"><h2>${ids.some((id) => id === "pack" || String(id).startsWith("solid")) ? "仓位 / 品号明细" : "储罐库存明细表"}</h2><span class="muted">${rows.length} 条</span></div>
- <div class="card-b" style="overflow:auto">
- <table>
- <thead><tr><th>储罐位号</th><th>所属仓库</th><th>产线</th><th>储存品名</th><th>设计容积</th><th>现存量</th><th>占用</th><th>可收</th><th>状态</th></tr></thead>
- <tbody>${rows.map(({ s, t }) => {
- const cap = t.cap || s.tankCap || 80;
- const use = cap ? Math.min(100, Math.round(t.qty / cap * 100)) : 0;
- const room = Math.max(0, cap - t.qty);
- const st = use >= 90 ? "将满" : t.qty < 15 ? "偏低" : "正常";
- const cls = use >= 90 || t.qty < 15 ? "st-due" : "st-run";
- return `<tr>
- <td class="cell-no">${s.sku || t.label}</td>
- <td class="wh-path">${whNameOf(s.wh)}</td>
- <td>${gradeHtml(s) || (s.supplier ? `<span class="grade fin">${s.supplier}</span>` : "—")}</td>
- <td class="wh-stock-name">${s.name}${s.packSpec ? " · " + s.packSpec : ""}${s.supplier ? "(" + s.supplier + ")" : ""}</td>
- <td class="num">${fmtT(cap)} 吨</td>
- <td class="num">${fmtT(t.qty)} 吨</td>
- <td><div class="tank-use"><span class="bar"><i class="${use >= 90 ? "warn" : ""}" style="width:${use}%"></i></span>${use}%</div></td>
- <td class="num">${fmtT(room)}</td>
- <td><span class="st ${cls}">${st}</span></td>
- </tr>`;
- }).join("")}</tbody>
- </table>
- </div>
- </article>`;
- }
- function processListHtml() {
- const ids = whIdsOf(currentWh);
- const docs = WH_PROCESS.filter((d) => ids.includes(d.rawWh) || ids.includes(d.toWh));
- const sum = (key) => docs.reduce((a, d) => a + Number(d[key] || 0), 0);
- const finSum = docs.reduce((a, d) => a + Number(d.finIn != null ? d.finIn : d.outQty || 0), 0);
- const waterSum = docs.reduce((a, d) => a + Number(d.mode === "convert" ? 0 : d.water || 0), 0);
- const body = docs.length ? docs.map((d, i) => {
- const kind = d.mode === "convert" ? "形态转换" : "稀释加工";
- return `<tr>
- <td>${String(i + 1).padStart(2, "0")}</td>
- <td>${d.date}</td>
- <td class="cell-no">${d.no}</td>
- <td>${kind}</td>
- <td class="cell-no">${d.planNo || "—"}</td>
- <td class="col-name">${whNameOf(d.rawWh)}</td>
- <td>${d.rawName}</td>
- <td>${fmtT(d.rawQty)}</td>
- <td>${d.mode === "convert" ? "—" : fmtT(d.water || 0)}</td>
- <td class="col-name">${whNameOf(d.toWh)}</td>
- <td>${d.product}</td>
- <td>${fmtT(d.outQty)}</td>
- <td>${fmtT(d.finIn != null ? d.finIn : d.outQty)}</td>
- </tr>`;
- }).join("") : `<tr><td colspan="13" class="muted">暂无加工单。</td></tr>`;
- const foot = docs.length ? `<tfoot><tr>
- <td colspan="7">合计</td>
- <td>${fmtT(sum("rawQty"))}</td>
- <td>${fmtT(waterSum)}</td>
- <td colspan="2"></td>
- <td>${fmtT(sum("outQty"))}</td>
- <td>${fmtT(finSum)}</td>
- </tr></tfoot>` : "";
- return `<article class="card">
- <div class="card-h"><h2>加工单明细表</h2><span class="muted">${docs.length} 张</span></div>
- <div class="card-b" style="overflow:auto">
- <table class="proc-table wide">
- <thead><tr>
- <th>序号</th>
- <th>加工日期</th>
- <th>加工单号</th>
- <th>类型</th>
- <th>来源计划</th>
- <th>投料仓库</th>
- <th>投料品名</th>
- <th>投料量(t)</th>
- <th>加水量(t)</th>
- <th>产出仓库</th>
- <th>产出品名</th>
- <th>产出数量(t)</th>
- <th>产成品入库(t)</th>
- </tr></thead>
- <tbody>${body}</tbody>
- ${foot}
- </table>
- </div>
- </article>`;
- }
- function convertListHtml() {
- const ids = whIdsOf(currentWh);
- const rows = WH_LEDGER.filter((r) => r.doc === "形态转换单" && ids.includes(r.wh));
- const body = rows.length ? rows.map((r, i) => `<tr>
- <td>${String(i + 1).padStart(2, "0")}</td>
- <td>${r.date}</td>
- <td class="cell-no">${r.no}</td>
- <td class="col-name">${whNameOf(r.wh)}</td>
- <td>${r.product}</td>
- <td>${r.inQty ? fmtT(r.inQty) : "—"}</td>
- <td>${r.outQty ? fmtT(r.outQty) : "—"}</td>
- <td>${r.note || "—"}</td>
- </tr>`).join("") : `<tr><td colspan="8" class="muted">暂无形态转换。</td></tr>`;
- return `<article class="card">
- <div class="card-h"><h2>形态转换明细</h2><span class="muted">${rows.length} 笔</span></div>
- <div class="card-b" style="overflow:auto">
- <table class="proc-table wide">
- <thead><tr>
- <th>序号</th>
- <th>日期</th>
- <th>转换单号</th>
- <th>仓库名称</th>
- <th>物料名称</th>
- <th>入库(t)</th>
- <th>出库(t)</th>
- <th>说明</th>
- </tr></thead>
- <tbody>${body}</tbody>
- </table>
- </div>
- </article>`;
- }
- function acidTargetWh(pct) {
- if (pct === 30) return "h2so4-30";
- if (pct === 50) return "h2so4-50";
- return "h2so4-custom";
- }
- function acidProductName(grade, pct, raw) {
- if (raw && raw.wh === "h2so4-sul" && pct === 98) return "98%硫磺酸";
- if (grade === "reagent") return pct === 98 ? "试剂硫酸" : pct + "%试剂酸";
- return pct === 98 ? "98%工业硫酸" : pct + "%工业硫酸";
- }
- function acidRaws() {
- return WH_STOCK.filter((s) => s.wh === "h2so4-98" || s.wh === "h2so4-sul" || s.wh === "h2so4-rg");
- }
- function fillAcidTargets(raw, keep) {
- const sel = document.getElementById("acidForm").target;
- sel.innerHTML = `<option value="30">加水 · 30%</option>
- <option value="50">加水 · 50%</option>
- <option value="custom">加水 · 其他定制浓度</option>`;
- if (keep && [...sel.options].some((o) => o.value === keep)) sel.value = keep;
- else sel.value = "50";
- }
- function openAcidDlg() {
- const form = document.getElementById("acidForm");
- const raws = acidRaws();
- const prefer = currentWh === "h2so4-rg" ? "h2so4-rg" : currentWh === "h2so4-sul" ? "h2so4-sul" : "h2so4-98";
- form.rawId.innerHTML = raws.map((s) =>
- `<option value="${s.wh}|${s.name}" ${s.wh === prefer ? "selected" : ""}>${s.name} · 结存 ${fmtT(s.qty)} 吨</option>`
- ).join("");
- form.outQty.value = "20";
- form.customPct.value = "";
- onAcidRawChange();
- document.getElementById("acidDlg").showModal();
- }
- function closeAcidDlg() { document.getElementById("acidDlg").close(); }
- function onAcidRawChange() {
- const form = document.getElementById("acidForm");
- const [wh, name] = (form.rawId.value || "").split("|");
- const raw = WH_STOCK.find((s) => s.wh === wh && s.name === name);
- fillAcidTargets(raw, form.target.value);
- onAcidTarget();
- }
- function onAcidTarget() {
- const custom = document.getElementById("acidForm").target.value === "custom";
- document.getElementById("acidCustomRow").style.display = custom ? "" : "none";
- updateAcidCalc();
- }
- function acidCalc() {
- const form = document.getElementById("acidForm");
- const [wh, name] = (form.rawId.value || "").split("|");
- const raw = WH_STOCK.find((s) => s.wh === wh && s.name === name);
- if (!raw) return null;
- const tgt = form.target.value;
- const outQty = Number(form.outQty.value);
- if (!outQty || outQty <= 0) return { raw, error: "请填写成品数量" };
- const pct = tgt === "custom" ? Number(form.customPct.value) : Number(tgt);
- if (!pct || pct <= 0 || pct >= raw.conc) return { raw, error: "请填写有效目标浓度(须低于原料浓度)" };
- const rawQty = +(outQty * pct / raw.conc).toFixed(3);
- const water = +(outQty - rawQty).toFixed(3);
- return {
- raw, mode: "dilute", pct, outQty, rawQty, water, loss: 0,
- toWh: acidTargetWh(pct), product: acidProductName(raw.grade, pct, raw), grade: raw.grade,
- loc: assignTankLoc(acidTargetWh(pct), raw.grade),
- unitCost: +(raw.cost * raw.conc / pct).toFixed(2)
- };
- }
- function updateAcidCalc() {
- const c = acidCalc();
- const box = document.getElementById("acidCalc");
- if (!c) { box.textContent = "请选择原料酸。"; return; }
- if (c.error) { box.textContent = c.error; return; }
- box.innerHTML = `加水稀释:消耗 <b>${c.raw.name} ${fmtT(c.rawQty)} 吨</b>(库存 ${fmtT(c.raw.qty)})+ 工艺水 <b>${fmtT(c.water)} 吨</b>
- → 得到 <b>${c.product} ${fmtT(c.outQty)} 吨</b>,入 <b>${whNameOf(c.toWh)}</b>。产线(工业 / 试剂)保持不变。`;
- }
- function saveAcidConvert() {
- const c = acidCalc();
- if (!c || c.error) { toast(c?.error || "请完整填写"); return; }
- if (c.rawQty > c.raw.qty + 1e-9) { toast(`原料酸不足,仅剩 ${fmtT(c.raw.qty)} 吨`); return; }
- c.raw.qty = +(c.raw.qty - c.rawQty).toFixed(3);
- let fin = WH_STOCK.find((s) => s.wh === c.toWh && s.name === c.product);
- if (!fin) {
- fin = { wh: c.toWh, name: c.product, grade: c.grade, conc: c.pct, qty: 0, cost: c.unitCost, unit: "吨", loc: c.loc };
- WH_STOCK.push(fin);
- }
- fin.qty = +(fin.qty + c.outQty).toFixed(3);
- const no = "ZX-" + today.replace(/-/g, "") + "-" + String(++whDocSeq).padStart(3, "0");
- const cost = c.rawQty * c.raw.cost;
- const noteOut = `稀释加工成 ${c.product} ${fmtT(c.outQty)} 吨`;
- const noteIn = `由 ${c.raw.name} 加水 / 加工`;
- WH_LEDGER.unshift({
- date: today, doc: "形态转换单", no, wh: c.raw.wh, product: c.raw.name,
- inQty: 0, inCost: 0, outQty: c.rawQty, outCost: cost,
- balQty: c.raw.qty, price: c.raw.cost, balCost: c.raw.qty * c.raw.cost,
- note: noteOut
- });
- WH_LEDGER.unshift({
- date: today, doc: "形态转换单", no, wh: c.toWh, product: c.product,
- inQty: c.outQty, inCost: cost, outQty: 0, outCost: 0,
- balQty: fin.qty, price: fin.cost, balCost: fin.qty * fin.cost,
- note: noteIn
- });
- WH_PROCESS.unshift({
- no, date: today, batch: "TL-" + today.replace(/-/g, "") + "-" + String(whDocSeq).padStart(3, "0"),
- mode: "dilute", rawName: c.raw.name, rawWh: c.raw.wh, rawQty: c.rawQty, rawConc: c.raw.conc,
- water: c.water, loss: c.loss || 0, product: c.product, toWh: c.toWh, outQty: c.outQty,
- finIn: c.outQty, diluteBal: fin.qty, grade: c.grade, loc: c.loc
- });
- closeAcidDlg();
- toast(`已转换:${c.raw.name} ${fmtT(c.rawQty)} 吨 → ${c.product} ${fmtT(c.outQty)} 吨`);
- renderWarehouse();
- }
- function whSelectDoc(doc) {
- if (doc === "要货单") {
- toast("要货单未启用,不进入库存成本明细账");
- return;
- }
- currentWhDoc = doc;
- const sel = document.getElementById("whDocFilter");
- sel.value = doc === "库存成本明细账" ? "" : doc;
- renderWhTable();
- }
- function onWhDocFilter() {
- const v = document.getElementById("whDocFilter").value;
- currentWhDoc = v || "库存成本明细账";
- renderWhTable();
- }
- function renderWarehouse() {
- renderWhNav();
- renderWhInfo();
- renderWhMode();
- renderWhProc();
- renderWhKpis();
- renderWhTable();
- showWhView(currentWhView);
- }
- function setCrumb(parent, child) {
- document.getElementById("crumbMod").textContent = parent;
- const tail = document.getElementById("crumbTail");
- if (!child || child === parent) {
- tail.classList.add("hidden");
- } else {
- tail.classList.remove("hidden");
- document.getElementById("crumb").textContent = child;
- }
- }
- function markNav(sel) {
- document.querySelectorAll(".side-item").forEach((b) => b.classList.remove("active"));
- const el = document.querySelector(sel);
- if (!el) return;
- el.classList.add("active");
- el.closest(".nav-group")?.classList.add("open");
- }
- function hideAllMods() {
- ["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) => {
- document.getElementById(id).classList.add("hidden");
- });
- }
- function showPage(page) {
- const meta = PLACE_PAGES[page];
- if (!meta) return;
- currentMod = page;
- hideAllMods();
- document.getElementById("mod-placeholder").classList.remove("hidden");
- document.getElementById("phTitle").textContent = meta.title;
- document.getElementById("phDesc").textContent = meta.desc;
- document.getElementById("phCardTitle").textContent = meta.title;
- document.getElementById("phComing").textContent = meta.coming || "当前演示重点在运输计划与购买证管理。";
- setCrumb(meta.parent, meta.parent === meta.title ? "" : meta.title);
- markNav(`[data-page="${page}"]`);
- }
- function showMod(mod) {
- currentMod = mod;
- hideAllMods();
- const box = document.getElementById("mod-" + mod);
- if (box) box.classList.remove("hidden");
- markNav(`[data-mod="${mod}"]`);
- if (mod === "fleet") {
- showView(currentFleetView || "etc");
- return;
- }
- if (mod === "product") {
- setCrumb("CRM管理", "产品管理");
- renderProducts();
- }
- if (mod === "quote") {
- setCrumb("CRM管理", "报价单管理");
- renderQuotes();
- }
- if (mod === "contract") {
- setCrumb("CRM管理", "合同管理");
- renderContracts();
- }
- if (mod === "plan") {
- setCrumb("运输管理", "运输计划");
- renderPlans();
- }
- if (mod === "cert") {
- setCrumb("运输管理", "购买证管理");
- showCertView(currentCertId ? "detail" : "wait");
- }
- if (mod === "expense") {
- setCrumb("人事管理", "报销管理");
- renderExpense();
- }
- if (mod === "duty") {
- setCrumb("人事管理", "值班管理");
- renderDuty();
- }
- if (mod === "staff") {
- setCrumb("人事管理", "员工档案");
- renderStaff();
- }
- if (mod === "warehouse") {
- renderWarehouse();
- return;
- }
- if (mod === "archive") {
- setCrumb("运输管理", "运输归档");
- renderArchive();
- }
- if (mod === "task") {
- setCrumb("运输管理", "任务单管理");
- renderTasks();
- }
- }
- function daysBetween(a, b) {
- return Math.round((new Date(b) - new Date(a)) / 86400000);
- }
- function expMoney(n) {
- return "¥" + Number(n || 0).toLocaleString("zh-CN", { minimumFractionDigits: 2 });
- }
- function expFinalRole(x) {
- return "总经理";
- }
- function expOverdue(x) {
- if (!x.date || ["已归档", "已驳回"].includes(x.status)) return false;
- return daysBetween(x.date, today) > 45;
- }
- function expBudgetLeft(dept) {
- const b = EXP_BUDGET[dept] || { total: 0, used: 0 };
- return b.total - b.used;
- }
- function expOpenLoan(user) {
- return expLoans.find((l) => l.user === user && l.status === "未清" && l.remain > 0);
- }
- function expStatusPill(st) {
- const map = {
- "草稿": "st-stop", "已驳回": "st-due", "待财务初审": "st-fix", "待批准": "st-fix",
- "待终审": "st-fix", "待付款": "st-run", "待归档": "st-run", "已归档": "st-stop",
- "待批准": "st-fix", "已批准": "st-run"
- };
- const label = st === "待终审" ? "待领导审核" : st;
- return `<span class="st ${map[st] || "st-stop"}">${label}</span>`;
- }
- function expLog(x, who, act, note) {
- x.logs = x.logs || [];
- const hh = new Date().getHours().toString().padStart(2, "0");
- const mm = new Date().getMinutes().toString().padStart(2, "0");
- x.logs.push({ t: today + " " + hh + ":" + mm, who, act, note: note || "" });
- }
- function expById(id) { return expenses.find((x) => x.id === id); }
- function kindTag(k) {
- const sq = k === "备用金" || k === "冲备用金";
- return `<span class="kind-tag ${sq ? "sq" : "bx"}">${k}</span>`;
- }
- function prePending(p) { return p.status === "待财务初审" || p.status === "待批准"; }
- function renderExpense() {
- const flowEl = document.getElementById("expFlow");
- flowEl.classList.remove("hidden");
- const bxDraft = expenses.filter((x) => x.status === "草稿" || x.status === "已驳回").length;
- const bxFin = expenses.filter((x) => x.status === "待财务初审").length;
- const bxLead = expenses.filter((x) => x.status === "待终审").length;
- const bxPay = expenses.filter((x) => x.status === "待付款").length;
- const bxArch = expenses.filter((x) => x.status === "待归档").length;
- const sqFin = preExps.filter(prePending).length;
- const sqOk = preExps.filter((p) => p.status === "已批准").length;
- const sqMine = preExps.length;
- const bxOn = (key) => {
- if (key === "我的报销") return expTab === "我的报销";
- if (key === "待财务初审") return expTab === "待财务初审" && expFinKind !== "备用金";
- return expTab === key;
- };
- const sqOn = (key) => {
- if (key === "申请") return expTab === "备用金申请";
- if (key === "待财务初审") return expTab === "待财务初审" && expFinKind !== "报销";
- if (key === "冲销") return expTab === "借款";
- return false;
- };
- const expCell = (it, last) => {
- if (it.ghost) {
- return `<div class="exp-cell ghost${last ? " last" : ""}"><span class="t"><b>${it.name}</b><em>${it.hint}</em></span></div>`;
- }
- return `<button class="exp-cell ${it.on ? "on" : ""}${last ? " last" : ""}" type="button" onclick="${it.click}">
- <span class="t"><b>${it.name}</b><em>${it.hint}</em></span>
- <span class="c">${it.n}</span>
- </button>`;
- };
- const bxCells = [
- { hint: "贴发票填报", name: "经办填报", n: bxDraft, on: bxOn("我的报销"), click: "setExpTab('我的报销')" },
- { hint: "核发票预算", name: "财务核票", n: bxFin, on: bxOn("待财务初审"), click: "setExpTab('待财务初审','报销')" },
- { hint: "总经理审", name: "领导审核", n: bxLead, on: bxOn("待终审"), click: "setExpTab('待终审')" },
- { hint: "打款或冲销", name: "出纳付款", n: bxPay, on: bxOn("待付款"), click: "setExpTab('待付款')" },
- { hint: "凭证装订", name: "制单归档", n: bxArch, on: bxOn("待归档"), click: "setExpTab('待归档')" }
- ].map((it, i, a) => expCell(it, i === a.length - 1)).join("");
- const sqCells = [
- { hint: "无票先借", name: "提出申请", n: sqMine, on: sqOn("申请"), click: "setExpTab('备用金申请')" },
- { hint: "核事由额度", name: "财务核额", n: sqFin, on: sqOn("待财务初审"), click: "setExpTab('待财务初审','备用金')" },
- { hint: "通过后可支", name: "批准待支", n: sqOk, on: expTab === "备用金申请" && !sqFin, click: "setExpTab('备用金申请')" },
- { hint: "未清余额", name: "报销冲销", n: expLoans.filter((l) => l.status === "未清").length, on: sqOn("冲销"), click: "setExpTab('借款')" },
- { ghost: true, name: "汇入报销", hint: "贴票后走上线" }
- ].map((it, i, a) => expCell(it, i === a.length - 1)).join("");
- flowEl.innerHTML = `
- <div class="exp-map">
- <div class="exp-map-grid">
- <div class="exp-map-head">
- <div></div>
- <div class="exp-colh"><b>1</b>发起</div>
- <div class="exp-colh"><b>2</b>财务初审</div>
- <div class="exp-colh"><b>3</b>审用</div>
- <div class="exp-colh"><b>4</b>资金</div>
- <div class="exp-colh"><b>5</b>收口</div>
- </div>
- <div class="exp-map-row bx">
- <div class="exp-map-lab"><b>报销</b><span>事后凭票</span></div>
- ${bxCells}
- </div>
- <div class="exp-map-row sq">
- <div class="exp-map-lab"><b>备用金</b><span>事前借款</span></div>
- ${sqCells}
- </div>
- </div>
- </div>`;
- const overdue = expenses.filter(expOverdue);
- const rejectN = expenses.filter((x) => x.status === "已驳回").length;
- const uncleared = expLoans.filter((l) => l.status === "未清").length;
- document.getElementById("expAlerts").innerHTML = `
- <div class="alert ${overdue.length ? "red" : "ok"}">逾期未结 ${overdue.length} 笔(业务结束超 45 天)</div>
- <div class="alert ${rejectN ? "red" : "ok"}">待补资料重提 ${rejectN} 笔</div>
- <div class="alert ${uncleared ? "" : "ok"}">未清借款 ${uncleared} 笔 · 前款不清后款不借</div>`;
- const hints = {
- "全部": "报销凭发票事后结算;备用金是借款核额后先支出事后凭票报销冲销。",
- "我的报销": "经办人工作台:贴发票填报。若已有备用金,结算选冲销,勿再走一笔新付款。",
- "备用金申请": "事前申请,财务初审通过后方可支出,事后必须凭票报销冲销。",
- "待财务初审": "报销看发票,通过后去领导审核;备用金只核额度,通过后即可支出。",
- "待终审": "仅报销单。总经理可同意或写明原因驳回。备用金不经领导审核。",
- "待付款": "仅报销单。审批通过后出纳对公支付或打个人卡;有备用金/借款则冲销。",
- "待归档": "仅报销单。会计录入凭证,报销单、发票、附件装订归档。",
- "借款": "备用金/借款台账。报销时选择冲销;未结清不得再借。"
- };
- document.getElementById("expHint").textContent = hints[expTab] || "";
- const bar = document.getElementById("expToolbar");
- if (expTab === "备用金申请") {
- bar.innerHTML = `<button class="btn btn-primary" type="button" onclick="openPreExpDlg()">+ 备用金申请</button>
- <input id="expQ" type="search" placeholder="申请号 / 申请人 / 事项" oninput="renderExpense()" />`;
- renderPreExpTable();
- return;
- }
- if (expTab === "借款") {
- bar.innerHTML = `<span class="muted">报销结算选「冲销借款」后在此核销余额</span>
- <input id="expQ" type="search" placeholder="借款号 / 经办人" oninput="renderExpense()" />`;
- renderLoanTable();
- return;
- }
- if (expTab === "待财务初审") {
- const nBx = expenses.filter((x) => x.status === "待财务初审").length;
- const nSq = preExps.filter(prePending).length;
- bar.innerHTML = `
- <div class="plan-tabs" id="expFinTabs">
- <button class="${!expFinKind ? "on" : ""}" type="button" onclick="setExpTab('待财务初审','')">全部 ${nBx + nSq}</button>
- <button class="${expFinKind === "报销" ? "on" : ""}" type="button" onclick="setExpTab('待财务初审','报销')">报销 ${nBx}</button>
- <button class="${expFinKind === "备用金" ? "on" : ""}" type="button" onclick="setExpTab('待财务初审','备用金')">备用金 ${nSq}</button>
- </div>
- <span class="split"></span>
- <input id="expQ" type="search" placeholder="单号 / 经办人 / 事由" oninput="renderFinanceReview()" />`;
- renderFinanceReview();
- return;
- }
- bar.innerHTML = `
- <button class="btn btn-primary" type="button" onclick="openExpenseDlg()">+ 填报报销单</button>
- <button class="btn" type="button" onclick="openPreExpDlg()">备用金申请</button>
- <span class="split"></span>
- <input id="expQ" type="search" placeholder="单号 / 经办人 / 部门 / 事由" oninput="renderExpenseList()" />
- <select id="expSt" onchange="renderExpenseList()">
- <option value="">全部状态</option>
- <option>草稿</option><option>待财务初审</option>
- <option value="待终审">待领导审核</option><option>待付款</option><option>待归档</option>
- <option>已归档</option><option>已驳回</option>
- </select>`;
- renderExpenseList();
- }
- function setExpTab(tab, kind) {
- if (tab === "pre") tab = "备用金申请";
- expTab = tab;
- expFinKind = tab === "待财务初审" ? (kind || "") : "";
- document.querySelectorAll("#expTabs button").forEach((b) => b.classList.toggle("on", b.dataset.tab === tab));
- renderExpense();
- }
- function renderExpenseList() {
- const qEl = document.getElementById("expQ");
- const stEl = document.getElementById("expSt");
- const q = (qEl?.value || "").trim();
- const st = stEl?.value || "";
- let rows = expenses.slice();
- if (expTab === "我的报销") rows = rows.filter((x) => ["草稿", "已驳回"].includes(x.status) || x.user === "周建军");
- else if (["待财务初审", "待终审", "待付款", "待归档"].includes(expTab)) rows = rows.filter((x) => x.status === expTab);
- if (st) rows = rows.filter((x) => x.status === st);
- if (q) rows = rows.filter((x) => [x.no, x.user, x.dept, x.type, x.remark, x.payWay].join(" ").includes(q));
- document.getElementById("expCount").textContent = `共 ${rows.length} 条`;
- document.getElementById("expTable").innerHTML = rows.length ? `<table class="erp-table" style="min-width:1180px">
- <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>${rows.map((x) => `<tr>
- <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>
- <td>${x.user}</td><td>${x.dept}</td><td>${x.type}</td>
- <td class="num">${expMoney(x.amount)}</td><td>${x.date}</td>
- <td>${(x.invoices || []).length} 张</td>
- <td>${x.payWay}${x.loanNo ? `<div class="muted">${x.loanNo}</div>` : ""}</td>
- <td>${expFinalRole(x)}</td>
- <td>${expStatusPill(x.status)}</td>
- </tr>`).join("")}</tbody></table>` : `<div class="empty">本节点暂无单据</div>`;
- }
- function expRowActs(x) {
- const d = `<button class="btn btn-sm" type="button" onclick="openExpDetail(${x.id})">详情</button>`;
- if (x.status === "草稿" || x.status === "已驳回") return `${d}<button class="btn btn-sm btn-primary" type="button" onclick="expSubmit(${x.id})">提交</button>`;
- 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>`;
- 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>`;
- if (x.status === "待付款") return `${d}<button class="btn btn-sm btn-primary" type="button" onclick="expAdvance(${x.id})">${x.payWay === "冲销借款" ? "冲借款" : "付款"}</button>`;
- if (x.status === "待归档") return `${d}<button class="btn btn-sm btn-primary" type="button" onclick="expAdvance(${x.id})">制单归档</button>`;
- return d;
- }
- function renderFinanceReview() {
- const q = (document.getElementById("expQ")?.value || "").trim();
- const bx = expenses.filter((x) => x.status === "待财务初审").map((x) => ({
- kind: "报销", no: x.no, user: x.user, dept: x.dept, matter: x.type, amount: x.amount, date: x.date,
- extra: (x.invoices || []).length + " 张发票" + (x.preNo ? " · 冲 " + x.preNo : ""), id: x.id
- }));
- const sq = preExps.filter(prePending).map((p) => ({
- kind: "备用金", no: p.no, user: p.user, dept: p.dept, matter: p.type, amount: p.amount, date: p.date,
- extra: p.remark, id: p.no
- }));
- const match = (r) => !q || [r.no, r.user, r.dept, r.matter, r.extra].join(" ").includes(q);
- const bxRows = bx.filter(match);
- const sqRows = sq.filter(match);
- const showBx = expFinKind !== "备用金";
- const showSq = expFinKind !== "报销";
- const nBx = showBx ? bxRows.length : 0;
- const nSq = showSq ? sqRows.length : 0;
- const finTable = (kind, rows) => {
- if (!rows.length) return `<div class="empty">${kind}暂无待初审</div>`;
- const bx = kind === "报销";
- return `<table class="erp-table" style="min-width:1080px">
- <thead><tr><th>单号</th><th>经办人</th><th>部门</th><th>事项</th><th>金额</th><th>日期</th><th>初审看什么</th><th>通过后</th></tr></thead>
- <tbody>${rows.map((r) => `<tr>
- <td class="cell-no${bx ? " row-link" : ""}"${bx ? ` onclick="openExpDetail(${r.id})"` : ""}>${kindTag(r.kind)}${r.no}</td>
- <td>${r.user}</td><td>${r.dept}</td><td>${r.matter}</td>
- <td class="num">${expMoney(r.amount)}</td><td>${r.date}</td>
- <td>${bx ? "发票真伪、附件、预算" : "事由与额度(无发票)"}<div class="muted">${r.extra || ""}</div></td>
- <td>${bx ? "进入领导审核" : "批准先支出,事后凭票报销冲销"}</td>
- </tr>`).join("")}</tbody></table>`;
- };
- document.getElementById("expCount").textContent = `待初审 ${nBx + nSq} 条(报销 ${nBx} · 备用金 ${nSq})`;
- document.getElementById("expTable").innerHTML = `
- ${showBx ? `<div class="fin-sec">${finTable("报销", bxRows)}</div>` : ""}
- ${showSq ? `<div class="fin-sec">${finTable("备用金", sqRows)}</div>` : ""}`;
- }
- function renderPreExpTable() {
- const q = (document.getElementById("expQ")?.value || "").trim();
- const rows = preExps.filter((p) => !q || [p.no, p.user, p.dept, p.type, p.remark].join(" ").includes(q));
- document.getElementById("expCount").textContent = `共 ${rows.length} 条备用金申请`;
- document.getElementById("expTable").innerHTML = `<table class="erp-table" style="min-width:880px">
- <thead><tr><th>申请号</th><th>申请人</th><th>部门</th><th>事项</th><th>预估金额</th><th>日期</th><th>状态</th><th>事由</th></tr></thead>
- <tbody>${rows.map((p) => `<tr>
- <td class="cell-no">${kindTag("备用金")}${p.no}</td><td>${p.user}</td><td>${p.dept}</td><td>${p.type}</td>
- <td>${expMoney(p.amount)}</td><td>${p.date}</td><td>${expStatusPill(p.status)}</td><td>${p.remark}</td>
- </tr>`).join("")}</tbody></table>`;
- }
- function renderLoanTable() {
- const q = (document.getElementById("expQ")?.value || "").trim();
- const rows = expLoans.filter((l) => !q || [l.no, l.user, l.dept].join(" ").includes(q));
- document.getElementById("expCount").textContent = `共 ${rows.length} 笔借款`;
- document.getElementById("expTable").innerHTML = `<table class="erp-table" style="min-width:880px">
- <thead><tr><th>借款号</th><th>经办人</th><th>部门</th><th>借款额</th><th>未冲余额</th><th>日期</th><th>状态</th></tr></thead>
- <tbody>${rows.map((l) => `<tr>
- <td class="cell-no">${l.no}</td><td>${l.user}</td><td>${l.dept}</td>
- <td>${expMoney(l.amount)}</td><td>${expMoney(l.remain)}</td><td>${l.date}</td>
- <td>${l.status === "未清" ? '<span class="st st-fix">未清</span>' : '<span class="st st-run">已清</span>'}</td>
- </tr>`).join("")}</tbody></table>`;
- }
- function previewExpFiles(input) {
- expFiles = [...(input.files || [])].map((f) => f.name);
- document.getElementById("expFileChips").innerHTML = expFiles.map((n) => `<span class="chip">${n}</span>`).join("") || "";
- }
- function expStaffDept(name) {
- return EXP_STAFF.find((s) => s.name === name)?.dept || "行政人事";
- }
- function syncExpTitle() {
- const f = document.getElementById("expenseForm");
- if (!f) return;
- const user = f.user.value;
- const d = (f.date.value || today).replaceAll("-", "");
- f.title.value = `${user}报销单${d}${String(expSeq).padStart(3, "0")}`;
- }
- function onExpUserChange() { syncExpTitle(); }
- function expLineSum() {
- return expLines.reduce((s, r) => s + Number(r.amount || 0), 0);
- }
- function renderExpLines() {
- const body = document.getElementById("expLineBody");
- const opts = `<option value="">请选择费用项目</option>` + EXP_TYPES.map((t) => `<option>${t}</option>`).join("");
- body.innerHTML = expLines.map((r, i) => `<tr>
- <td>${i + 1}</td>
- <td><select class="${r.type ? "" : "placeholder"}" required onchange="expLines[${i}].type=this.value;this.classList.toggle('placeholder',!this.value)">${opts}</select></td>
- <td><input type="number" min="0" step="0.01" value="${Number(r.amount || 0).toFixed(2)}" oninput="expLines[${i}].amount=this.value;updateExpSums()" /></td>
- <td><input placeholder="请输入报销事由" value="${(r.reason || "").replaceAll('"', """)}" oninput="expLines[${i}].reason=this.value" /></td>
- <td>
- <div class="exp-datewrap ${r.date ? "has" : ""}">
- <svg viewBox="0 0 24 24"><rect x="4" y="6" width="16" height="14" rx="2"/><path d="M8 4v4M16 4v4M4 11h16"/></svg>
- <input type="date" value="${r.date || ""}" onchange="expLines[${i}].date=this.value;this.parentElement.classList.toggle('has',!!this.value)" aria-label="发生时间,可选" />
- <span class="exp-date-ph">可选</span>
- </div>
- </td>
- </tr>`).join("");
- document.querySelectorAll("#expLineBody select").forEach((sel, i) => { sel.value = expLines[i].type || ""; sel.classList.toggle("placeholder", !sel.value); });
- updateExpSums();
- }
- function addExpLine() {
- expLines.push({ type: "", amount: 0, reason: "", date: "" });
- renderExpLines();
- }
- function removeExpLine(i) {
- expLines.splice(i, 1);
- if (!expLines.length) expLines.push({ type: "", amount: 0, reason: "", date: "" });
- renderExpLines();
- }
- function selectedInvoices() {
- return invWallet.filter((x) => selectedInvIds.includes(x.id));
- }
- function updateExpSums() {
- document.getElementById("expLineSum").textContent = expMoney(expLineSum());
- const picked = selectedInvoices();
- const used = picked.reduce((s, x) => s + x.amount, 0);
- const bal = invWallet.reduce((s, x) => s + x.amount, 0) - used;
- document.getElementById("expInvBal").textContent = picked.length ? expMoney(Math.max(0, bal)) : expMoney(0);
- document.getElementById("expInvSum").textContent = picked.length ? expMoney(used) : expMoney(0);
- document.getElementById("expInvChips").innerHTML = picked.map((x) =>
- `<span class="chip">${x.name} ${expMoney(x.amount)}</span>`).join("");
- }
- function openInvPick() {
- const box = document.getElementById("invPickList");
- box.innerHTML = invWallet.map((x) => {
- const on = selectedInvIds.includes(x.id);
- return `<button class="inv-pick-item ${on ? "on" : ""}" type="button" data-id="${x.id}" onclick="toggleInvPick(${x.id})">
- <input type="checkbox" ${on ? "checked" : ""} onclick="event.stopPropagation();toggleInvPick(${x.id})" />
- <div><b>${x.name}</b><div class="muted">${x.date}</div></div>
- <span class="amt">${expMoney(x.amount)}</span>
- </button>`;
- }).join("");
- document.getElementById("invPickDialog").showModal();
- }
- function toggleInvPick(id) {
- if (selectedInvIds.includes(id)) selectedInvIds = selectedInvIds.filter((x) => x !== id);
- else selectedInvIds.push(id);
- openInvPick();
- }
- function confirmInvPick() {
- document.getElementById("invPickDialog").close();
- updateExpSums();
- }
- function openExpenseDlg(preNo) {
- const f = document.getElementById("expenseForm");
- f.reset();
- f.user.innerHTML = EXP_STAFF.map((s) => `<option value="${s.name}">${s.name}</option>`).join("");
- f.user.value = "吴九华";
- f.date.value = today;
- expFiles = [];
- selectedInvIds = [];
- expLines = [{ type: "", amount: 0, reason: "", date: "" }];
- document.getElementById("expFileChips").innerHTML = "";
- if (preNo) {
- const p = preExps.find((x) => x.no === preNo);
- if (p) {
- f.user.value = p.user;
- f.remark.value = p.remark;
- expLines = [{ type: p.type === "出差" ? "差旅费" : p.type === "招待" ? "招待费" : p.type === "采购" ? "采购费" : "其他", amount: p.amount, reason: p.remark, date: today }];
- }
- }
- syncExpTitle();
- renderExpLines();
- document.getElementById("expenseDialog").showModal();
- }
- function closeExpenseDlg() { document.getElementById("expenseDialog").close(); }
- function openPreExpDlg() {
- document.getElementById("preExpForm").reset();
- document.getElementById("preExpForm").user.value = "周建军";
- document.getElementById("preExpDialog").showModal();
- }
- function savePreExp() {
- const f = document.getElementById("preExpForm");
- if (!f.reportValidity()) return;
- const no = "SQ-" + today.replaceAll("-", "") + "-" + String(preSeq++).padStart(3, "0");
- 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() });
- document.getElementById("preExpDialog").close();
- expTab = "待财务初审";
- expFinKind = "备用金";
- updateBadges();
- renderExpense();
- toast("备用金已提交财务初审,通过后再支出");
- }
- function approvePreExp(no, ok) {
- const p = preExps.find((x) => x.no === no);
- if (!p) return;
- p.status = ok ? "已批准" : "已驳回";
- updateBadges();
- renderExpense();
- toast(ok ? "备用金财务初审通过,可以先支出,事后凭票报销冲销" : "备用金申请已驳回");
- }
- function saveExpense(submit) {
- const f = document.getElementById("expenseForm");
- if (!f.reportValidity()) return;
- const lines = expLines.filter((r) => r.type || Number(r.amount) > 0 || (r.reason || "").trim());
- if (!lines.length || lines.some((r) => !r.type || Number(r.amount) <= 0 || !(r.reason || "").trim())) {
- toast("请完整填写费用项目、金额和报销事由");
- return;
- }
- const user = f.user.value.trim();
- const dept = expStaffDept(user);
- const amt = expLineSum();
- const type = lines[0].type;
- const remark = (f.remark.value.trim() || lines.map((r) => r.reason).filter(Boolean).join(";"));
- const invoices = selectedInvoices().map((x) => x.name).concat(expFiles);
- const loan = expOpenLoan(user);
- const payWay = loan ? "冲销借款" : "个人银行卡";
- const loanNo = loan ? loan.no : "";
- const over = amt > expBudgetLeft(dept);
- const bizDate = lines.map((r) => r.date).filter(Boolean).sort()[0] || f.date.value;
- const late = daysBetween(bizDate, today) > 45;
- const no = "BX-" + (f.date.value || today).replaceAll("-", "") + "-" + String(expSeq++).padStart(3, "0");
- const x = {
- id: Date.now(), no, user, dept, type, amount: amt, title: f.title.value.trim(),
- date: bizDate, submit: submit ? today : "", status: submit ? (late ? "已驳回" : "待财务初审") : "草稿",
- remark, invoices, lines: lines.map((r) => ({ ...r, amount: Number(r.amount) })),
- payWay, loanNo, bank: "", preNo: "", overBudget: over, logs: []
- };
- if (!submit) expLog(x, user, "保存草稿", f.title.value.trim());
- else if (late) expLog(x, "系统", "逾期退回", "业务结束已超过 45 天,按制度不予受理");
- else expLog(x, user, "提交报销", (x.invoices.length || 0) + " 张附件 / 发票");
- expenses.unshift(x);
- closeExpenseDlg();
- expTab = submit && !late ? "待财务初审" : "我的报销";
- updateBadges();
- renderExpense();
- toast(late ? "已超过报销时限,系统退回" : submit ? "已提交审批" : "已保存草稿");
- }
- function expSubmit(id) {
- const x = expById(id);
- if (!x) return;
- if (!(x.invoices || []).length) { toast("请先在详情中确认已贴发票"); openExpDetail(id); return; }
- if (expOverdue(x)) {
- x.status = "已驳回";
- expLog(x, "系统", "逾期退回", "业务结束已超过 45 天,按制度不予受理");
- toast("逾期不予受理");
- } else {
- x.status = "待财务初审";
- x.submit = today;
- expLog(x, x.user, "提交报销", "补资料后重新发起");
- toast("已提交财务初审");
- }
- updateBadges();
- renderExpense();
- }
- function expAdvance(id) {
- const x = expById(id);
- if (!x) return;
- if (x.status === "待财务初审") {
- x.status = "待终审";
- x.finalRole = expFinalRole(x);
- expLog(x, "财务会计", "财务初审通过", x.overBudget ? "票据合规,超预算转总经理专项终审" : "发票、附件、制度与预算核验通过");
- toast("财务初审通过,进入领导审核");
- } else if (x.status === "待终审") {
- x.status = "待付款";
- expLog(x, expFinalRole(x), "终审同意", "");
- toast("终审同意,请出纳付款");
- } else if (x.status === "待付款") {
- if (x.payWay === "冲销借款" && x.loanNo) {
- const l = expLoans.find((i) => i.no === x.loanNo);
- if (l) {
- l.remain = Math.max(0, l.remain - x.amount);
- if (l.remain === 0) l.status = "已清";
- }
- expLog(x, "出纳", "已冲借款", `冲销 ${x.loanNo}`);
- toast("已冲销借款");
- } else {
- expLog(x, "出纳", "已付款", x.payWay + " " + expMoney(x.amount));
- toast("出纳已付款");
- }
- x.status = "待归档";
- } else if (x.status === "待归档") {
- x.status = "已归档";
- x.voucher = "记-" + today.replaceAll("-", "").slice(0, 6) + "-" + String(80 + (x.id % 20));
- expLog(x, "会计", "制单归档", "凭证 " + x.voucher + ",发票装订归档");
- toast("已制单归档");
- }
- updateBadges();
- renderExpense();
- const dlg = document.getElementById("expDetailDialog");
- if (dlg.open) openExpDetail(id);
- }
- function openExpReject(id) {
- expRejectId = id;
- document.getElementById("expRejectForm").reset();
- document.getElementById("expRejectDlg").showModal();
- }
- function confirmExpReject() {
- const f = document.getElementById("expRejectForm");
- if (!f.reportValidity()) return;
- const x = expById(expRejectId);
- if (!x) return;
- const reason = f.reason.value.trim();
- const who = x.status === "待财务初审" ? "财务会计" : x.status === "待终审" ? expFinalRole(x) : (x.dept + "负责人");
- const act = x.status === "待财务初审" ? "退回补资料" : "驳回";
- x.status = "已驳回";
- expLog(x, who, act, reason);
- document.getElementById("expRejectDlg").close();
- document.getElementById("expDetailDialog").close();
- updateBadges();
- renderExpense();
- toast("已退回经办人,请补资料后重提");
- }
- function openExpDetail(id) {
- const x = expById(id);
- if (!x) return;
- document.getElementById("expDetailTitle").textContent = x.no + " · " + x.status;
- const left = expBudgetLeft(x.dept);
- document.getElementById("expDetailBody").innerHTML = `
- <div class="split-h">
- <div>
- <h4>报销信息</h4>
- <div class="kv">
- ${kv("经办人", x.user)}${kv("部门", x.dept)}${kv("类别", x.type)}
- ${kv("金额", expMoney(x.amount))}${kv("发生日", x.date)}${kv("提交日", x.submit || "—")}
- ${kv("结算", x.payWay)}${kv("借款", x.loanNo || "无")}${kv("账户", x.bank || "—")}
- ${kv("备用金申请", x.preNo || "无")}${kv("终审角色", expFinalRole(x))}
- ${kv("预算剩余", expMoney(left) + (x.overBudget ? "(本单超预算)" : ""))}
- ${kv("凭证", x.voucher || "—")}
- </div>
- <p style="margin-top:10px">${x.remark || ""}</p>
- </div>
- <div>
- <h4>原始单据</h4>
- <div class="chips">${(x.invoices || []).map((n) => `<span class="chip">${n}</span>`).join("") || `<span class="muted">尚未贴票</span>`}</div>
- <h4 style="margin-top:16px">审批轨迹</h4>
- <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>
- </div>
- </div>`;
- document.getElementById("expDetailFoot").innerHTML = `
- <button class="btn" type="button" onclick="document.getElementById('expDetailDialog').close()">关闭</button>
- ${expRowActs(x).replace(`<button class="btn btn-sm" type="button" onclick="openExpDetail(${x.id})">详情</button>`, "")}`;
- document.getElementById("expDetailDialog").showModal();
- }
- function dutyPad(n) { return String(n).padStart(2, "0"); }
- function dutyDateStr(y, m, d) { return `${y}-${dutyPad(m)}-${dutyPad(d)}`; }
- function dutyOfMonth() {
- const prefix = `${dutyYM.y}-${dutyPad(dutyYM.m)}-`;
- return dutyShifts.filter((s) => s.date.startsWith(prefix));
- }
- function dutyChipText(s) {
- const names = (s.people || []).join("、");
- const post = s.post ? ` · ${s.post}` : "";
- return `${DUTY_TYPE[s.type] || s.type} ${names}${post}`;
- }
- function renderDuty() {
- document.getElementById("dutyMonthLab").textContent = `${dutyYM.y}年${dutyYM.m}月`;
- const monthRows = dutyOfMonth();
- const countType = (t) => monthRows.filter((s) => s.type === t).reduce((n, s) => n + (s.people || []).length, 0);
- const nAm = countType("am");
- const nPm = countType("pm");
- const nWatch = countType("watch");
- const nOther = countType("other");
- const nSum = nAm + nPm + nWatch + nOther;
- document.getElementById("dutyPills").innerHTML = `
- <span class="duty-pill"><i class="am"></i>早班 <b>${nAm}</b></span>
- <span class="duty-pill"><i class="pm"></i>晚班 <b>${nPm}</b></span>
- <span class="duty-pill"><i class="watch"></i>值班 <b>${nWatch}</b></span>
- <span class="duty-sum">本月合计 <b>${nSum}</b></span>`;
- const peopleMap = {};
- monthRows.forEach((s) => (s.people || []).forEach((p) => { peopleMap[p] = (peopleMap[p] || 0) + 1; }));
- const peopleOrder = [...DUTY_STAFF].filter((p) => peopleMap[p]).sort((a, b) => peopleMap[b] - peopleMap[a] || DUTY_STAFF.indexOf(a) - DUTY_STAFF.indexOf(b));
- document.getElementById("dutyPeople").innerHTML = peopleOrder.length
- ? `<span>本月人员</span>` + peopleOrder.map((p) => `<em>${p}</em> <b>${peopleMap[p]}</b>`).join("")
- : `<span>本月人员</span><span>暂无</span>`;
- const y = dutyYM.y, m = dutyYM.m;
- const first = new Date(y, m - 1, 1);
- const start = new Date(y, m - 1, 1 - first.getDay());
- const last = new Date(y, m, 0);
- const endPad = 6 - last.getDay();
- const end = new Date(y, m - 1, last.getDate() + endPad);
- const cells = [];
- for (let t = start.getTime(); t <= end.getTime(); t += 86400000) {
- const dt = new Date(t);
- const yy = dt.getFullYear(), mm = dt.getMonth() + 1, dd = dt.getDate();
- const key = dutyDateStr(yy, mm, dd);
- const out = mm !== m;
- const shifts = dutyShifts.filter((s) => s.date === key);
- const chips = shifts.map((s) => `<span class="duty-chip ${s.type}">${dutyChipText(s)}</span>`).join("");
- const empty = !out && !shifts.length ? `<span class="duty-empty">未排班,点击配置</span>` : "";
- cells.push(`<button class="duty-day${out ? " out" : ""}${key === DUTY_TODAY ? " today" : ""}" type="button" onclick="openDutyDay('${key}')">
- <div class="num">${dd}</div>${chips}${empty}
- </button>`);
- }
- document.getElementById("dutyGrid").innerHTML = cells.join("");
- }
- function shiftDutyMonth(delta) {
- dutyYM.m += delta;
- if (dutyYM.m < 1) { dutyYM.m = 12; dutyYM.y -= 1; }
- if (dutyYM.m > 12) { dutyYM.m = 1; dutyYM.y += 1; }
- renderDuty();
- }
- function goDutyToday() {
- const [y, m] = DUTY_TODAY.split("-").map(Number);
- dutyYM = { y, m };
- renderDuty();
- }
- function copyDutyLink() {
- const url = location.href.split("#")[0] + "#/duty/schedule";
- if (navigator.clipboard?.writeText) navigator.clipboard.writeText(url).catch(() => {});
- toast("已复制值班排班链接");
- }
- function renderDutyStaffPick() {
- document.getElementById("dutyStaffPick").innerHTML = DUTY_STAFF.map((n) =>
- `<button class="chip pick${dutyPickPeople.includes(n) ? " on" : ""}" type="button" onclick="toggleDutyPerson('${n}')">${n}</button>`
- ).join("");
- }
- function toggleDutyPerson(name) {
- if (dutyPickPeople.includes(name)) dutyPickPeople = dutyPickPeople.filter((x) => x !== name);
- else dutyPickPeople.push(name);
- renderDutyStaffPick();
- }
- function renderDutyExist() {
- const rows = dutyShifts.filter((s) => s.date === dutyPickDate);
- const box = document.getElementById("dutyExist");
- box.innerHTML = rows.length
- ? `<div class="muted" style="margin-bottom:8px">当日已排</div><div class="duty-exist">${rows.map((s) =>
- `<div class="row"><span class="duty-chip ${s.type}" style="width:auto;margin:0">${dutyChipText(s)}</span>
- <button class="btn btn-sm" type="button" onclick="removeDutyShift(${s.id})">删除</button></div>`
- ).join("")}</div>`
- : `<div class="muted">当日尚未排班</div>`;
- }
- function openDutyDay(date) {
- dutyPickDate = date;
- dutyPickPeople = [];
- const f = document.getElementById("dutyForm");
- f.type.value = "am";
- f.post.value = "配酸";
- document.getElementById("dutyDlgTitle").textContent = date + " 值班配置";
- renderDutyStaffPick();
- renderDutyExist();
- document.getElementById("dutyDialog").showModal();
- }
- function saveDutyShift() {
- if (!dutyPickDate) return;
- if (!dutyPickPeople.length) { toast("请选择人员"); return; }
- const f = document.getElementById("dutyForm");
- const type = f.type.value;
- const post = (f.post.value || "").trim();
- const hit = dutyShifts.find((s) => s.date === dutyPickDate && s.type === type);
- if (hit) {
- hit.people = [...dutyPickPeople];
- hit.post = post;
- } else {
- dutyShifts.push({ id: ++dutySeq, date: dutyPickDate, type, people: [...dutyPickPeople], post });
- }
- dutyPickPeople = [];
- renderDutyStaffPick();
- renderDutyExist();
- renderDuty();
- toast("已保存排班");
- }
- function removeDutyShift(id) {
- dutyShifts = dutyShifts.filter((s) => s.id !== id);
- renderDutyExist();
- renderDuty();
- toast("已删除");
- }
- const STAFF_DEPTS = ["行政人事", "财务部", "销售部", "运输部", "仓储部", "生产部", "配酸车间"];
- let staffTab = "全部";
- let staffPage = 1;
- let staffPageSize = 20;
- let staffSeq = 2025030;
- let selectedStaffIds = new Set();
- let staffDetailId = null;
- let staffs = [
- { 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: "负责档案、考勤与值班排班。" },
- { 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: "客户合同与报价对接。" },
- { 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 主驾,危货从业资格在期。" },
- { 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 主驾。" },
- { 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: "配酸、分装现场负责人。" },
- { 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: "" },
- { 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: "硫酸库、液碱库。" },
- { 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。" },
- { 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: "危货押运证将到期,需复审。" },
- { 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: "值班排班骨干。" },
- { 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: "" },
- { 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: "" },
- { 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: "" },
- { 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: "周末值班。" },
- { 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: "协助档案与考勤。" },
- { 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: "" },
- { 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 押运。" },
- { 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: "" },
- { 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: "报销初审、备用金核额。" },
- { 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: "" },
- { 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: "运输计划与任务单调度。" },
- { 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 离职,档案留存。" }
- ];
- function staffById(id) { return staffs.find((x) => x.id === id); }
- function staffContractSoon(x) {
- if (!x.contractEnd || x.status === "离职") return false;
- const d = daysBetween(today, x.contractEnd);
- return d >= 0 && d <= 30;
- }
- function staffStatusPill(st) {
- if (st === "在职") return `<span class="st st-run">在职</span>`;
- if (st === "试用") return `<span class="st st-fix">试用</span>`;
- return `<span class="st st-stop">离职</span>`;
- }
- function filteredStaff() {
- const q = (document.getElementById("staffQ")?.value || "").trim();
- const dept = document.getElementById("staffDept")?.value || "";
- let rows = staffs.slice();
- if (staffTab !== "全部") rows = rows.filter((x) => x.status === staffTab);
- if (dept) rows = rows.filter((x) => x.dept === dept);
- if (q) rows = rows.filter((x) => [x.no, x.name, x.post, x.phone, x.dept].join(" ").includes(q));
- return rows;
- }
- function setStaffTab(tab) {
- staffTab = tab;
- staffPage = 1;
- document.querySelectorAll("#staffTabs button").forEach((b) => b.classList.toggle("on", b.dataset.tab === tab));
- renderStaff();
- }
- function toggleStaff(id, on) {
- if (on) selectedStaffIds.add(id); else selectedStaffIds.delete(id);
- renderStaff();
- }
- function toggleStaffAll(on) {
- const rows = filteredStaff();
- const start = (staffPage - 1) * staffPageSize;
- rows.slice(start, start + staffPageSize).forEach((x) => on ? selectedStaffIds.add(x.id) : selectedStaffIds.delete(x.id));
- renderStaff();
- }
- function fillStaffDeptSelect(val) {
- const sel = document.getElementById("staffDeptSelect");
- if (sel) {
- sel.innerHTML = STAFF_DEPTS.map((d) => `<option>${d}</option>`).join("");
- if (val) sel.value = val;
- }
- const filter = document.getElementById("staffDept");
- if (filter && !filter.dataset.ready) {
- filter.innerHTML = `<option value="">全部部门</option>` + STAFF_DEPTS.map((d) => `<option>${d}</option>`).join("");
- filter.dataset.ready = "1";
- }
- }
- function renderStaff() {
- fillStaffDeptSelect();
- const onJob = staffs.filter((x) => x.status === "在职").length;
- const trial = staffs.filter((x) => x.status === "试用").length;
- const left = staffs.filter((x) => x.status === "离职").length;
- const due = staffs.filter(staffContractSoon).length;
- document.getElementById("staffStats").innerHTML = `
- <div class="stat"><div class="k">在职</div><div class="v">${onJob}</div><div class="s">劳动合同 / 劳务在岗</div></div>
- <div class="stat"><div class="k">试用</div><div class="v">${trial}</div><div class="s">试用期内人员</div></div>
- <div class="stat"><div class="k">离职</div><div class="v">${left}</div><div class="s">档案留存</div></div>
- <div class="stat"><div class="k">合同将到期</div><div class="v">${due}</div><div class="s">30 日内到期</div></div>`;
- const rows = filteredStaff();
- const pages = Math.max(1, Math.ceil(rows.length / staffPageSize));
- if (staffPage > pages) staffPage = pages;
- const start = (staffPage - 1) * staffPageSize;
- const pageRows = rows.slice(start, start + staffPageSize);
- const allOn = pageRows.length && pageRows.every((x) => selectedStaffIds.has(x.id));
- document.getElementById("staffSelHint").textContent = `已选 ${selectedStaffIds.size} 项`;
- document.getElementById("staffTable").innerHTML = pageRows.length ? `<table class="erp-table staff-table">
- <thead><tr>
- <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="toggleStaffAll(this.checked)" /></th>
- <th>工号</th><th>姓名</th><th>性别</th><th>部门</th><th>岗位</th><th>手机</th><th>入职日期</th><th>合同到期</th><th>用工类型</th><th>状态</th>
- </tr></thead>
- <tbody>${pageRows.map((x) => `<tr>
- <td class="chk"><input type="checkbox" ${selectedStaffIds.has(x.id) ? "checked" : ""} onchange="toggleStaff(${x.id}, this.checked)" /></td>
- <td class="cell-no">${x.no}</td>
- <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>
- <td>${x.gender}</td><td>${x.dept}</td><td>${x.post}</td><td>${x.phone}</td>
- <td>${x.hire}</td>
- <td class="${staffContractSoon(x) ? "staff-due" : ""}">${x.contractEnd || "—"}${staffContractSoon(x) ? " 将到期" : ""}</td>
- <td>${x.kind}</td>
- <td>${staffStatusPill(x.status)}</td>
- </tr>`).join("")}</tbody></table>` : `<div class="empty">暂无符合条件的员工</div>`;
- const show = Math.min(pages, 6);
- const nums = [];
- for (let n = 1; n <= show; n++) nums.push(n);
- document.getElementById("staffPager").innerHTML = `
- <span class="muted">共 ${rows.length} 条</span>
- <select class="page-size" onchange="staffPageSize=+this.value;staffPage=1;renderStaff()">
- <option value="20" ${staffPageSize === 20 ? "selected" : ""}>20条/页</option>
- <option value="50" ${staffPageSize === 50 ? "selected" : ""}>50条/页</option>
- </select>
- <button type="button" onclick="staffPage=Math.max(1, staffPage-1);renderStaff()">‹</button>
- ${nums.map((n) => `<button type="button" class="${n === staffPage ? "on" : ""}" onclick="staffPage=${n};renderStaff()">${n}</button>`).join("")}
- <button type="button" onclick="staffPage=Math.min(${pages}, staffPage+1);renderStaff()">›</button>
- <span class="muted">前往</span>
- <input class="quote-goto" type="number" min="1" max="${pages}" value="${staffPage}" onchange="staffPage=Math.min(${pages}, Math.max(1, +this.value||1));renderStaff()" />
- <span class="muted">页</span>`;
- }
- function openStaffDlg(id) {
- const f = document.getElementById("staffForm");
- f.reset();
- const x = id ? staffById(id) : null;
- fillStaffDeptSelect(x ? x.dept : "行政人事");
- document.getElementById("staffDlgTitle").textContent = x ? "编辑员工" : "新增员工";
- f.id.value = x ? x.id : "";
- if (x) {
- f.name.value = x.name; f.no.value = x.no; f.gender.value = x.gender;
- f.post.value = x.post; f.phone.value = x.phone; f.hire.value = x.hire;
- f.contractEnd.value = x.contractEnd; f.kind.value = x.kind; f.status.value = x.status;
- f.edu.value = x.edu; f.native.value = x.native; f.emergency.value = x.emergency;
- f.emergPhone.value = x.emergPhone; f.remark.value = x.remark || "";
- } else {
- f.hire.value = today; f.status.value = "试用"; f.kind.value = "劳动合同";
- }
- document.getElementById("staffDialog").showModal();
- }
- function saveStaff() {
- const f = document.getElementById("staffForm");
- if (!f.name.value.trim() || !f.post.value.trim()) { toast("请填写姓名和岗位"); return; }
- const id = Number(f.id.value);
- const row = {
- name: f.name.value.trim(), no: f.no.value.trim(), gender: f.gender.value, dept: f.dept.value,
- post: f.post.value.trim(), phone: f.phone.value.trim(), hire: f.hire.value, contractEnd: f.contractEnd.value,
- kind: f.kind.value, status: f.status.value, edu: f.edu.value, native: f.native.value.trim(),
- emergency: f.emergency.value.trim(), emergPhone: f.emergPhone.value.trim(), remark: f.remark.value.trim()
- };
- if (id) {
- const x = staffById(id);
- if (x) Object.assign(x, row);
- } else {
- if (!row.no) row.no = "SX-" + (++staffSeq);
- staffs.unshift({ id: Date.now(), ...row });
- }
- document.getElementById("staffDialog").close();
- renderStaff();
- toast("已保存员工档案");
- }
- function openStaffDetail(id) {
- const x = staffById(id);
- if (!x) return;
- staffDetailId = id;
- document.getElementById("staffDetailTitle").textContent = x.name + " · " + x.no;
- document.getElementById("staffDetailBody").innerHTML = `
- <div class="split-h">
- <div>
- <h4>基本信息</h4>
- <div class="kv">
- ${kv("工号", x.no)}${kv("姓名", x.name)}${kv("性别", x.gender)}
- ${kv("部门", x.dept)}${kv("岗位", x.post)}${kv("手机", x.phone)}
- ${kv("学历", x.edu)}${kv("籍贯", x.native || "—")}
- </div>
- </div>
- <div>
- <h4>合同与联系</h4>
- <div class="kv">
- ${kv("状态", x.status)}${kv("用工类型", x.kind)}
- ${kv("入职日期", x.hire || "—")}${kv("合同到期", x.contractEnd || "—")}
- ${kv("紧急联系人", x.emergency || "—")}${kv("联系人电话", x.emergPhone || "—")}
- </div>
- <p style="margin-top:12px" class="muted">${x.remark || "无备注"}</p>
- </div>
- </div>`;
- document.getElementById("staffDetailDialog").showModal();
- }
- function editStaffFromDetail() {
- document.getElementById("staffDetailDialog").close();
- if (staffDetailId) openStaffDlg(staffDetailId);
- }
- function editSelectedStaff() {
- const ids = [...selectedStaffIds];
- if (ids.length !== 1) { toast("请勾选一名员工后编辑"); return; }
- openStaffDlg(ids[0]);
- }
- function leaveSelectedStaff() {
- const ids = [...selectedStaffIds];
- if (!ids.length) { toast("请先勾选员工"); return; }
- let n = 0;
- ids.forEach((id) => {
- const x = staffById(id);
- if (x && x.status !== "离职") { x.status = "离职"; x.remark = (x.remark ? x.remark + ";" : "") + today + " 办理离职"; n++; }
- });
- selectedStaffIds.clear();
- renderStaff();
- toast(n ? `已办理 ${n} 人离职` : "所选人员已是离职状态");
- }
- function showCertView(name, id) {
- if (name === "detail") {
- if (id != null) currentCertId = id;
- if (currentCertId == null) {
- toast("请先从列表选择一条证件");
- name = "list";
- }
- }
- const waitEl = document.getElementById("view-cert-wait");
- if (waitEl) waitEl.classList.toggle("hidden", name !== "wait");
- document.getElementById("view-cert-list").classList.toggle("hidden", name !== "list");
- document.getElementById("view-cert-detail").classList.toggle("hidden", name !== "detail");
- document.querySelectorAll("#certSubnav button").forEach((b) => b.classList.toggle("on", b.dataset.view === name));
- document.getElementById("navDetail").disabled = currentCertId == null;
- document.getElementById("crumb").textContent = name === "detail" ? "证件信息" : name === "wait" ? "待关联计划" : "证件列表";
- if (name === "wait") renderWaitPlans();
- if (name === "list") renderCertList();
- if (name === "detail") renderCertDetail();
- }
- function kv(lab, val) {
- return `<div class="lab">${lab}</div><div class="val">${val || "—"}</div>`;
- }
- function certThumbHtml(src, alt) {
- if (!src) return "";
- return `<button type="button" class="cert-thumb" onclick="openCertImg(this)"><img src="${src}" alt="${alt || "证件"}"></button>`;
- }
- function openCertImg(el) {
- const img = el && (el.tagName === "IMG" ? el : el.querySelector("img"));
- if (!img || !img.src) return;
- const view = document.getElementById("certImgView");
- const dlg = document.getElementById("certImgDialog");
- view.src = img.src;
- view.alt = img.alt || "证件原图";
- if (dlg.open) dlg.close();
- dlg.showModal();
- }
- function closeCertImg(ev) {
- const dlg = document.getElementById("certImgDialog");
- if (!dlg.open) return;
- if (!ev || ev.target === dlg || ev.target === document.getElementById("certImgView")) dlg.close();
- }
- function renderWaitPlans() {
- const waitList = waitListPlans();
- const waitLinkAll = plans.filter(isWaitLinkPlan);
- const todayN = waitLinkAll.filter(isWaitTodayPlan).length;
- const weekN = waitLinkAll.filter(isWaitWeekPlan).length;
- const buyWarnN = certs.filter((c) => buyWarn(c)).length;
- const waitStats = document.getElementById("waitStats");
- if (waitStats) {
- waitStats.innerHTML = [
- ["今日待关联计划", todayN, "送货或计划日期为今天", ""],
- ["本周待关联计划", weekN, "本周一至周日", ""],
- ["购买证预警", buyWarnN, "余量 ≤ 自设预警量", "filterCertSt('余量预警')"]
- ].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("");
- }
- const waitBox = document.getElementById("certWaitPlans");
- if (!waitBox) return;
- const allWait = plans.filter(isWaitListPlan);
- const nIn = allWait.filter((p) => planCertScene(p) === "in").length;
- const nOut = allWait.filter((p) => planCertScene(p) === "out").length;
- const nDirect = allWait.filter((p) => planCertScene(p) === "direct").length;
- const tabs = [
- ["", `全部 ${allWait.length}`],
- ["in", `入库单 ${nIn}`],
- ["out", `出库单 ${nOut}`],
- ["direct", `直销单 ${nDirect}`]
- ].map(([k, lab]) => `<button type="button" class="${waitLinkSceneTab === k ? "on" : ""}" onclick="setWaitLinkScene('${k}')">${lab}</button>`).join("");
- waitBox.innerHTML = `<div class="wait-scene-tabs">${tabs}</div>` + (waitList.length ? `<div class="wait-table-wrap"><table class="erp-table plan-table wait-table">
- <thead><tr>
- <th class="col-op-buy">购买证</th>
- <th class="col-op-trans">运输证</th>
- <th class="idx">序号</th>
- <th class="col-no">计划编号</th>
- <th>跟进状态</th>
- <th>计划日期</th>
- <th>送货时间</th>
- <th>单据类型</th>
- <th>车牌号</th>
- <th>驾押人员</th>
- <th>装货客户</th>
- <th class="col-ship">送货客户</th>
- <th>产品名称</th>
- <th>产品规格</th>
- <th>包装规格</th>
- <th>运输证总量</th>
- <th>本次计划数量</th>
- <th>已发数量</th>
- <th>余量</th>
- <th>合同</th>
- </tr></thead>
- <tbody>${waitList.map((p, i) => waitLinkRowHtml(p, i)).join("")}</tbody>
- </table></div>` : `<div class="empty">${waitLinkSceneTab ? "该场景暂无已安排、已选车号的易制毒计划。" : "暂无已安排、已选车号的易制毒运输计划。"}</div>`);
- }
- function renderCertList() {
- const q = (document.getElementById("certQ").value || "").trim();
- const st = document.getElementById("certSt").value;
- const warnN = certs.filter((c) => certStatusOf(c) === "余量预警" || certStatusOf(c) === "即将到期").length;
- const transN = certs.filter((c) => transWarn(c)).length;
- const buyWarnN = certs.filter((c) => buyWarn(c)).length;
- const overN = certs.filter((c) => certOver(c)).length;
- const vehN = certs.reduce((s, c) => s + vehicleWarnCount(c), 0);
- const todayN = certs.filter((c) => c.created === today).length;
- const todayTransN = certs.reduce((s, c) => s + transOf(c).filter((t) => t.created === today).length, 0);
- const waitLink = plans.filter(isWaitLinkPlan).length;
- const vehRowsAll = certs.flatMap((c) => vehicleRowsOf(c).map((r) => ({ ...r, cert: c })));
- const vehWarnRows = vehRowsAll.filter((r) => r.level);
- const stepEl = document.getElementById("certSteps");
- if (stepEl) {
- const step2on = waitLink > 0;
- const step3on = vehWarnRows.length > 0 || buyWarnN || transN || overN;
- stepEl.innerHTML = [
- ["1", "上传证件", "上传购买证、运输证,核对证载数量与车号", `${certs.length} 本购买证 / ${todayTransN || certs.reduce((s, c) => s + transCount(c), 0)} 张运输证`, "openCertDlg(false)", ""],
- ["2", "人工关联计划", "核对证件、车辆后点击确认关联", waitLink ? `${waitLink} 条待确认` : "暂无待关联计划", "focusCertWait()", step2on ? "on" : ""],
- ["3", "余量预警", "完成后扣减已完成量;购买证或运输证余量 ≤ 自设预警量时预警", vehWarnRows.length ? `${vehWarnRows.length} 车次预警` : "关联、完成后自动计算", "focusCertVehWarn()", step3on ? "warn" : ""]
- ].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("");
- }
- document.getElementById("listAlerts").innerHTML = `
- ${overN ? `<div class="alert red">关联计划量已超过购买证或运输证证载 ${overN} 条</div>` : ""}
- ${vehN ? `<div class="alert red">关联车辆计划量预警 ${vehN} 车次(超证载 / 购买证或运输证余量 ≤ 自设预警量)</div>` : ""}
- ${transN ? `<div class="alert red">运输证余量预警 ${transN} 条</div>` : ""}
- ${buyWarnN ? `<div class="alert red">购买证余量预警 ${buyWarnN} 条</div>` : ""}
- ${warnN && !buyWarnN ? `<div class="alert red">购买证即将到期 ${warnN} 条</div>` : ""}
- <div class="alert ok">在册购买证 ${certs.length} 条 · 待关联计划 ${waitLink} 条</div>`;
- document.getElementById("listStats").innerHTML = [
- ["今日上传证件", todayN + " / " + todayTransN, "购买证 / 运输证", "openCertDlg(false)"],
- ["购买证预警", buyWarnN, "余量 ≤ 自设预警量", "filterCertSt('余量预警')"],
- ["运输证预警", transN, "余量 ≤ 自设预警量", "filterCertSt('运输证预警')"],
- ["车辆计划预警", vehN, "各车运输计划量对照两证", "focusCertVehWarn()"]
- ].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("");
- renderWaitPlans();
- const rows = certs.filter((c) => {
- const blob = [c.buyNo, transNos(c).join(" "), c.product, c.buyer, c.seller,
- transOf(c).map((t) => t.plates).join(" "), transOf(c).map((t) => t.consignee).join(" "),
- transOf(c).map((t) => t.carrier).join(" ")].join(" ");
- if (q && !blob.includes(q)) return false;
- if (st && certStatusOf(c) !== st) return false;
- return true;
- });
- document.getElementById("listCount").textContent = `共 ${rows.length} 条`;
- document.getElementById("certListTable").innerHTML = rows.length ? `<table>
- <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>
- <tbody>${rows.map((c) => {
- const nos = transNos(c);
- const carriers = [...new Set(transOf(c).map((t) => t.carrier).filter(Boolean))];
- const cars = allDispatches(c);
- const warnItems = transOf(c).filter(transItemWarn);
- return `<tr class="row-link" onclick="showCertView('detail', ${c.id})">
- <td>${certStHtml(certStatusOf(c))}</td>
- <td class="cell-no">${c.buyNo}</td>
- <td>${nos.length ? nos.map((n) => `<div>${n}</div>`).join("") : "—"}<div class="muted">${transCount(c)} 张</div></td>
- <td>${c.product}<div class="muted">${c.ctrl || ""}</div></td>
- <td>${c.buyer || "—"}</td>
- <td>${carriers.join("<br>") || "—"}</td>
- <td>${c.buyQty} ${c.unit}</td>
- <td class="${remainGap(c) < -1e-6 ? "staff-due" : ""}">${fmtTon(holdingOf(c))} ${c.unit}<div class="muted">${openLinkedPlans(c).length} 条在途</div></td>
- <td>${fmtTon(deductedOf(c))} ${c.unit}</td>
- <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) => {
- const t = findTrans(c, d.transId);
- const vh = vehiclePlanHold(d.plate, d.transId);
- const done = vehicleDeduct(d.plate, d.transId);
- const over = vehicleHold(d.plate, d.transId) - Number(d.qty || 0) > 1e-6;
- const total = t ? Number(t.transQty || 0) : 0;
- const left = t ? transItemGap(t) : 0;
- const leftWarn = t && left <= transWarnQty(t) + 1e-9;
- 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>`;
- }).join("")}</tbody></table>` : "—"}</td>
- <td>${remainHtml(c)}</td>
- <td>${transRemainHtml(c)}${warnItems.length ? `<div class="muted">${warnItems.length} 张预警</div>` : ""}</td>
- </tr>`;
- }).join("")}</tbody></table>` : `<div class="empty">没有记录。请先上传购买证及运输证。</div>`;
- updateBadges();
- }
- function filterCertSt(st) {
- const sel = document.getElementById("certSt");
- if (sel) sel.value = st || "";
- showCertView("list");
- }
- function focusCertWait() {
- showCertView("wait");
- }
- function focusCertVehWarn() {
- showCertView("list");
- }
- function waitCertOpts(p, selectedId, kind) {
- const pred = kind === "procure" ? certIsProcure : certIsShipBuy;
- const opts = [`<option value="">请选择</option>`];
- certs.forEach((c) => {
- const hit = pred(p, c);
- if (!hit && c.id !== selectedId) return;
- if (certVoided(c) && c.id !== selectedId) return;
- opts.push(`<option value="${c.id}" ${c.id === selectedId ? "selected" : ""} ${certVoided(c) ? "disabled" : ""}>${c.buyNo} · ${certVoided(c) ? "已核销" : "余 " + fmtTon(remain(c)) + c.unit}${hit ? " ✓" : ""}</option>`);
- });
- return opts.join("");
- }
- function waitBuyLinked(p) {
- if (planNeedProcureCert(p)) return !!(p.procureCertId && p.shipCertId);
- return !!(p.certId || p.shipCertId);
- }
- function waitOpBtns(p) {
- const buyOn = waitBuyLinked(p);
- const transOn = !!p.transId;
- const buyBtn = buyOn
- ? `<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>`
- : `<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>`;
- const transBtn = transOn
- ? `<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>`
- : `<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>`;
- return `<td class="col-op-buy">${buyBtn}</td><td class="col-op-trans">${transBtn}</td>`;
- }
- function cancelWaitBuyLink(planId) {
- const p = plans.find((x) => x.id === planId);
- if (!p || !waitBuyLinked(p)) return;
- const extra = p.transId ? "将同时取消已关联的运输证。" : "";
- if (!confirm(`已关联购买证不可直接修改。${extra}确定取消关联购买证?`)) return;
- p.certId = null;
- p.shipCertId = null;
- p.procureCertId = null;
- p.transId = null;
- delete waitLinkPicks[planId];
- toast(`${p.no} 已取消购买证关联`);
- renderCertList();
- updateBadges();
- }
- function cancelWaitTransLink(planId) {
- const p = plans.find((x) => x.id === planId);
- if (!p || !p.transId) return;
- if (!confirm("已关联运输证不可直接修改。确定取消关联运输证?")) return;
- p.transId = null;
- if (waitLinkPicks[planId]) waitLinkPicks[planId].transId = "";
- toast(`${p.no} 已取消运输证关联`);
- renderCertList();
- updateBadges();
- }
- function waitPlanTransOf(p) {
- if (!p || !p.transId) return { c: null, t: null };
- const ids = [p.certId, p.shipCertId, p.procureCertId].filter(Boolean);
- for (const id of ids) {
- const c = certs.find((x) => x.id === id);
- const t = c ? findTrans(c, p.transId) : null;
- if (t) return { c, t };
- }
- for (const c of certs) {
- const t = findTrans(c, p.transId);
- if (t) return { c, t };
- }
- return { c: null, t: null };
- }
- function waitLinkRowHtml(p, i) {
- const { c, t } = waitPlanTransOf(p);
- const unit = (c && c.unit) || p.unit || "吨";
- const transTotal = t ? Number(t.transQty || 0) : null;
- const sent = t ? transItemDeduct(t) : null;
- const left = t ? transItemGap(t) : null;
- const warnAt = t ? transWarnQty(t) : 0;
- const leftWarn = left != null && left <= warnAt + 1e-9;
- return `<tr>
- ${waitOpBtns(p)}
- <td class="idx">${(i || 0) + 1}</td>
- <td class="cell-no col-no">${p.no}</td>
- <td>${planStHtml(p)}</td>
- <td>${p.date}</td>
- <td>${planDeliverAt(p)}</td>
- <td>${planTypeName(p)}</td>
- <td>${p.tractor || "—"}</td>
- <td>${planCrew(p)}</td>
- <td class="clip">${p.loadFrom || "—"}</td>
- <td class="col-ship">${p.customer || p.settler || "—"}</td>
- <td>${p.product || "—"}${p.ctrl === "易制毒" ? `<span class="prod-tag red">易制毒</span>` : ""}</td>
- <td>${p.spec || "—"}</td>
- <td>${p.pack || "—"}</td>
- <td class="num">${transTotal == null ? "—" : `${fmtTon(transTotal)} ${unit}`}</td>
- <td class="num qty">${p.planQty}${p.unit ? " " + p.unit : ""}</td>
- <td class="num">${sent == null ? "—" : `${fmtTon(sent)} ${unit}`}</td>
- <td class="num ${leftWarn ? "staff-due" : ""}">${left == null ? "—" : `${fmtTon(left)} ${unit}`}</td>
- <td class="clip">${planContractNo(p)}</td>
- </tr>`;
- }
- function waitPlanBarHtml(p) {
- return `<div class="wait-plan-bar">
- <span>计划 <b>${p.no}</b></span>
- <span>${planTypeName(p)}</span>
- <span>${p.product || "—"}${p.ctrl === "易制毒" ? `<span class="prod-tag red">易制毒</span>` : ""}</span>
- <span>计划量 <b>${p.planQty}${p.unit ? " " + p.unit : ""}</b></span>
- <span>车牌 <b>${p.tractor || "—"}</b></span>
- <span>装货 ${p.loadFrom || "—"}</span>
- <span>送货 ${p.customer || p.settler || "—"}</span>
- </div>`;
- }
- function waitBuyCardHtml(c, extra, pickHtml) {
- if (!c) return `<div class="empty">暂无购买证信息</div>`;
- return `<div class="cert-pane buy">
- <div class="cert-pane-h">
- ${pickHtml || ""}
- <span class="cert-tag buy">购买证</span>
- <h2>购买备案证明</h2>
- ${extra || ""}
- </div>
- <article class="card">
- <div class="card-h"><h2>${c.buyNo || "未编号"}</h2></div>
- <div class="card-b"><div class="kv">
- ${kv("编号", c.buyNo)}${kv("核发日期", c.buyDate)}
- ${kv("购买单位", c.buyer)}${kv("法定代表人", c.buyerLegal)}
- ${kv("电话", c.buyerTel)}${kv("住所", c.buyerAddr)}
- ${kv("销售单位", c.seller)}${kv("销售方法人", c.sellerLegal)}
- ${kv("购买物品", c.product)}${kv("用途", c.usage)}
- ${kv("数量", (c.buyQty || "—") + " " + (c.unit || ""))}${kv("有效次数", c.buyTimes)}
- ${kv("有效期限", (c.buyFrom || "") + " 至 " + (c.buyDue || ""))}
- ${kv("公安机关", c.buyPolice)}${kv("经办人", c.buyHandler)}
- ${kv("余量", fmtTon(remain(c)) + " " + (c.unit || ""))}
- </div>${c.buyUrl ? `<img src="${c.buyUrl}" alt="购买证" style="margin-top:12px;max-height:160px;object-fit:contain;width:100%">` : ""}</div>
- </article>
- </div>`;
- }
- function waitTransCardHtml(c, t, plate, actionHtml) {
- const hit = plate && (platesOfTrans(t).includes(plate) || String(t.plates || "").includes(plate));
- const left = transItemRemain(t);
- const warn = transItemWarn(t);
- const used = transItemHold(t);
- const tPct = Math.min(100, (used / (Number(t.transQty || 1) || 1)) * 100);
- return `<article class="card wait-trans-sheet${hit ? " hit" : ""}">
- <div class="card-h">
- <h2>${t.transNo || "未编号"}</h2>
- ${hit ? `<span class="match-rec veh">匹配 ${plate}</span>` : ""}
- ${actionHtml || ""}
- </div>
- <div class="card-b"><div class="kv">
- ${kv("编号", t.transNo)}${kv("核发日期", t.transDate)}
- ${kv("货主", t.owner)}${kv("法定代表人", t.ownerLegal)}
- ${kv("运输物品", c.product)}${kv("数量", (t.transQty || "—") + " " + (c.unit || ""))}
- ${kv("包装", t.pack)}${kv("购买许可证号", t.permitNo)}
- ${kv("运输单位", t.carrier)}${kv("运输方式", t.mode)}
- ${kv("车号(次)", t.plates || platesOfTrans(t).join("、") || "—")}${kv("收货单位", t.consignee)}
- ${kv("运输路线", t.route)}${kv("申请单位", t.applicantUnit)}
- ${kv("有效期限", (t.transFrom || "") + " 至 " + (t.transDue || ""))}
- ${kv("公安机关", t.transPolice)}${kv("经办人", t.transHandler)}
- </div>
- <div style="margin-top:14px;color:var(--muted);font-size:12px">本证进度(已完成 ${transItemDeduct(t)} + 在途计划 ${transItemPlan(t)} / 运输证 ${t.transQty || 0})${warn ? ` · 余量 ${left} 吨预警` : ""}</div>
- <div class="qty-bar"><div class="qty-fill${warn ? " warn" : ""}" style="width:${tPct}%"></div></div>
- ${t.transUrl ? `<img src="${t.transUrl}" alt="运输证" style="margin-top:12px;max-height:160px;object-fit:contain;width:100%">` : ""}
- </div>
- </article>`;
- }
- function waitDemoCertsForPlan(p) {
- return certs.filter((c) => certSelectable(c) && (!p.ctrl || !c.ctrl || p.ctrl === c.ctrl));
- }
- function waitBuyCertTableHtml(p, kind, selectedId) {
- const pred = kind === "procure" ? certIsProcure : certIsShipBuy;
- const title = kind === "procure" ? "装货方购买证" : "送货方购买证";
- const name = kind === "procure" ? "waitProcureCert" : "waitShipCert";
- let rows = certs.filter((c) => (pred(p, c) || c.id === selectedId) && (!certVoided(c) || c.id === selectedId));
- if (!rows.length) rows = waitDemoCertsForPlan(p);
- if (!rows.length) return `<div class="empty">没有可关联的${title}(已核销证件不可选)。</div>`;
- const live = rows.filter((c) => !certVoided(c));
- const picked = (selectedId && !certVoided(certs.find((x) => x.id === selectedId)) && selectedId)
- || (kind === "procure" ? (suggestProcureCert(p) || {}).id : (suggestShipCert(p) || {}).id)
- || (live[0] && live[0].id);
- if (!live.length) return `<div class="empty">匹配的${title}均已核销,无法再关联。</div>`;
- return live.map((c) => {
- const hit = pred(p, c);
- const pick = `<label class="wait-cert-pick"><input type="radio" name="${name}" value="${c.id}" ${c.id === picked ? "checked" : ""} onchange="refreshWaitBuyTransPane()" /></label>`;
- const extra = `<span class="muted">${title}</span>${hit ? `<span class="match-rec">已匹配</span>` : ""}`;
- return waitBuyCardHtml(c, extra, pick);
- }).join("");
- }
- function refreshWaitBuyTransPane() {
- const p = plans.find((x) => x.id === waitBuyPlanId);
- const box = document.getElementById("waitBuyTransPane");
- if (!p || !box) return;
- const ids = [...document.querySelectorAll("#waitBuyDialog [name=waitProcureCert]:checked, #waitBuyDialog [name=waitShipCert]:checked")]
- .map((el) => +el.value).filter(Boolean);
- const uniq = [...new Set(ids)];
- if (!uniq.length) {
- box.innerHTML = `<div class="empty">请先在左侧选择购买证,右侧带出该证下的运输证信息。</div>`;
- return;
- }
- const plate = p.tractor || "";
- const cards = [];
- const seen = new Set();
- uniq.forEach((id) => {
- const c = certs.find((x) => x.id === id);
- if (!certSelectable(c)) return;
- const list = transOf(c).filter((t) => transSelectable(c, t));
- const matched = list.filter((t) => plate && (platesOfTrans(t).includes(plate) || String(t.plates || "").includes(plate)));
- const show = matched.length ? matched : list;
- if (!show.length && list.length) show.push(...list);
- show.forEach((t) => {
- if (seen.has(t.id)) return;
- seen.add(t.id);
- cards.push(waitTransCardHtml(c, t, plate));
- });
- });
- const oneSide = planNeedProcureCert(p);
- box.innerHTML = `<div class="cert-pane trans">
- <div class="cert-pane-h">
- <span class="cert-tag trans">运输证</span>
- <h2>运输备案证明</h2>
- <span class="muted">${oneSide ? "任选一方" : `${cards.length} 张`}${plate ? ` · ${plate}` : ""}</span>
- </div>
- ${cards.length ? cards.join("") : `<div class="empty">所选购买证下暂无对应车辆的运输证。</div>`}
- </div>`;
- }
- function openWaitBuyDlg(planId) {
- const p = plans.find((x) => x.id === planId);
- if (!p) return;
- if (waitBuyLinked(p)) { cancelWaitBuyLink(planId); return; }
- if (!certs.length) { toast("请先上传购买证及运输证"); openCertDlg(false); return; }
- waitBuyPlanId = planId;
- const scene = planCertScene(p);
- const needProcure = planNeedProcureCert(p);
- document.getElementById("waitBuyHint").textContent = needProcure
- ? `${p.no} · 直销单 · 装货方、送货方均需关联购买证;运输证只需选择一方购买证下对应车辆的运输证`
- : `${p.no} · ${planSceneLabel(scene)} · 请选择送货方购买证,右侧同步查看该证下运输证`;
- const shipSel = p.shipCertId || (suggestShipCert(p) || {}).id;
- const procSel = p.procureCertId || p.certId || (suggestProcureCert(p) || {}).id;
- const pickHtml = needProcure
- ? waitBuyCertTableHtml(p, "procure", procSel) + `<div style="height:14px"></div>` + waitBuyCertTableHtml(p, "ship", shipSel)
- : waitBuyCertTableHtml(p, "ship", shipSel);
- document.getElementById("waitBuyBody").innerHTML = waitPlanBarHtml(p) + `<div class="wait-dlg-split">
- <div class="wait-info-stack">${pickHtml}</div>
- <div class="wait-info-stack" id="waitBuyTransPane"></div>
- </div>`;
- refreshWaitBuyTransPane();
- const buyDlg = document.getElementById("waitBuyDialog");
- if (buyDlg.open) buyDlg.close();
- buyDlg.showModal();
- }
- function closeWaitBuyDlg() {
- document.getElementById("waitBuyDialog").close();
- waitBuyPlanId = null;
- }
- function saveWaitBuyCert() {
- const p = plans.find((x) => x.id === waitBuyPlanId);
- if (!p) return;
- const shipEl = document.querySelector("#waitBuyDialog [name=waitShipCert]:checked");
- const procEl = document.querySelector("#waitBuyDialog [name=waitProcureCert]:checked");
- const shipId = shipEl ? +shipEl.value : 0;
- const procId = procEl ? +procEl.value : 0;
- if (planNeedProcureCert(p)) {
- if (!procId || !shipId) { toast("直销单请同时选择装货方购买证和送货方购买证"); return; }
- p.procureCertId = procId;
- p.shipCertId = shipId;
- p.certId = procId;
- } else {
- if (!shipId) { toast("请选择送货方购买证"); return; }
- p.shipCertId = shipId;
- p.procureCertId = null;
- p.certId = shipId;
- }
- p.transId = null;
- closeWaitBuyDlg();
- toast(`${p.no} 已关联购买证`);
- renderCertList();
- updateBadges();
- }
- function waitTransRowsForPlan(p) {
- const ids = planNeedProcureCert(p)
- ? [p.procureCertId || p.certId, p.shipCertId]
- : [p.shipCertId || p.certId];
- const uniq = [...new Set(ids.filter(Boolean))];
- const plate = p.tractor || "";
- const rows = [];
- const seen = new Set();
- uniq.forEach((id) => {
- const c = certs.find((x) => x.id === id);
- if (!c) return;
- transOf(c).forEach((t) => {
- if (!transSelectable(c, t)) return;
- const hit = !plate || platesOfTrans(t).includes(plate) || String(t.plates || "").includes(plate);
- if (seen.has(t.id)) return;
- if (hit) {
- seen.add(t.id);
- rows.push({ c, t, hit: true });
- }
- });
- });
- if (!rows.length) {
- uniq.forEach((id) => {
- const c = certs.find((x) => x.id === id);
- if (!c) return;
- transOf(c).forEach((t) => {
- if (!transSelectable(c, t) || seen.has(t.id)) return;
- seen.add(t.id);
- rows.push({ c, t, hit: false });
- });
- });
- }
- return rows;
- }
- function openWaitTransDlg(planId) {
- const p = plans.find((x) => x.id === planId);
- if (!p) return;
- if (!waitBuyLinked(p)) {
- toast(planNeedProcureCert(p) ? "直销单请先关联装货方、送货方购买证" : "请先关联购买证");
- openWaitBuyDlg(planId);
- return;
- }
- if (p.transId) { cancelWaitTransLink(planId); return; }
- waitTransPlanId = planId;
- const plate = p.tractor || "";
- const rows = waitTransRowsForPlan(p);
- const buyIds = [...new Set((planNeedProcureCert(p)
- ? [p.procureCertId || p.certId, p.shipCertId]
- : [p.shipCertId || p.certId]).filter(Boolean))];
- const buyCards = buyIds.map((id) => {
- const c = certs.find((x) => x.id === id);
- if (!c) return "";
- const tag = planNeedProcureCert(p) && (id === p.shipCertId && id !== (p.procureCertId || p.certId))
- ? `<span class="match-rec">送货方</span>`
- : (planNeedProcureCert(p) ? `<span class="match-rec">装货方</span>` : `<span class="match-rec">送货方</span>`);
- return waitBuyCardHtml(c, tag);
- }).join("") || `<div class="empty">请先关联购买证</div>`;
- document.getElementById("waitTransHint").textContent = planNeedProcureCert(p)
- ? `${p.no} · 车牌 ${plate || "—"} · 装货方/送货方购买证已列左侧,运输证只需关联其中一方下对应该车的运输证`
- : `${p.no} · 车牌 ${plate || "—"} · 先核对已关联购买证,再选择该证下对应该车的运输证`;
- const transHtml = rows.length
- ? `<div class="cert-pane trans">
- <div class="cert-pane-h">
- <span class="cert-tag trans">运输证</span>
- <h2>运输备案证明</h2>
- <span class="muted">${rows.length} 张${planNeedProcureCert(p) ? " · 任选一方" : ""}</span>
- </div>
- ${rows.map(({ c, t }) => waitTransCardHtml(c, t, plate,
- `<button class="btn btn-sm btn-primary" type="button" onclick="saveWaitTransCert(${c.id}, ${t.id})">关联此证</button>`
- )).join("")}
- </div>`
- : `<div class="empty">已关联购买证下没有车牌 ${plate || ""} 对应的运输证,请先追加运输证或核对车号。</div>`;
- document.getElementById("waitTransBody").innerHTML = waitPlanBarHtml(p) + `<div class="wait-dlg-split">
- <div class="wait-info-stack">${buyCards}</div>
- <div class="wait-info-stack">${transHtml}</div>
- </div>`;
- const transDlg = document.getElementById("waitTransDialog");
- if (transDlg.open) transDlg.close();
- transDlg.showModal();
- }
- function closeWaitTransDlg() {
- document.getElementById("waitTransDialog").close();
- waitTransPlanId = null;
- }
- function saveWaitTransCert(certId, transId) {
- const p = plans.find((x) => x.id === waitTransPlanId);
- const c = certs.find((x) => x.id === certId);
- const t = c ? findTrans(c, transId) : null;
- if (!p || !c || !t) return;
- const err = attachPlanToCert(p, c, t, true, p.tractor);
- if (err) { toast(err); return; }
- if (planNeedProcureCert(p) && (!p.shipCertId || !(p.procureCertId || p.certId))) {
- toast("直销单还需同时关联装货方、送货方购买证");
- closeWaitTransDlg();
- openWaitBuyDlg(p.id);
- return;
- }
- closeWaitTransDlg();
- toast(`已关联运输证 ${t.transNo} · ${p.tractor || ""}`);
- renderCertList();
- updateBadges();
- }
- function onWaitShipCertChange(planId) {
- const p = plans.find((x) => x.id === planId);
- const sel = document.querySelector(`[data-wait-ship="${planId}"]`);
- if (!p || !sel) return;
- const shipId = +sel.value || "";
- const saved = waitLinkPicks[planId] || {};
- if (planNeedProcureCert(p)) {
- waitLinkPicks[planId] = { ...saved, shipCertId: shipId };
- } else {
- const c = certs.find((x) => x.id === shipId);
- const plate = saved.plate || (c && suggestVehicleFromCert(c, p)) || "";
- const t = transForPlate(c, plate) || suggestTransForPlan(c, p);
- waitLinkPicks[planId] = { certId: shipId, shipCertId: shipId, transId: t && t.id, plate };
- }
- renderCertList();
- }
- function onWaitProcureCertChange(planId) {
- const p = plans.find((x) => x.id === planId);
- const sel = document.querySelector(`[data-wait-procure="${planId}"]`);
- if (!p || !sel) return;
- const saved = waitLinkPicks[planId] || {};
- const certId = +sel.value || "";
- const c = certs.find((x) => x.id === certId);
- const plate = saved.plate || (c && suggestVehicleFromCert(c, p)) || "";
- const t = transForPlate(c, plate) || suggestTransForPlan(c, p);
- waitLinkPicks[planId] = { ...saved, certId, transId: t && t.id, plate };
- renderCertList();
- }
- function onWaitTransChange(planId) {
- const p = plans.find((x) => x.id === planId);
- const sel = document.querySelector(`[data-wait-trans="${planId}"]`);
- if (!p || !sel) return;
- const saved = waitLinkPicks[planId] || {};
- const pick = waitPickOf(p);
- const c = pick.c;
- const t = c ? findTrans(c, +sel.value) : null;
- let plate = saved.plate || pick.plate || "";
- if (t && plate && !platesOfTrans(t).includes(plate) && !String(t.plates || "").includes(plate)) {
- plate = suggestVehicleForPlan(t, p);
- }
- waitLinkPicks[planId] = { ...saved, certId: c && c.id, shipCertId: pick.shipC && pick.shipC.id, transId: +sel.value, plate };
- renderCertList();
- }
- function onWaitVehChange(planId) {
- const p = plans.find((x) => x.id === planId);
- const sel = document.querySelector(`[data-wait-veh="${planId}"]`);
- if (!sel) return;
- const saved = waitLinkPicks[planId] || {};
- const pick = waitPickOf(p);
- const c = pick.c;
- const plate = sel.value;
- const t = transForPlate(c, plate);
- waitLinkPicks[planId] = { ...saved, certId: c && c.id, shipCertId: pick.shipC && pick.shipC.id, transId: t ? t.id : "", plate };
- renderCertList();
- }
- function applyWaitLinkToPlan(p, c, t, plate, shipC) {
- if (planNeedProcureCert(p)) {
- p.procureCertId = c.id;
- p.shipCertId = shipC ? shipC.id : null;
- } else {
- p.shipCertId = (shipC && shipC.id) || c.id;
- p.procureCertId = null;
- }
- return attachPlanToCert(p, c, t, true, plate);
- }
- function confirmWaitLink(planId, silent) {
- const p = plans.find((x) => x.id === planId);
- if (!p) return false;
- if (!certs.length) {
- if (!silent) { toast("请先上传购买证及运输证"); openCertDlg(false); }
- return false;
- }
- const transSel = document.querySelector(`[data-wait-trans="${planId}"]`);
- const vehSel = document.querySelector(`[data-wait-veh="${planId}"]`);
- const shipSel = document.querySelector(`[data-wait-ship="${planId}"]`);
- const procSel = document.querySelector(`[data-wait-procure="${planId}"]`);
- const pick = waitPickOf(p);
- const scene = planCertScene(p);
- let c = pick.c;
- if (planNeedProcureCert(p) && procSel && procSel.value) c = certs.find((x) => x.id === +procSel.value) || c;
- else if (!planNeedProcureCert(p) && shipSel && shipSel.value) c = certs.find((x) => x.id === +shipSel.value) || c;
- const shipC = (shipSel && shipSel.value && certs.find((x) => x.id === +shipSel.value)) || pick.shipC;
- const t = (transSel && c && findTrans(c, +transSel.value)) || pick.t;
- const plate = (vehSel && vehSel.value) || pick.plate;
- if (!c || !t) {
- if (!silent) {
- toast(scene === "direct" ? "直销单请先匹配山峡采购证及运输证" : "请先匹配送货方购买证与运输证");
- linkWaitPlan(planId);
- }
- return false;
- }
- if (planNeedProcureCert(p) && !shipC) {
- if (!silent) toast("直销单还需关联送货方购买证");
- return false;
- }
- const err = applyWaitLinkToPlan(p, c, t, plate, shipC);
- if (err) {
- if (!silent) toast(err);
- return false;
- }
- delete waitLinkPicks[planId];
- if (!silent) {
- const after = afterLinkCalc(c, t, p.tractor, 0, p.id, shipC);
- const flag = after.over ? ",已超证预警" : (after.warn ? `,余量已达自设预警量` : "");
- const extra = planNeedProcureCert(p) && shipC ? `,送货方证 ${shipC.buyNo}` : "";
- toast(`已关联 ${p.no} · ${planSceneLabel(scene)} → ${t.transNo}${p.tractor ? " · " + p.tractor : ""}${extra},本车运输计划量 ${fmtTon(p.vehPlanQty != null ? p.vehPlanQty : p.planQty)}${c.unit}${flag}`);
- afterManualLink();
- }
- return true;
- }
- function afterManualLink() {
- showMod("cert");
- showCertView("list");
- updateBadges();
- setTimeout(() => {
- if (waitLinkPlans().length) focusCertWait();
- else focusCertVehWarn();
- }, 40);
- }
- function confirmAllWaitLinks() {
- const ids = waitLinkPlans().map((p) => p.id);
- let n = 0;
- ids.forEach((id) => {
- const p = plans.find((x) => x.id === id);
- const pick = waitPickOf(p || {});
- if (!p || !pick.c || !pick.t || !pick.matchShip) return;
- if (planCertScene(p) === "direct" && (!pick.matchProcure || !pick.shipC)) return;
- if (confirmWaitLink(id, true)) n += 1;
- });
- if (!n) {
- toast("没有已按客户、车辆自动匹配、可直接确认的计划");
- return;
- }
- toast(`已人工关联 ${n} 条计划,已按购买证 / 运输证计算各车运输计划量`);
- afterManualLink();
- }
- function linkWaitPlan(planId) {
- const p = plans.find((x) => x.id === planId);
- if (!p) return;
- if (!certs.length) {
- toast("请先上传购买证及运输证");
- openCertDlg(false);
- return;
- }
- const c = suggestCertForPlan(p) || certs[0];
- if (!c) {
- toast("没有可改选的购买证,请先上传证件");
- showCertView("list");
- return;
- }
- showCertView("detail", c.id);
- openCertLinkPlanDlg(c.id, [p.id]);
- }
- function renderCertDetail() {
- const c = certs.find((x) => x.id === currentCertId);
- if (!c) { showCertView("list"); return; }
- const list = transOf(c);
- document.getElementById("navDetail").disabled = false;
- document.getElementById("detailTitle").textContent = c.product + " · 购买证 / 运输证";
- document.getElementById("detailSub").textContent =
- `识别生成于 ${c.created} · 购买证 ${c.buyNo} · 运输证 ${list.length} 张`;
- const r = remain(c);
- const tr = transRemain(c);
- const hold = holdingOf(c);
- const done = deductedOf(c);
- const unassignedAll = list.reduce((s, t) => s + unassignedTrans(t), 0);
- const companies = [...new Set(list.map((t) => t.carrier).filter(Boolean))];
- document.getElementById("detailStats").innerHTML = [
- ["购买证数量", c.buyQty + " " + c.unit, "购买备案数量", "tone-buy"],
- ["购买证剩余", (remainGap(c) < 0 ? remainGap(c).toFixed(2) : r) + " " + c.unit, `已完成扣减 ${fmtTon(done)} · 在途计划 ${fmtTon(hold)}`, "tone-buy"],
- ["运输证张数", list.length + " 张", `运量合计 ${transTotal(c)} ${c.unit} · 未分派 ${unassignedAll} ${c.unit}`, "tone-trans"],
- ["运输证剩余", tr + " " + c.unit, transWarn(c) ? `有运输证余量 ≤ 自设预警量` : `余量 ≤ 各证自设预警量时预警`, "tone-trans"]
- ].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("");
- const warnItems = list.filter(transItemWarn);
- const linked = linkedPlansOf(c);
- const vehRows = vehicleRowsOf(c);
- const waitN = waitLinkPlans().filter((p) => planMatchesCert(p, c)).length;
- document.getElementById("certRelHint").innerHTML =
- `① 已上传运输证 <em>${list.length}</em> 张 → ② 人工关联计划 <em>${linked.length}</em> 条(在途计划 ${fmtTon(hold)}${c.unit},已完成 ${fmtTon(done)}${c.unit})→ ③ 各车运输计划量 = 受购买证、运输证数量约束;完成后扣减实发量;余量 ≤ 自设预警量时预警。${waitN ? ` 另有 ${waitN} 条品名匹配计划待勾选。` : ""}`;
- const formulaEl = document.getElementById("certQtyFormula");
- if (formulaEl) {
- const buyCls = remainGap(c) < -1e-6 ? "bad" : (buyWarn(c) ? "warn" : "");
- formulaEl.innerHTML = [
- `<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>`,
- ...list.map((t) => {
- const used = transItemPlan(t);
- const shippedT = transItemDeduct(t);
- const g = transItemGap(t);
- const cls = g < -1e-6 ? "bad" : (transItemWarn(t) ? "warn" : "");
- 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>`;
- })
- ].join("");
- }
- const warnCard = document.getElementById("certWarnQtyCard");
- if (warnCard) {
- warnCard.innerHTML = `<div class="card-h"><h2>余量预警设置</h2><span class="muted">余量 ≤ 该值时预警;默认按证件核准总量的 10%,可自行调整</span></div>
- <div class="card-b"><table class="warn-qty-table">
- <thead><tr><th>证件</th><th>证号</th><th>证载</th><th>当前余量</th><th>预警量</th></tr></thead>
- <tbody>
- <tr>
- <td><span class="cert-tag buy">购买证</span></td>
- <td class="cell-no">${c.buyNo || "—"}</td>
- <td>${fmtTon(c.buyQty)} ${c.unit}</td>
- <td>${fmtTon(remainGap(c))} ${c.unit}</td>
- <td>${warnQtyField(c.id, null, c.unit)}</td>
- </tr>
- ${list.map((t) => `<tr>
- <td><span class="cert-tag trans">运输证</span></td>
- <td class="cell-no">${t.transNo || "—"}</td>
- <td>${fmtTon(t.transQty)} ${c.unit}</td>
- <td>${fmtTon(transItemGap(t))} ${c.unit}</td>
- <td>${warnQtyField(c.id, t.id, c.unit)}</td>
- </tr>`).join("")}
- </tbody>
- </table></div>`;
- }
- document.getElementById("detailWarns").innerHTML = [
- remainGap(c) < -1e-6 ? `<div class="alert red">购买证关联计划量已超证载 ${(-remainGap(c)).toFixed(2)} ${c.unit}。</div>` : "",
- buyWarn(c) && remainGap(c) >= 0 ? `<div class="alert red">购买证余量 ${fmtTon(remain(c))} ${c.unit} ≤ ${fmtTon(buyWarnQty(c))} 吨预警。</div>` : "",
- ...warnItems.map((t) =>
- `<div class="alert red">运输证 ${t.transNo} 余量 ${transItemRemain(t)} ${c.unit}(阈值 ${fmtTon(transWarnQty(t))} 吨)${transItemGap(t) < 0 ? ",已超证" : ",请尽快安排同公司车辆发运"}。</div>`
- ),
- ...vehRows.filter((r) => r.level === "over").map((r) =>
- `<div class="alert red">${r.plate} 关联计划量 ${r.hold} ${c.unit},${r.text}。</div>`
- )
- ].filter(Boolean).join("");
- document.getElementById("certVehicleWarn").innerHTML = vehRows.length ? `<table>
- <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>
- <tbody>${vehRows.map((r) => {
- const t = findTrans(c, r.transId) || { transQty: 0 };
- const leftVeh = (r.cap || 0) - r.hold;
- const buyLeft = remainGap(c);
- const transLeft = transItemGap(t);
- const nPlan = vehLinkedPlans(r.plate, r.transId).length;
- const plateArg = String(r.plate).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
- return `<tr class="${r.level === "over" ? "veh-over" : ""}">
- <td class="cell-no">${r.plate}</td>
- <td>${r.driver || "—"}</td>
- <td>${r.transNo}<div class="muted">${r.carrier || ""}</div></td>
- <td><button class="plan-n-link${nPlan ? "" : " off"}" type="button" onclick="openVehPlanDlg(${c.id}, ${r.transId}, '${plateArg}')">${nPlan} 条</button></td>
- <td>${fmtTon(r.bound || r.cap || 0)} ${c.unit}<div class="muted">分派 ${r.cap || 0}</div></td>
- <td class="${r.level === "over" ? "staff-due" : ""}">${fmtTon(r.planHold || 0)} ${c.unit}${r.level === "over" ? ` <span class="need-tag">超</span>` : ""}</td>
- <td>${fmtTon(r.shipped || 0)} ${c.unit}</td>
- <td class="${leftVeh < -1e-6 ? "staff-due" : ""}">${leftVeh.toFixed(2)} ${c.unit}</td>
- <td class="${buyLeft < -1e-6 ? "staff-due" : ""}">${buyLeft.toFixed(2)} ${c.unit}</td>
- <td class="${transLeft < -1e-6 ? "staff-due" : ""}">${transLeft.toFixed(2)} ${c.unit}</td>
- <td>${r.level ? `<span class="remain ${r.level === "over" ? "bad" : "warn"}">${r.text}</span>` : `<span class="remain">正常</span>`}</td>
- </tr>`;
- }).join("")}</tbody></table>` : `<div class="empty">上传运输证后会带出车号。人工勾选计划后,各车计划量对照购买证 / 运输证证载预警。</div>`;
- document.getElementById("certLinkedPlans").innerHTML = linked.length ? `<table>
- <thead><tr><th>计划编号</th><th>状态</th><th>品名</th><th>车辆</th><th>占用运输证</th><th>计划量</th><th>已完成扣减</th></tr></thead>
- <tbody>${linked.map((p) => {
- const t = findTrans(c, p.transId);
- const over = t && p.tractor && vehicleOver({ plate: p.tractor, qty: ((t.dispatches || []).find((d) => d.plate === p.tractor) || {}).qty || 0 }, t);
- const doneN = deductQty(p);
- return `<tr class="${over ? "veh-over" : ""}">
- <td class="cell-no">${p.no}</td>
- <td>${planStHtml(p)}</td>
- <td>${p.product}<div class="muted">${p.ctrl || ""}</div></td>
- <td>${p.tractor || "未派车"}<div class="muted">${p.driver || ""}</div></td>
- <td>${t ? t.transNo : "—"}</td>
- <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>
- <td class="num">${doneN ? doneN + " " + (p.unit || c.unit) : "未扣减"}</td>
- </tr>`;
- }).join("")}</tbody></table>` : `<div class="empty">尚未关联计划。</div>`;
- const transCards = list.map((t, i) => {
- const left = transItemRemain(t);
- const warn = transItemWarn(t);
- const used = transItemHold(t);
- const tPct = Math.min(100, (used / (Number(t.transQty || 1) || 1)) * 100);
- return `<article class="card">
- <div class="card-h">
- <h2>${t.transNo || "未编号"}</h2>
- <span class="chip ghost">${t.transFile || "已识别"}</span>
- </div>
- <div class="card-b"><div class="kv">
- ${kv("编号", t.transNo)}${kv("核发日期", t.transDate)}
- ${kv("货主", t.owner)}${kv("法定代表人", t.ownerLegal)}
- ${kv("运输物品", c.product)}${kv("数量", (t.transQty || "—") + " " + c.unit)}
- ${kv("预警量", warnQtyField(c.id, t.id, c.unit))}
- ${kv("包装", t.pack)}${kv("购买许可证号", t.permitNo)}
- ${kv("运输单位", t.carrier)}${kv("运输方式", t.mode)}
- ${kv("车号(次)", t.plates)}${kv("收货单位", t.consignee)}
- ${kv("运输路线", t.route)}${kv("申请单位", t.applicantUnit)}
- ${kv("有效期限", (t.transFrom || "") + " 至 " + (t.transDue || ""))}
- ${kv("公安机关", t.transPolice)}${kv("经办人", t.transHandler)}
- </div>
- <div style="margin-top:14px;color:var(--muted);font-size:12px">本证进度(已完成 ${transItemDeduct(t)} + 在途计划 ${transItemPlan(t)} / 运输证 ${t.transQty || 0})${warn ? ` · 余量 ${left} 吨预警` : ""}</div>
- <div class="qty-bar"><div class="qty-fill${warn ? " warn" : ""}" style="width:${tPct}%"></div></div>
- ${certThumbHtml(t.transUrl, "运输证")}
- </div>
- </article>`;
- }).join("");
- const transBlock = transCards
- ? `<div class="trans-card-row">${transCards}</div>`
- : `<article class="card"><div class="card-b"><div class="empty">尚未识别运输证,请点击「再识别一张运输证」。</div></div></article>`;
- document.getElementById("detailSheet").innerHTML = `
- <div class="cert-duo">
- <div class="cert-pane buy">
- <div class="cert-pane-h">
- <span class="cert-tag buy">购买证</span>
- <h2>购买备案证明</h2>
- </div>
- <article class="card">
- <div class="card-h"><h2>${c.buyNo || "未编号"}</h2><span class="chip ghost">${c.buyFile || "已识别"}</span></div>
- <div class="card-b"><div class="kv">
- ${kv("编号", c.buyNo)}${kv("核发日期", c.buyDate)}
- ${kv("购买单位", c.buyer)}${kv("法定代表人", c.buyerLegal)}
- ${kv("电话", c.buyerTel)}${kv("住所", c.buyerAddr)}
- ${kv("销售单位", c.seller)}${kv("销售方法人", c.sellerLegal)}
- ${kv("购买物品", c.product)}${kv("用途", c.usage)}
- ${kv("数量", c.buyQty + " " + c.unit)}${kv("预警量", warnQtyField(c.id, null, c.unit))}
- ${kv("有效次数", c.buyTimes)}
- ${kv("有效期限", (c.buyFrom || "") + " 至 " + (c.buyDue || ""))}
- ${kv("公安机关", c.buyPolice)}${kv("经办人", c.buyHandler)}
- </div>${certThumbHtml(c.buyUrl, "购买证")}</div>
- </article>
- </div>
- <div class="cert-pane trans">
- <div class="cert-pane-h">
- <span class="cert-tag trans">运输证</span>
- <h2>运输备案证明</h2>
- <span class="muted">${list.length} 张</span>
- </div>
- ${transBlock}
- </div>
- </div>`;
- document.getElementById("dispatchHint").textContent =
- `本购买证挂 ${list.length} 张运输证${companies.length ? "(" + companies.join("、") + ")" : ""}。按各运输证数量分别分派同公司车辆;运输证合计剩余 ${tr}${c.unit}。`;
- document.getElementById("detailDispatch").innerHTML = list.length ? list.map((t) => {
- const cars = t.dispatches || [];
- const left = transItemRemain(t);
- return `<div style="margin-bottom:16px">
- <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px">
- <strong><span class="cert-tag trans" style="margin-right:6px">运输证</span>${t.transNo}</strong>
- <span class="muted">${t.carrier || "—"} · 运量 ${t.transQty || 0}${c.unit} · 已分 ${assignedSum(t)}${c.unit} · 未分 ${unassignedTrans(t)}${c.unit}${transItemWarn(t) ? ` · 余 ${left} 吨预警` : ""}</span>
- <span style="display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)">预警量 ${warnQtyField(c.id, t.id, c.unit)}</span>
- <div class="actions">
- <button class="btn btn-sm" type="button" onclick="splitTransToVehicles(${c.id}, ${t.id})">按本证均分</button>
- <button class="btn btn-sm btn-primary" type="button" onclick="openDispatchDlg(${t.id})">分派车辆</button>
- </div>
- </div>
- ${cars.length ? `<table>
- <thead><tr><th>车牌</th><th>驾驶员</th><th>运输公司</th><th>分派运量</th><th>关联计划量</th><th>已运</th><th>对照预警</th></tr></thead>
- <tbody>${cars.map((d, i) => {
- const hold = vehicleHold(d.plate, t.id);
- const planHold = vehiclePlanHold(d.plate, t.id);
- const shipped = vehicleDeduct(d.plate, t.id);
- const over = vehicleOver(d, t);
- const alert = vehicleAlertOf(c, t, hold, Number(d.qty || 0), planHold, shipped);
- return `<tr class="${over ? "veh-over" : ""}">
- <td class="cell-no">${d.plate}</td><td>${d.driver || "—"}</td>
- <td>${t.carrier || "—"}</td>
- <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"
- onchange="updateDispatchQty(${t.id}, ${i}, this.value)" /></td>
- <td class="${over ? "staff-due" : ""}">${fmtTon(planHold)} ${c.unit}${over ? ` <span class="need-tag">超</span>` : ""}</td>
- <td>${fmtTon(shipped)} ${c.unit}</td>
- <td>${alert.level ? `<span class="remain ${alert.level === "over" ? "bad" : "warn"}">${alert.text}</span>` : `<span class="remain">正常</span>`}</td>
- </tr>`;
- }).join("")}</tbody>
- </table>` : `<div class="empty" style="padding:16px 0">本运输证尚未分派车辆。证件上的车号会在上传时自动带入。</div>`}
- </div>`;
- }).join("") : `<div class="empty">请先识别运输证,再按证分派车辆。</div>`;
- updateBadges();
- }
- function fillCertForm(data) {
- const form = document.getElementById("certForm");
- Object.entries(data).forEach(([k, v]) => {
- const el = form.querySelector(`[name="${k}"]`);
- if (el) el.value = v;
- });
- }
- function setOcrStatus(kind, text, ok) {
- const el = document.getElementById(kind + "Status");
- if (!el) return;
- el.textContent = text;
- el.classList.toggle("ok", !!ok);
- }
- function transSlotHtml(n) {
- return `<div class="extra-trans" data-slot="${n}">
- <h4><span class="cert-tag trans">运输证</span> 运输证信息(第 ${n + 1} 张)
- <button class="btn btn-sm" type="button" style="margin-left:auto" onclick="this.closest('.extra-trans').remove()">移除</button></h4>
- <label class="upload" style="margin-bottom:10px">
- <input type="file" accept="image/*,.pdf" onchange="recognizeExtraTrans(this)" />
- <span class="plus">+</span><span>上传本张运输证</span>
- </label>
- <img class="ocr-preview hidden extra-preview" alt="运输证预览" />
- <div class="form trans-fields" style="grid-template-columns:1fr 1fr">
- <label><span><span class="star">*</span>编号</span><input name="transNo" required /></label>
- <label>核发日期<input name="transDate" type="date" /></label>
- <label class="span2">货主<input name="owner" /></label>
- <label>法定代表人<input name="ownerLegal" /></label>
- <label>电话<input name="ownerTel" /></label>
- <label class="span2">货主住所<input name="ownerAddr" /></label>
- <label>包装<input name="pack" /></label>
- <label>购买许可/备案证号<input name="permitNo" /></label>
- <label class="span2">运输单位<input name="carrier" /></label>
- <label>住所/电话<input name="carrierAddr" /></label>
- <label>运输方式<input name="mode" /></label>
- <label>车号(次)<input name="plates" /></label>
- <label class="span2">运输路线<input name="route" /></label>
- <label class="span2">收货单位<input name="consignee" /></label>
- <label>运输数量<input name="transQty" type="number" step="0.01" /></label>
- <label>余量预警<input name="warnQty" type="number" step="0.01" min="0" placeholder="核准总量 10%" title="默认按证件核准总量的 10% 预警,可改" /></label>
- <label>有效次数<input name="transTimes" /></label>
- <label>有效开始<input name="transFrom" type="date" /></label>
- <label>有效截止<input name="transDue" type="date" /></label>
- <label class="span2">申请单位<input name="applicantUnit" /></label>
- <label class="span2">公安机关<input name="transPolice" /></label>
- <label>经办人<input name="transHandler" /></label>
- </div>
- </div>`;
- }
- function addTransSlot() {
- extraTransN += 1;
- document.getElementById("extraTransSlots").insertAdjacentHTML("beforeend", transSlotHtml(extraTransN));
- const box = document.querySelector(`#extraTransSlots .extra-trans[data-slot="${extraTransN}"]`);
- bindExtraTransDrop(box);
- }
- function bindCertDrop(el, kind) {
- if (!el || el.dataset.dropBound) return;
- el.dataset.dropBound = "1";
- el.addEventListener("dragover", (e) => { e.preventDefault(); el.classList.add("drag"); });
- el.addEventListener("dragleave", () => el.classList.remove("drag"));
- el.addEventListener("drop", (e) => {
- e.preventDefault();
- el.classList.remove("drag");
- const file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
- if (!file) return;
- const input = document.getElementById(kind === "buy" ? "buyFile" : "transFile");
- if (!input) return;
- const dt = new DataTransfer();
- dt.items.add(file);
- input.files = dt.files;
- recognize(kind, input);
- });
- }
- function bindExtraTransDrop(box) {
- if (!box || box.dataset.dropBound) return;
- box.dataset.dropBound = "1";
- box.addEventListener("dragover", (e) => { e.preventDefault(); box.classList.add("drag"); });
- box.addEventListener("dragleave", () => box.classList.remove("drag"));
- box.addEventListener("drop", (e) => {
- e.preventDefault();
- box.classList.remove("drag");
- const file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
- const input = box.querySelector("input[type=file]");
- if (!file || !input) return;
- const dt = new DataTransfer();
- dt.items.add(file);
- input.files = dt.files;
- recognizeExtraTrans(input);
- });
- }
- function recognize(kind, input) {
- const file = input.files && input.files[0];
- if (!file) return;
- certFiles[kind] = file.name;
- const preview = document.getElementById(kind === "buy" ? "buyPreview" : "transPreview");
- const reader = new FileReader();
- reader.onload = () => {
- preview.src = reader.result;
- preview.classList.remove("hidden");
- certFiles[kind + "Url"] = reader.result;
- };
- reader.readAsDataURL(file);
- setOcrStatus(kind, "正在识别…", false);
- setTimeout(() => {
- if (kind === "buy") {
- fillFields(document.getElementById("buyFormCol"), ocrBuySample);
- ensureWarnQtyInputs(document.getElementById("buyFormCol"));
- } else {
- fillFields(document.getElementById("transForm0"), ocrTransSample);
- ensureWarnQtyInputs(document.getElementById("transForm0"));
- }
- setOcrStatus(kind, "识别完成,请核对后保存", true);
- toast(kind === "buy" ? "购买证已识别" : "运输证已识别");
- }, 600);
- }
- function recognizeExtraTrans(input) {
- const file = input.files && input.files[0];
- const box = input.closest(".extra-trans");
- if (!file || !box) return;
- const preview = box.querySelector(".extra-preview");
- const reader = new FileReader();
- reader.onload = () => {
- preview.src = reader.result;
- preview.classList.remove("hidden");
- box.dataset.file = file.name;
- box.dataset.url = reader.result;
- };
- reader.readAsDataURL(file);
- setTimeout(() => {
- fillFields(box, ocrTransSamples[extraTransN % ocrTransSamples.length]);
- ensureWarnQtyInputs(box);
- toast("运输证已识别");
- }, 500);
- }
- function setBuyFieldsRequired(on) {
- ["buyNo", "buyer", "product", "buyQty"].forEach((name) => {
- const el = document.querySelector(`#buyFormCol [name="${name}"]`);
- if (el) el.required = on;
- });
- }
- function openCertDlg(demo, buyId) {
- const form = document.getElementById("certForm");
- form.reset();
- form.querySelector("[name=unit]").value = "吨";
- ensureWarnQtyInputs(form);
- attachBuyId = buyId || null;
- extraTransN = 0;
- document.getElementById("extraTransSlots").innerHTML = "";
- certFiles.buy = certFiles.trans = null;
- certFiles.buyUrl = certFiles.transUrl = "";
- ["buyPreview", "transPreview"].forEach((id) => {
- document.getElementById(id).classList.add("hidden");
- document.getElementById(id).removeAttribute("src");
- });
- document.getElementById("buyFile").value = "";
- document.getElementById("transFile").value = "";
- const appending = !!attachBuyId;
- document.getElementById("buyOcrBox").classList.toggle("hidden", appending);
- document.getElementById("buyFormCol").classList.toggle("hidden", appending);
- setBuyFieldsRequired(!appending);
- document.getElementById("certDlgTitle").textContent = appending ? "追加运输证" : "上传购买证及运输证";
- document.getElementById("certDlgHint").textContent = appending
- ? "在已有购买证下继续上传运输证,车号将自动进入分派。"
- : "先上传购买证和运输证并核对数量、车号;保存后再人工勾选对应运输计划。";
- document.getElementById("certSaveBtn").textContent = appending ? "追加运输证" : "保存证件";
- if (demo) {
- fillFields(document.getElementById("buyFormCol"), ocrBuySample);
- fillFields(document.getElementById("transForm0"), ocrTransSample);
- certFiles.buy = "购买备案证明样例.png";
- certFiles.trans = "运输备案证明样例.png";
- certFiles.buyUrl = "购买备案证明样例.png";
- certFiles.transUrl = "运输备案证明样例.png";
- const bp = document.getElementById("buyPreview");
- const tp = document.getElementById("transPreview");
- bp.src = certFiles.buyUrl; bp.classList.remove("hidden");
- tp.src = certFiles.transUrl; tp.classList.remove("hidden");
- setOcrStatus("buy", "已填入购买备案证明样例", true);
- setOcrStatus("trans", "已填入运输备案证明样例", true);
- } else {
- setOcrStatus("buy", appending ? "沿用已有购买证" : "待上传", appending);
- setOcrStatus("trans", "待上传", false);
- }
- document.getElementById("certDialog").showModal();
- }
- function closeCertDlg() {
- document.getElementById("certDialog").close();
- attachBuyId = null;
- setBuyFieldsRequired(true);
- }
- function transDraftFromFields(f, file, url) {
- return {
- id: ++transSeq, created: today, shipped: 0,
- transNo: f.transNo, transDate: f.transDate, owner: f.owner, ownerLegal: f.ownerLegal,
- ownerTel: f.ownerTel, ownerAddr: f.ownerAddr, pack: f.pack, permitNo: f.permitNo,
- carrier: f.carrier, carrierAddr: f.carrierAddr, mode: f.mode, plates: f.plates,
- route: f.route, consignee: f.consignee, transQty: f.transQty === "" ? 0 : +f.transQty,
- transTimes: f.transTimes, transFrom: f.transFrom, transDue: f.transDue,
- applicantUnit: f.applicantUnit, transPolice: f.transPolice, transHandler: f.transHandler,
- warnQty: warnQtyOf(f.warnQty, f.transQty),
- transFile: file || "运输证.jpg", transUrl: url || "", dispatches: []
- };
- }
- function collectTransDrafts() {
- const list = [];
- const first = fieldsOf(document.getElementById("transForm0"));
- if ((first.transNo || "").trim()) {
- list.push(transDraftFromFields(first, certFiles.trans, certFiles.transUrl));
- }
- document.querySelectorAll("#extraTransSlots .extra-trans").forEach((box) => {
- const f = fieldsOf(box);
- if (!(f.transNo || "").trim()) return;
- list.push(transDraftFromFields(f, box.dataset.file, box.dataset.url));
- });
- return list;
- }
- function saveCert() {
- const form = document.getElementById("certForm");
- if (!form.reportValidity()) return;
- const transDrafts = collectTransDrafts();
- if (!transDrafts.length) {
- toast("请至少填写一张运输证编号");
- return;
- }
- const hasTransFile = !!(certFiles.trans || certFiles.transUrl) ||
- [...document.querySelectorAll("#extraTransSlots .extra-trans")].some((b) => b.dataset.file || b.dataset.url);
- if (!attachBuyId && !(certFiles.buy || certFiles.buyUrl)) {
- toast("请先上传购买证");
- return;
- }
- if (!hasTransFile) {
- toast("请先上传运输证");
- return;
- }
- transDrafts.forEach(seedDispatchesOnTrans);
- if (attachBuyId) {
- const c = certs.find((x) => x.id === attachBuyId);
- if (!c) return;
- (c.transList ||= []).push(...transDrafts);
- closeCertDlg();
- toast(`已追加 ${transDrafts.length} 张运输证,可在本页设置预警量并分派车辆`);
- showMod("cert");
- showCertView("detail", c.id);
- return;
- }
- const buy = fieldsOf(document.getElementById("buyFormCol"));
- const rec = {
- ...buy, id: ++certSeq, created: today, shipped: 0,
- buyQty: +buy.buyQty, unit: buy.unit || "吨",
- warnQty: warnQtyOf(buy.warnQty, buy.buyQty),
- ctrl: /硝|铵|炸药/.test(buy.product || "") ? "易制爆" : "易制毒",
- buyFile: certFiles.buy || "购买证.jpg",
- buyUrl: certFiles.buyUrl || "",
- transList: transDrafts
- };
- certs.unshift(rec);
- closeCertDlg();
- showMod("cert");
- showCertView("detail", rec.id);
- const matchIds = waitLinkPlans().filter((p) => planMatchesCert(p, rec)).map((p) => p.id);
- toast(matchIds.length
- ? `已保存。可在本页设置预警量、分派车辆;有 ${matchIds.length} 条计划可关联`
- : "已保存。可在本页设置购买证 / 运输证预警量并分派车辆");
- }
- function seedDispatchesOnTrans(t) {
- const raw = String(t.plates || "").split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean);
- const company = t.carrier || "";
- const seen = new Set();
- t.dispatches = [];
- raw.forEach((plate) => {
- if (seen.has(plate)) return;
- seen.add(plate);
- let meta = carrierFleet.find((v) => v.plate === plate);
- if (!meta) {
- meta = { company: company || "未登记运输公司", plate, driver: "", cap: 32 };
- if (company) carrierFleet.push(meta);
- }
- if (company && meta.company && meta.company !== company) return;
- t.dispatches.push({ plate, driver: meta.driver || "", qty: 0, shipped: 0 });
- });
- if (t.dispatches.length) splitOneTrans(t, true);
- }
- function activeTrans() {
- const c = certs.find((x) => x.id === currentCertId);
- return c ? findTrans(c, currentTransId) : null;
- }
- function openDispatchDlg(transId) {
- const c = certs.find((x) => x.id === currentCertId);
- if (!c) return;
- currentTransId = transId;
- const t = findTrans(c, transId);
- if (!t) return;
- const form = document.getElementById("dispatchForm");
- form.reset();
- form.querySelector("[name=company]").value = t.carrier || "";
- const used = new Set((t.dispatches || []).map((d) => d.plate));
- let pool = sameCompanyCars(t.carrier).filter((v) => !used.has(v.plate));
- if (!pool.length) pool = carrierFleet.filter((v) => !used.has(v.plate) && (!t.carrier || v.company === t.carrier));
- const sel = form.querySelector("[name=plate]");
- sel.innerHTML = pool.map((v) => `<option value="${v.plate}">${v.plate} · ${v.driver}(核定 ${v.cap} 吨)</option>`).join("")
- + `<option value="__new__">录入同公司其他车辆…</option>`;
- if (!pool.length) sel.value = "__new__";
- form.querySelector("[name=qty]").value = unassignedTrans(t) || "";
- document.getElementById("dispatchDlgHint").textContent =
- `「${t.carrier || "运输公司"}」车辆共用运输证 ${t.transNo}。证载 ${t.transQty || 0}${c.unit},未分派 ${unassignedTrans(t)}${c.unit},计划占用 ${transItemHold(t)}${c.unit}。`;
- syncDispatchDriver();
- document.getElementById("dispatchDialog").showModal();
- }
- function closeDispatchDlg() { document.getElementById("dispatchDialog").close(); }
- function syncDispatchDriver() {
- const plate = document.querySelector("#dispatchForm [name=plate]").value;
- const isNew = plate === "__new__";
- document.getElementById("dispatchNewPlateRow").classList.toggle("hidden", !isNew);
- document.querySelector("#dispatchForm [name=newPlate]").required = isNew;
- const v = carrierFleet.find((x) => x.plate === plate);
- if (v) document.querySelector("#dispatchForm [name=driver]").value = v.driver;
- }
- function saveDispatch() {
- const c = certs.find((x) => x.id === currentCertId);
- const t = activeTrans();
- const form = document.getElementById("dispatchForm");
- if (!c || !t || !form.reportValidity()) return;
- let plate = form.querySelector("[name=plate]").value;
- if (plate === "__new__") {
- plate = (form.querySelector("[name=newPlate]").value || "").trim();
- if (!plate) {
- toast("请填写同公司车牌");
- return;
- }
- if (!carrierFleet.some((x) => x.plate === plate)) {
- carrierFleet.push({ company: t.carrier || "", plate, driver: form.querySelector("[name=driver]").value, cap: 32 });
- }
- }
- const driver = form.querySelector("[name=driver]").value;
- const qty = +form.querySelector("[name=qty]").value;
- const v = carrierFleet.find((x) => x.plate === plate);
- if (v && t.carrier && v.company !== t.carrier) {
- toast("只能分派同一运输公司的车辆,以便共用一张运输证");
- return;
- }
- if ((t.dispatches || []).some((d) => d.plate === plate)) {
- toast("该车已在本运输证下");
- return;
- }
- if (qty - unassignedTrans(t) > 1e-6) {
- toast(`分派量超过运输证未分派量 ${unassignedTrans(t)} ${c.unit}`);
- return;
- }
- (t.dispatches ||= []).push({ plate, driver, qty, shipped: 0 });
- t.plates = t.dispatches.map((d) => d.plate).join("、");
- closeDispatchDlg();
- toast(`已将 ${qty}${c.unit} 分派给 ${plate}`);
- renderCertDetail();
- }
- function splitOneTrans(t, silent) {
- if (!t || !(t.dispatches || []).length) {
- if (!silent) toast("请先分派车辆");
- return false;
- }
- const n = t.dispatches.length;
- const total = Number(t.transQty || 0);
- const base = Math.floor((total / n) * 100) / 100;
- let acc = 0;
- t.dispatches.forEach((d, i) => {
- d.qty = i === n - 1 ? Math.round((total - acc) * 100) / 100 : base;
- acc += d.qty;
- if (Number(d.shipped) > Number(d.qty)) d.shipped = d.qty;
- });
- return true;
- }
- function splitTransToVehicles(certId, transId) {
- const c = certs.find((x) => x.id === (certId || currentCertId));
- const t = c ? findTrans(c, transId) : null;
- if (!c || !t) return;
- if (!splitOneTrans(t, false)) return;
- toast(`已按运输证 ${t.transQty}${c.unit} 均分到 ${t.dispatches.length} 辆同公司车辆`);
- renderCertDetail();
- }
- function updateDispatchQty(transId, index, value) {
- const c = certs.find((x) => x.id === currentCertId);
- const t = c ? findTrans(c, transId) : null;
- if (!c || !t || !t.dispatches[index]) return;
- const d = t.dispatches[index];
- const qty = Math.max(Number(d.shipped || 0), +value || 0);
- const others = assignedSum(t) - Number(d.qty);
- if (others + qty - Number(t.transQty || 0) > 1e-6) {
- toast(`分派合计不能超过运输证 ${t.transQty} ${c.unit}`);
- renderCertDetail();
- return;
- }
- d.qty = qty;
- toast(`${d.plate} 分派运量已改为 ${qty} ${c.unit}`);
- renderCertDetail();
- }
- function pickVehicleForTrans(t, needQty, plate) {
- const list = (t.dispatches || []).slice();
- if (plate) {
- const hit = list.find((d) => d.plate === plate);
- if (hit) return hit;
- }
- list.sort((a, b) =>
- (Number(b.qty) - vehicleHold(b.plate, t.id)) - (Number(a.qty) - vehicleHold(a.plate, t.id)));
- return list.find((d) => Number(d.qty) - vehicleHold(d.plate, t.id) + 1e-9 >= needQty) || list[0] || null;
- }
- function pickVehicleForPlan(cert, needQty, transId, plate) {
- const t = transId ? findTrans(cert, transId) : transOf(cert)[0];
- if (!t) return null;
- return pickVehicleForTrans(t, needQty, plate);
- }
- function applyShipToTrans(t, plate, qty) {
- if (!t || !(t.dispatches || []).length) return;
- let left = qty;
- const hit = t.dispatches.find((d) => d.plate === plate);
- const order = hit ? [hit, ...t.dispatches.filter((d) => d !== hit)] : t.dispatches;
- order.forEach((d) => {
- if (left <= 0) return;
- const room = Math.max(0, Number(d.qty) - Number(d.shipped || 0));
- const take = Math.min(room, left);
- d.shipped = Number(d.shipped || 0) + take;
- left -= take;
- });
- }
- function applyShipToDispatch(cert, plate, qty, transId) {
- const t = transId ? findTrans(cert, transId) : transOf(cert)[0];
- applyShipToTrans(t, plate, qty);
- }
- function planCrew(p) {
- const names = [p.driver, p.escort].filter(Boolean);
- return names.length ? names.join(" / ") : "—";
- }
- function planDeliverAt(p) {
- const d = p.deliverDate || p.date;
- if (!d) return "—";
- return p.deliverTime ? `${d} ${p.deliverTime}` : d;
- }
- function planContractNo(p) {
- return p.contractNo || "—";
- }
- function planStHtml(p) {
- const s = typeof p === "string" ? p : p.status;
- if (typeof p === "object" && p.waitNotify && s === "待安排") return `<span class="st-pill wait">等客户通知</span>`;
- if (typeof p === "object" && p.archived) return `<span class="st-pill arch">已归档</span>`;
- if (s === "已完成") return `<span class="st-pill fin">已完成</span>`;
- if (s === "已安排") return `<span class="st-pill ok">已安排</span>`;
- if (s === "已作废") return `<span class="st-pill void">已作废</span>`;
- if (s === "草稿") return `<span class="st-pill">草稿</span>`;
- if (s === "待安排") return `<span class="st-pill wait">待安排</span>`;
- return `<span class="st-pill wait">${s || "待安排"}</span>`;
- }
- function planTabMatch(p, tab) {
- if (tab === "待安排") return p.status === "待安排";
- if (tab === "已安排") return p.status === "已安排";
- if (tab === "草稿中") return p.status === "草稿";
- if (tab === "入库单") return p.type === "入直单" || p.type === "入库单";
- if (tab === "出库单") return p.type === "出库单";
- if (tab === "直销单") return p.type === "直销单";
- return true;
- }
- function planTabStatus(tab) {
- if (tab === "待处理" || tab === "待安排") return "待安排";
- if (tab === "审核中" || tab === "已安排") return "已安排";
- if (tab === "草稿中") return "草稿";
- if (tab === "已完成") return "已完成";
- if (tab === "已作废") return "已作废";
- return "";
- }
- function productCell(p) {
- const tags = [];
- if (p.shipMode === "槽车运输" && p.product !== "槽车运输") tags.push(`<span class="prod-tag orange">槽车运输</span>`);
- if (p.ctrl === "易制毒") tags.push(`<span class="prod-tag red">易制毒</span>`);
- if (p.ctrl === "易制爆") tags.push(`<span class="prod-tag orange">易制爆</span>`);
- const name = p.product === "槽车运输" ? `<span class="prod-tag orange">槽车运输</span>` : (p.product || "—");
- return `${name}${tags.join("")}`;
- }
- function matchCell(p) {
- if (p.status === "已作废") return "—";
- if (p.certId) return `<span class="match-tag yes">有匹配 [3]</span> <span class="match-tag ok">已匹配</span>`;
- if (p.ctrl) return `<span class="match-tag yes">有匹配 [3]</span>`;
- if (p.status === "已完成") return `<span class="match-tag yes">有匹配 [3]</span> <span class="match-tag ok">已匹配</span>`;
- return "—";
- }
- function sourceCell(p) {
- const cert = certs.find((c) => c.id === p.certId);
- return `<div class="src-cell">
- <span class="ico"><span class="src-dot">${(p.source || "手").slice(0, 1)}</span>${p.sourceNo || p.no}</span>
- <span>${cert ? cert.buyNo : p.no}</span>
- </div>`;
- }
- function planOps(p) {
- if (p.status === "已作废") return "";
- const taskBtn = `<button class="op-link" type="button" onclick="openPlanTaskDlg(${p.id})">${p.taskNo ? "查看任务单" : "下发"}</button>`;
- if (p.status === "待安排") {
- const extra = (p.ctrl && !p.certId) ? `<button class="op-link" type="button" onclick="openLinkCertDlg(${p.id})">关联</button>` : "";
- return `${extra}<button class="op-link" type="button" onclick="arrangePlan(${p.id})">审核</button>${taskBtn}`;
- }
- if (p.status === "已安排") {
- return `<button class="op-link" type="button" onclick="unapprovePlan(${p.id})">反审核</button><button class="op-link" type="button" onclick="completePlan(${p.id})">完成</button>${taskBtn}`;
- }
- if (p.status === "已完成" && !p.archived) {
- return `<button class="op-link" type="button" onclick="showMod('archive')">去归档审核</button>${taskBtn}`;
- }
- return `<button class="op-link" type="button" onclick="showMod('archive')">查看归档</button>${taskBtn}`;
- }
- function selectedPlans() {
- return plans.filter((p) => selectedPlanIds.has(p.id));
- }
- function togglePlanSel(id, on) {
- if (on) selectedPlanIds.add(id);
- else selectedPlanIds.delete(id);
- const row = document.querySelector(`#planTable tbody input[data-id="${id}"]`)?.closest("tr");
- if (row) row.classList.toggle("sel", on);
- }
- function togglePlanSelAll(on) {
- document.querySelectorAll("#planTable tbody input[type=checkbox]").forEach((el) => {
- el.checked = on;
- togglePlanSel(+el.dataset.id, on);
- });
- }
- function isWaitPrecursorPlan(p) {
- return p.ctrl === "易制毒" && !p.certId && p.status !== "已完成" && p.status !== "已作废";
- }
- function isTomorrowWaitPlan(p) {
- if (!p || p.status !== "待安排") return false;
- const tom = dayShift(today, 1);
- return p.date === tom || p.deliverDate === tom;
- }
- function isTodayPlan(p) {
- if (!p || p.status === "已作废") return false;
- return p.date === today || p.deliverDate === today;
- }
- function isTodayDonePlan(p) {
- return isTodayPlan(p) && p.status === "已完成";
- }
- function isTodayOpenPlan(p) {
- return isTodayPlan(p) && p.status !== "已完成";
- }
- function isWaitReviewPrecursorPlan(p) {
- return !!(p && p.ctrl === "易制毒" && p.status === "待安排");
- }
- function isWaitArchivePlan(p) {
- return !!(p && p.status === "已完成" && !p.archived);
- }
- function planCardMatch(p, key) {
- if (key === "tomorrowWait") return isTomorrowWaitPlan(p);
- if (key === "todayAll") return isTodayPlan(p);
- if (key === "todayDone") return isTodayDonePlan(p);
- if (key === "todayOpen") return isTodayOpenPlan(p);
- if (key === "precursorReview") return isWaitReviewPrecursorPlan(p);
- if (key === "waitArchive") return isWaitArchivePlan(p);
- return true;
- }
- function resetPlanListTabs() {
- planTab = "全部";
- planDateTab = "全部";
- document.querySelectorAll("#planTabs button").forEach((x) => x.classList.toggle("on", x.dataset.tab === "全部"));
- document.querySelectorAll("#planDateTabs button").forEach((x) => x.classList.toggle("on", x.dataset.tab === "全部"));
- }
- function setPlanCardFilter(key) {
- planCardFilter = planCardFilter === key ? "" : key;
- if (planCardFilter) resetPlanListTabs();
- renderPlans();
- if (planCardFilter) document.getElementById("planTable")?.scrollIntoView({ behavior: "smooth", block: "start" });
- }
- function toggleUnlinkedOnly() {
- setPlanCardFilter("precursorReview");
- }
- function toggleWaitPrecursorPlans() {
- setPlanCardFilter("precursorReview");
- }
- function batchAudit() {
- const list = selectedPlans();
- if (!list.length) { toast("请先勾选计划"); return; }
- const wait = list.find((p) => p.status === "待安排");
- if (wait) { arrangePlan(wait.id); return; }
- const ready = list.find((p) => p.status === "已安排");
- if (ready) { unapprovePlan(ready.id); return; }
- toast("所选计划无需审核或反审核");
- }
- function unapprovePlan(id) {
- const p = plans.find((x) => x.id === id);
- if (!p || p.status !== "已安排") return;
- p.status = "待安排";
- toast(`${p.no} 已反审核,回到待安排`);
- renderPlans();
- }
- function batchLinkCert() {
- goLinkBuyTransCert();
- }
- function goLinkBuyTransCert() {
- showMod("cert");
- if (!certs.length) {
- toast("请先上传购买证及运输证");
- openCertDlg(false);
- return;
- }
- showCertView("list");
- if (!waitLinkPlans().length) {
- toast("暂无待关联计划,可在证件上勾选");
- return;
- }
- toast("请人工核对运输证和车辆后点击「确认关联」");
- setTimeout(focusCertWait, 40);
- }
- function suggestProcFromPlan(p) {
- const text = `${p.product || ""} ${p.spec || ""}`;
- const qty = Number(p.realQty != null ? p.realQty : p.planQty) || 0;
- const conc = parseConc(text) || parseConc(p.spec);
- const grade = acidGradeOf(text);
- const isAcid = /硫酸|试剂酸|硫磺|槽车运输/.test(text) || p.spec === "CP";
- const isHcl = /盐酸/.test(text);
- const isAlkali = /烧碱|液碱/.test(text);
- let draft;
- if (isAcid && conc && conc < 90) {
- const rawName = grade === "reagent" ? "试剂硫酸" : "98%工业硫酸";
- const rawWh = grade === "reagent" ? "h2so4-rg" : "h2so4-98";
- const rawQty = +(qty * conc / 98).toFixed(3);
- const product = /槽车/.test(p.product || "")
- ? (grade === "reagent" ? conc + "%试剂酸" : conc + "%工业硫酸")
- : p.product;
- const round = conc < 40 ? 30 : conc < 70 ? 50 : 50;
- const toWh = suggestWarehouse(product, p.spec) || acidTargetWh(round);
- draft = { mode: "dilute", rawName, rawWh, rawQty, water: Math.max(0, +(qty - rawQty).toFixed(3)), product, toWh, outQty: qty };
- } else if (isHcl) {
- const toWh = suggestWarehouse(p.product, p.spec) || "hcl-31-ind";
- draft = { mode: "convert", rawName: p.product, rawWh: toWh, rawQty: qty, water: 0, product: p.product, toWh, outQty: qty };
- } else if (isAlkali) {
- const toWh = suggestWarehouse(p.product, p.spec) || "naoh-32";
- draft = { mode: "convert", rawName: p.product, rawWh: toWh, rawQty: qty, water: 0, product: p.product, toWh, outQty: qty };
- } else {
- const toWh = suggestWarehouse(p.product, p.spec) || warehouseForPlan(p) || "h2so4-50";
- draft = { mode: "convert", rawName: p.product, rawWh: toWh || "h2so4-50", rawQty: qty, water: 0, product: p.product, toWh: toWh || "h2so4-50", outQty: qty };
- }
- return draft;
- }
- function suggestTaskKind(p) {
- const text = `${p.product || ""} ${p.spec || ""} ${p.pack || ""}`;
- if (/分装|袋装|PL|桶装|吨桶|吨袋|IBC/.test(text)) return "分装";
- if (/精制|试剂/.test(text)) return "精制";
- return "稀释";
- }
- function suggestTaskContent(list, kind) {
- return list.map((p) => {
- const qty = Number(p.realQty) > 0 ? p.realQty : p.planQty;
- return `请按运输计划 ${p.no} 对 ${p.product}${p.spec ? "(" + p.spec + ")" : ""} ${qty}${p.unit} 执行${kind}作业。`;
- }).join("\n");
- }
- function defaultPlanTime() {
- return today + "T08:00";
- }
- function staffOptionsHtml(selected) {
- return SITE_STAFF.map((n) => `<option ${n === selected ? "selected" : ""}>${n}</option>`).join("");
- }
- function suggestAssignee(p) {
- const text = `${p.product || ""} ${p.spec || ""}`;
- if (/盐酸/.test(text)) return "张伟(盐酸库)";
- if (/烧碱|液碱/.test(text)) return "王磊(液碱库)";
- if (/分装|袋装/.test(text)) return "陈刚(分装库)";
- if (/精制|试剂/.test(text)) return "赵敏(精制岗)";
- return "刘洋(硫酸库)";
- }
- function previewTaskNo(kind) {
- const code = { 分装: "FZ", 稀释: "XS", 精制: "JZ" }[kind] || "FZ";
- return "SX" + code + today.replace(/-/g, "") + "-" + String(taskSeq + 1).padStart(3, "0");
- }
- function nextTaskNo(kind) {
- const code = { 分装: "FZ", 稀释: "XS", 精制: "JZ" }[kind] || "FZ";
- return "SX" + code + today.replace(/-/g, "") + "-" + String(++taskSeq).padStart(3, "0");
- }
- function planTaskLinesOf(list) {
- return list.map((p) => ({
- planId: p.id,
- planNo: p.no,
- product: p.product || "—",
- model: p.spec || "",
- pack: p.pack || "",
- unit: p.unit || "吨",
- qty: Number(p.realQty) > 0 ? p.realQty : p.planQty,
- done: false
- }));
- }
- function taskLinesOf(t) {
- if (t.lines && t.lines.length) return t.lines;
- return [{ product: t.product || "—", model: t.spec || "", pack: t.pack || "", unit: t.unit || "吨", qty: t.qty, done: t.status === "已完成" }];
- }
- function renderPlanTaskLines(list) {
- const lines = planTaskLinesOf(list);
- const pad = lines.concat(Array.from({ length: Math.max(0, 5 - lines.length) }, () => null));
- document.getElementById("planTaskLines").innerHTML = `<table class="task-line-table"><thead><tr>
- <th>序号</th><th>产品名称</th><th>产品型号</th><th>包装规格</th><th>单位</th><th>数量</th><th>是否完成</th>
- </tr></thead><tbody>${pad.map((ln, i) => ln ? `<tr>
- <td>${i + 1}</td><td>${ln.product}</td><td>${ln.model || "—"}</td><td>${ln.pack || "—"}</td>
- <td>${ln.unit}</td><td class="num">${ln.qty}</td><td>□ 完成</td>
- </tr>` : `<tr><td>${i + 1}</td><td></td><td></td><td></td><td></td><td></td><td></td></tr>`).join("")}</tbody></table>`;
- }
- function onPlanTaskKindChange() {
- const form = document.getElementById("planTaskForm");
- if (form) form.no.value = previewTaskNo(form.kind.value);
- }
- function refreshTaskContent() {
- onPlanTaskKindChange();
- }
- function taskStHtml(s) {
- const cls = s === "已完成" ? "st-run" : s === "执行中" ? "st-fix" : "st-stop";
- return `<span class="st ${cls}">${s}</span>`;
- }
- function openPlanTaskDlg(id) {
- let list = [];
- if (id != null && typeof id !== "object") {
- const one = plans.find((x) => x.id === id);
- if (one) list = [one];
- } else {
- list = selectedPlans().filter((x) => x.status !== "已作废");
- }
- if (!list.length) { toast("请先勾选运输计划,再点「下发任务单」"); return; }
- if (list.some((x) => x.status === "已作废")) { toast("已作废计划不能下发任务单"); return; }
- const linked = list.filter((x) => x.taskNo);
- if (linked.length === list.length) {
- const nos = [...new Set(linked.map((x) => x.taskNo))];
- const t = nos.length === 1 ? TASKS.find((x) => x.no === nos[0]) : null;
- if (t) { openTaskSheetDlg(t.id); return; }
- toast("所选计划均已下发任务单");
- showMod("task");
- return;
- }
- list = list.filter((x) => !x.taskNo);
- linkingPlanTaskIds = list.map((x) => x.id);
- const form = document.getElementById("planTaskForm");
- const kind = suggestTaskKind(list[0]);
- const orderBy = String(list[0].salesman || "调度员").split("/")[0].trim() || "调度员";
- form.kind.value = kind;
- form.no.value = previewTaskNo(kind);
- form.orderDate.value = today;
- form.deliverDate.value = list[0].deliverDate || list[0].date || today;
- form.orderBy.value = orderBy;
- form.handler.innerHTML = staffOptionsHtml(suggestAssignee(list[0]));
- form.maker.value = "郑丽娜";
- form.auditor.value = "";
- form.supervisor.value = "";
- form.bucket.value = "桶循环";
- form.remark.value = list.map((x) => x.customer || x.settler || "").filter(Boolean)[0] || "";
- renderPlanTaskLines(list);
- document.getElementById("planTaskHint").textContent =
- list.length === 1
- ? `来源计划 ${list[0].no} · ${planTypeName(list[0])}。明细按附件任务单字段带出,确认后下发。`
- : `已勾选 ${list.length} 条计划,将合并为 1 张${kind}任务单(产品明细多行)。`;
- document.getElementById("planTaskDialog").showModal();
- }
- function closePlanTaskDlg() {
- document.getElementById("planTaskDialog").close();
- linkingPlanTaskIds = [];
- }
- function savePlanTask() {
- const list = plans.filter((x) => linkingPlanTaskIds.includes(x.id));
- if (!list.length) { toast("未找到来源计划"); return; }
- const form = document.getElementById("planTaskForm");
- if (!form.reportValidity()) return;
- const f = Object.fromEntries(new FormData(form));
- const lines = planTaskLinesOf(list);
- const no = nextTaskNo(f.kind);
- const first = lines[0];
- TASKS.unshift({
- id: taskSeq, no, kind: f.kind, status: "待执行",
- date: f.orderDate || today,
- orderDate: f.orderDate, deliverDate: f.deliverDate,
- orderBy: f.orderBy, handler: f.handler, maker: f.maker || "郑丽娜",
- auditor: f.auditor || "", supervisor: f.supervisor || "",
- bucket: f.bucket || "桶循环", remark: f.remark || "",
- planTime: (f.deliverDate || today) + "T08:00",
- planId: list[0].id, planNo: list.map((x) => x.no).join("、"),
- product: first.product, spec: first.model, pack: first.pack,
- qty: lines.reduce((s, ln) => s + Number(ln.qty || 0), 0), unit: first.unit,
- assignee: f.handler, lines
- });
- list.forEach((p) => { p.taskNo = no; });
- closePlanTaskDlg();
- toast(`已下发任务单 ${no},含 ${lines.length} 行明细`);
- selectedPlanIds.clear();
- renderPlans();
- showMod("task");
- openTaskSheetDlg(taskSeq);
- }
- function taskSheetBox(on) {
- return `<span class="ts-box ${on ? "on" : ""}"></span>`;
- }
- function closeTaskSheetDlg() {
- document.getElementById("taskSheetDialog").close();
- }
- function openTaskSheetDlg(id) {
- const t = TASKS.find((x) => x.id === id);
- if (!t) { toast("未找到任务单"); return; }
- const lines = taskLinesOf(t);
- const pad = lines.concat(Array.from({ length: Math.max(0, 5 - lines.length) }, () => ({})));
- const b = t.bucket || "";
- const blank = (v) => v || "";
- document.querySelector("#taskSheetDialog h3").textContent = `山峡${t.kind}任务单`;
- document.getElementById("taskSheetBody").innerHTML = `<div class="task-sheet">
- <h3>山峡${t.kind}任务单</h3>
- <div class="ts-no">NO: ${t.no}</div>
- <table>
- <colgroup>
- <col style="width:9%"><col style="width:16%"><col style="width:14%"><col style="width:16%">
- <col style="width:12%"><col style="width:12%"><col style="width:21%">
- </colgroup>
- <tr>
- <td colspan="3">下单日期:${blank(t.orderDate || t.date)}</td>
- <td colspan="4">交货日期:${blank(t.deliverDate)}</td>
- </tr>
- <tr>
- <td colspan="3">下单人:${blank(t.orderBy)}</td>
- <td colspan="4">经手人:${blank(t.handler || t.assignee)}</td>
- </tr>
- <tr>
- <th>序号</th><th>产品名称</th><th>产品型号</th><th>包装规格</th><th>单位</th><th>数量</th><th>是否完成</th>
- </tr>
- ${pad.map((ln, i) => `<tr>
- <td class="idx">${i + 1}</td>
- <td class="center">${blank(ln.product)}</td>
- <td class="center">${blank(ln.model || ln.spec)}</td>
- <td class="center">${blank(ln.pack)}</td>
- <td class="center">${blank(ln.unit)}</td>
- <td class="center">${ln.qty != null && ln.qty !== "" ? ln.qty : ""}</td>
- <td class="center">${ln.product ? (ln.done ? "☑完成" : "□完成") : ""}</td>
- </tr>`).join("")}
- <tr>
- <td class="center">备注</td>
- <td colspan="6">${blank(t.remark)}
- <span class="ts-checks">
- ${taskSheetBox(/全新/.test(b))}全新桶
- ${taskSheetBox(/回收/.test(b))}是否回收桶
- ${taskSheetBox(/循环/.test(b))}桶循环
- </span>
- </td>
- </tr>
- <tr>
- <td colspan="7" class="ts-foot">制单人:${blank(t.maker)} 审核:${blank(t.auditor) || "________"} 生产主管:${blank(t.supervisor) || "________"}</td>
- </tr>
- </table>
- </div>`;
- document.getElementById("taskSheetDialog").showModal();
- }
- function nowLocal() {
- const d = new Date();
- const p = (n) => String(n).padStart(2, "0");
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;
- }
- function selectedTasks() {
- return TASKS.filter((t) => selectedTaskIds.has(t.id));
- }
- function toggleTaskSel(id, on) {
- if (on) selectedTaskIds.add(id);
- else selectedTaskIds.delete(id);
- }
- function toggleTaskSelAll(on) {
- document.querySelectorAll("#taskTable tbody input[type=checkbox]").forEach((el) => {
- el.checked = on;
- toggleTaskSel(+el.dataset.id, on);
- });
- }
- function renderTasks() {
- const q = (document.getElementById("taskQ")?.value || "").trim();
- const st = document.getElementById("taskSt")?.value || "";
- const rows = TASKS.filter((t) => {
- if (taskTab === "待执行" && t.status !== "待执行") return false;
- if (taskTab === "已完成" && t.status !== "已完成") return false;
- if (["稀释", "精制", "分装"].includes(taskTab) && t.kind !== taskTab) return false;
- if (st && t.status !== st) return false;
- 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(" ");
- if (q && !blob.includes(q)) return false;
- return true;
- });
- const kindN = (status) => ["稀释", "精制", "分装"].map((kind) =>
- TASKS.filter((t) => t.kind === kind && (!status || t.status === status)).length
- );
- const slashTriple = (parts) => parts.map((p) => `<span>${p}</span>`).join("<i>/</i>");
- const kindsTitle = `<span class="kinds">${slashTriple(["稀释", "精制", "分装"])}</span>`;
- document.getElementById("taskStats").innerHTML = [
- ["待执行", slashTriple(kindN("待执行")), "wait"],
- ["执行中", slashTriple(kindN("执行中")), "run"],
- ["已完成", slashTriple(kindN("已完成")), "done"],
- ["全部", slashTriple(kindN("")), "all"]
- ].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("");
- document.getElementById("taskHint").innerHTML = `当前 <b>${rows.length}</b> 条`;
- document.getElementById("taskCount").textContent = `共 ${rows.length} 条`;
- const allOn = rows.length && rows.every((t) => selectedTaskIds.has(t.id));
- document.getElementById("taskTable").innerHTML = `<table class="erp-table task-ops"><thead><tr>
- <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="toggleTaskSelAll(this.checked)" /></th>
- <th>任务单号</th><th>类型</th><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><tbody>${rows.length ? rows.map((t) => {
- const lines = taskLinesOf(t);
- const first = lines[0] || {};
- const more = lines.length > 1 ? ` 等${lines.length}行` : "";
- return `<tr class="${selectedTaskIds.has(t.id) ? "sel" : ""}">
- <td class="chk"><input type="checkbox" data-id="${t.id}" ${selectedTaskIds.has(t.id) ? "checked" : ""} onchange="toggleTaskSel(${t.id}, this.checked)" /></td>
- <td class="cell-no"><button class="op-link" type="button" onclick="openTaskSheetDlg(${t.id})">${t.no}</button></td>
- <td>${t.kind}任务单</td>
- <td>${taskStHtml(t.status)}</td>
- <td>${t.orderDate || t.date || "—"}</td>
- <td>${t.deliverDate || "—"}</td>
- <td>${t.orderBy || "—"}</td>
- <td>${t.handler || t.assignee || "—"}</td>
- <td>${(first.product || t.product || "—") + more}</td>
- <td>${first.model || t.spec || "—"}</td>
- <td>${first.pack || t.pack || "—"}</td>
- <td>${first.unit || t.unit || "—"}</td>
- <td class="num">${first.qty != null ? first.qty : t.qty}</td>
- <td class="clip">${[t.remark, t.bucket].filter(Boolean).join(" · ") || "—"}</td>
- <td>${t.maker || "—"}</td>
- </tr>`;
- }).join("") : `<tr><td colspan="15" class="muted">暂无任务单。请在运输计划勾选后点「下发任务单」。</td></tr>`}</tbody></table>`;
- updateBadges();
- }
- function taskOps(t) {
- const sheet = `<button class="op-link" type="button" onclick="openTaskSheetDlg(${t.id})">查看单据</button>`;
- if (t.status === "待执行") {
- return `${sheet}<button class="op-link" type="button" onclick="receiveTask(${t.id})">接收任务</button><button class="op-link" type="button" onclick="openTaskReportDlg(${t.id})">回传完成</button>`;
- }
- if (t.status === "执行中") {
- return `${sheet}<button class="op-link" type="button" onclick="openTaskReportDlg(${t.id})">回传完成</button>`;
- }
- return `${sheet}<button class="op-link" type="button" onclick="openTaskReportDlg(${t.id})">查看回传</button>`;
- }
- function receiveTask(id) {
- const t = TASKS.find((x) => x.id === id);
- if (!t || t.status !== "待执行") { toast("仅待执行任务可接收"); return; }
- t.status = "执行中";
- t.receivedAt = nowLocal();
- toast(`${t.no} 现场已接收,可回传完成`);
- renderTasks();
- }
- function batchReceiveTask() {
- const list = selectedTasks().filter((t) => t.status === "待执行");
- if (!list.length) { toast("请勾选待执行的任务单后再点「接收任务」"); return; }
- list.forEach((t) => {
- t.status = "执行中";
- t.receivedAt = nowLocal();
- });
- toast(`已接收 ${list.length} 张任务单`);
- selectedTaskIds.clear();
- renderTasks();
- }
- function batchReportTask() {
- const list = selectedTasks().filter((t) => t.status !== "已完成");
- if (!list.length) { toast("请勾选未完成的任务单后再点「回传完成」"); return; }
- openTaskReportDlg(list[0].id);
- }
- function renderTaskSheetPhotoList(photos, clickable) {
- const thumbs = document.getElementById("taskSheetPhotoThumbs");
- const chips = document.getElementById("taskSheetPhotoChips");
- const list = photos || [];
- thumbs.innerHTML = list.map((a, i) => {
- if (a.url && (a.type || "").startsWith("image/")) {
- const btn = clickable
- ? `<button type="button" onclick="openAttach('task',${reportingTaskId},${i})"><img alt="${a.name}" src="${a.url}" /></button>`
- : `<img alt="${a.name}" src="${a.url}" />`;
- return btn;
- }
- return "";
- }).join("");
- chips.innerHTML = list.map((a, i) =>
- clickable && a.url
- ? `<button class="chip" type="button" onclick="openAttach('task',${reportingTaskId},${i})">${a.name}</button>`
- : `<span class="chip ghost">${a.name}</span>`
- ).join("");
- }
- function previewTaskSheetPhotos(input) {
- const files = [...input.files];
- renderTaskSheetPhotoList(files.map((f) => ({
- name: f.name,
- type: f.type,
- url: f.type.startsWith("image/") ? URL.createObjectURL(f) : ""
- })), false);
- }
- function openTaskReportDlg(id) {
- const t = TASKS.find((x) => x.id === id);
- if (!t) return;
- reportingTaskId = id;
- const form = document.getElementById("taskReportForm");
- const view = t.status === "已完成";
- document.getElementById("taskReportTitle").textContent = view ? "查看回传" : "回传完成任务";
- document.getElementById("taskReportHint").textContent = view
- ? `${t.no} · ${t.assignee} 已回传完成。`
- : `${t.no} · ${t.kind}任务单 · ${t.product} ${t.qty}${t.unit}。现场完成后填写回传说明,并拍照上传纸质分装单。`;
- form.no.value = t.no;
- form.assignee.value = t.assignee;
- form.doneTime.value = t.doneTime || nowLocal();
- form.doneQty.value = t.doneQty != null ? t.doneQty : t.qty;
- form.report.value = t.report || "";
- ["doneTime", "doneQty", "report"].forEach((n) => { form[n].readOnly = view; form[n].disabled = view; });
- const photoInput = form.sheetPhotos;
- photoInput.value = "";
- photoInput.required = !view;
- document.getElementById("taskSheetPhotoBtn").classList.toggle("hidden", view);
- document.getElementById("taskSheetPhotoHint").textContent = view
- ? "已上传的纸质分装单照片,点击可查看。"
- : "须拍摄已签字完成的纸质分装任务单,可多张。";
- renderTaskSheetPhotoList(t.sheetPhotos || [], view);
- document.getElementById("taskReportSave").classList.toggle("hidden", view);
- document.getElementById("taskReportDialog").showModal();
- }
- function closeTaskReportDlg() {
- document.getElementById("taskReportDialog").close();
- reportingTaskId = null;
- }
- function saveTaskReport() {
- const t = TASKS.find((x) => x.id === reportingTaskId);
- if (!t) { toast("未找到任务单"); return; }
- const form = document.getElementById("taskReportForm");
- const photoInput = form.sheetPhotos;
- if (!photoInput.files.length) {
- toast("请拍照上传已完成的纸质分装单");
- photoInput.focus();
- return;
- }
- if (!form.reportValidity()) return;
- const f = Object.fromEntries(new FormData(form));
- readFiles(photoInput.files).then((photos) => {
- if (t.status === "待执行") {
- t.receivedAt = t.receivedAt || nowLocal();
- }
- t.status = "已完成";
- t.doneTime = f.doneTime;
- t.doneQty = +f.doneQty;
- t.report = f.report;
- t.sheetPhotos = photos;
- if (t.lines && t.lines.length) t.lines.forEach((ln) => { ln.done = true; });
- closeTaskReportDlg();
- toast(`${t.no} 已回传完成`);
- selectedTaskIds.delete(t.id);
- renderTasks();
- });
- }
- function voidPlan(id) {
- const p = plans.find((x) => x.id === id);
- if (!p || p.status === "已完成") return;
- p.status = "已作废";
- toast(`${p.no} 已作废`);
- renderPlans();
- }
- function copyPlan() {
- const rows = selectedPlans();
- if (rows.length !== 1) { toast("请勾选一条计划后再复制"); return; }
- const src = rows[0];
- const no = "PO" + today.replace(/-/g, "") + String(++planSeq).padStart(3, "0");
- plans.unshift({
- ...src, id: planSeq, no, status: "草稿", realQty: null, tractor: "", driver: "", escort: "",
- certId: null, transId: null, warehouseId: null, archived: false, waitNotify: false,
- source: "复制", sourceNo: src.no, taskNo: "", contractNo: src.contractNo || ""
- });
- selectedPlanIds.clear();
- toast("已复制为草稿 " + no);
- renderPlans();
- }
- function arrangeSelectedPlans() {
- const list = selectedPlans().filter((p) => p.status === "待安排" || p.status === "草稿");
- if (!list.length) { toast("请勾选待安排或草稿计划"); return; }
- list.forEach((p) => arrangePlan(p.id));
- }
- function waitCustomerNotify() {
- const list = selectedPlans().filter((p) => p.status === "待安排" || p.status === "草稿");
- if (!list.length) { toast("请勾选待安排计划"); return; }
- list.forEach((p) => { p.waitNotify = true; p.status = "待安排"; });
- toast(`已标记 ${list.length} 条为等客户通知`);
- renderPlans();
- }
- function printPlanDocs() {
- const list = selectedPlans();
- if (!list.length) { toast("请先勾选计划"); return; }
- toast("演示:已打印 " + list.map((p) => p.no).join("、"));
- }
- function voidSelectedPlans() {
- const list = selectedPlans().filter((p) => p.status !== "已完成" && p.status !== "已作废");
- if (!list.length) { toast("请勾选可作废的计划"); return; }
- list.forEach((p) => { p.status = "已作废"; p.waitNotify = false; });
- selectedPlanIds.clear();
- toast("已作废所选计划");
- renderPlans();
- }
- function createTriParty() {
- toast("演示:三方人车快速创建");
- }
- function renderPlanKpis() {
- const box = document.getElementById("planKpis");
- if (!box) return;
- const todayAllN = plans.filter(isTodayPlan).length;
- const todayDoneN = plans.filter(isTodayDonePlan).length;
- const todayOpenN = plans.filter(isTodayOpenPlan).length;
- const todayOn = ["todayAll", "todayDone", "todayOpen"].includes(planCardFilter);
- const bit = (key, cls, label, n) =>
- `<button class="${cls}${planCardFilter === key ? " on" : ""}" type="button" onclick="setPlanCardFilter('${key}')">${label ? `${label} ` : ""}<b>${n}</b></button>`;
- box.innerHTML = [
- `<div class="stat tone-wait ${planCardFilter === "tomorrowWait" ? "on" : ""}">
- <div class="k">明日待安排计划</div>
- <button class="v" type="button" onclick="setPlanCardFilter('tomorrowWait')">${plans.filter(isTomorrowWaitPlan).length}</button>
- </div>`,
- `<div class="stat tone-today ${todayOn ? "on" : ""}">
- <div class="k">今日计划</div>
- <div class="today-bits">
- ${bit("todayAll", "", "共", todayAllN)}
- ${bit("todayDone", "", "已完成", todayDoneN)}
- ${bit("todayOpen", "open", "未完成", todayOpenN)}
- </div>
- </div>`,
- `<div class="stat tone-ctrl ${planCardFilter === "precursorReview" ? "on" : ""}">
- <div class="k">待审核易制毒计划</div>
- <button class="v" type="button" onclick="setPlanCardFilter('precursorReview')">${plans.filter(isWaitReviewPrecursorPlan).length}</button>
- </div>`,
- `<div class="stat tone-arch ${planCardFilter === "waitArchive" ? "on" : ""}">
- <div class="k">待归档计划</div>
- <button class="v" type="button" onclick="setPlanCardFilter('waitArchive')">${plans.filter(isWaitArchivePlan).length}</button>
- </div>`
- ].join("");
- }
- function renderPlans() {
- const q = (document.getElementById("planQ")?.value || "").trim();
- const alerts = document.getElementById("planAlerts");
- if (alerts) alerts.innerHTML = "";
- const rows = plans.filter((p) => {
- if (!planTabMatch(p, planTab)) return false;
- const d = p.date || "";
- if (planDateTab === "昨日" && d !== dayShift(today, -1)) return false;
- if (planDateTab === "今日" && d !== today) return false;
- if (planDateTab === "明日" && d !== dayShift(today, 1)) return false;
- if (planDateTab === "本周") {
- const [a, b] = quoteWeekRange();
- if (d < a || d > b) return false;
- }
- if (planCardFilter && !planCardMatch(p, planCardFilter)) return false;
- 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(" ");
- if (q && !blob.includes(q)) return false;
- return true;
- });
- const size = parseInt(document.getElementById("planPageSize").value, 10) || 20;
- const pageRows = rows.slice(0, size);
- renderPlanKpis();
- document.getElementById("planCount").textContent = "状态全选";
- const allOn = pageRows.length && pageRows.every((p) => selectedPlanIds.has(p.id));
- document.getElementById("planTable").innerHTML = `<table class="erp-table plan-table"><thead><tr>
- <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="togglePlanSelAll(this.checked)" /></th>
- <th class="idx">序号</th>
- <th class="col-no">计划编号</th>
- <th>跟进状态</th>
- <th>计划日期</th>
- <th>送货时间</th>
- <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><tbody>${pageRows.map((p, i) => {
- const out = p.status === "已作废" ? "—" : (p.realQty == null ? 0 : p.realQty);
- return `<tr class="${p.ctrl && !p.certId && p.status === "待安排" ? "row-wait" : ""}${selectedPlanIds.has(p.id) ? " sel" : ""}">
- <td class="chk"><input type="checkbox" data-id="${p.id}" ${selectedPlanIds.has(p.id) ? "checked" : ""} onchange="togglePlanSel(${p.id}, this.checked)" /></td>
- <td class="idx">${i + 1}</td>
- <td class="cell-no col-no">${p.no}</td>
- <td>${planStHtml(p)}</td>
- <td>${p.date}</td>
- <td>${planDeliverAt(p)}</td>
- <td>${planTypeName(p)}</td>
- <td>${p.tractor || "—"}</td>
- <td>${planCrew(p)}</td>
- <td class="clip">${p.loadFrom || "—"}</td>
- <td class="clip">${p.customer || p.settler || "—"}</td>
- <td>${p.product || "—"}</td>
- <td>${p.spec || "—"}</td>
- <td>${p.pack || "—"}</td>
- <td class="num qty">${p.planQty}${p.unit ? " " + p.unit : ""}</td>
- <td class="num out-qty">${out}${out !== "—" && p.unit ? " " + p.unit : ""}</td>
- <td class="clip">${planContractNo(p)}</td>
- <td>${p.taskNo ? `<button class="op-link" type="button" onclick="openPlanTaskDlg(${p.id})">${p.taskNo}</button>` : "—"}</td>
- </tr>`;
- }).join("")}</tbody></table>`;
- updateBadges();
- }
- function openPlanDlg() {
- const form = document.getElementById("planForm");
- form.reset();
- form.querySelector("[name=unit]").value = "吨";
- form.querySelector("[name=date]").value = today;
- document.getElementById("planDialog").showModal();
- }
- function closePlanDlg() { document.getElementById("planDialog").close(); }
- function savePlan() {
- const form = document.getElementById("planForm");
- if (!form.reportValidity()) return;
- const f = Object.fromEntries(new FormData(form));
- plans.unshift({
- ...f, id: ++planSeq, no: "PO" + today.replace(/-/g, "") + String(planSeq).padStart(3, "0"),
- status: "待安排", planQty: +f.planQty, realQty: null, tractor: "", driver: "", certId: null, warehouseId: null,
- deliverDate: f.deliverDate || f.date,
- salesman: f.salesman || "调度员",
- dept: f.dept || "运输调度组",
- shipMode: f.shipMode || "槽车运输",
- spec: f.spec || "",
- pack: f.pack || "槽罐车",
- settler: f.settler || f.customer || "",
- docKind: f.type === "直销单" ? "开单类" : "选单类",
- source: "手工录入",
- sourceNo: "PO" + today.replace(/-/g, "") + String(planSeq).padStart(3, "0"),
- deliverTime: "",
- escort: "",
- contractNo: f.contractNo || ""
- });
- closePlanDlg();
- toast("计划已保存为待安排");
- renderPlans();
- }
- function arrangePlan(id) {
- const p = plans.find((x) => x.id === id);
- if (!p) return;
- if (p.status === "已作废" || p.status === "已完成") {
- toast("该计划不能安排");
- return;
- }
- if (p.ctrl && !p.certId) {
- toast("请先在购买证管理中人工勾选关联计划");
- const c = suggestCertForPlan(p);
- showMod("cert");
- if (c) openCertLinkPlanDlg(c.id, [p.id]);
- else openCertDlg(false);
- return;
- }
- const cert = certs.find((c) => c.id === p.certId);
- if (cert) {
- const d = pickVehicleForPlan(cert, p.planQty, p.transId, p.tractor);
- if (d) {
- p.tractor = d.plate;
- p.driver = d.driver || p.driver;
- }
- }
- p.status = "已安排";
- toast(p.tractor ? `已安排 ${p.tractor},分派运量来自同一运输公司运输证` : "已安排发运");
- renderPlans();
- }
- function refreshCertViews() {
- if (currentMod !== "cert") return;
- if (document.getElementById("view-cert-detail")?.classList.contains("hidden")) renderCertList();
- else renderCertDetail();
- updateBadges();
- }
- function completePlan(id) {
- const p = plans.find((x) => x.id === id);
- if (!p) return;
- if (p.status !== "已安排") {
- toast("仅已安排的计划可标记完成");
- return;
- }
- if (p.ctrl && !p.certId) {
- toast("易制毒 / 易制爆计划请先关联购买证和运输证");
- openLinkCertDlg(id);
- return;
- }
- p.status = "已完成";
- if (p.realQty == null) p.realQty = p.planQty;
- p.archived = false;
- p.warehouseId = isDirectSale(p) ? null : suggestWarehouse(p.product, p.spec);
- let msg = `${p.no} 已完成,请到运输归档人工审核`;
- if (p.certId) {
- const s = settleCertOnComplete(p);
- if (s) {
- msg = `${p.no} 已完成,扣减 ${fmtTon(s.n)}${p.unit};购买证余 ${fmtTon(s.buyLeft)}${s.cert.unit},运输证余 ${fmtTon(s.transLeft)}${s.cert.unit}`;
- 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))) {
- msg += "(余量已达自设预警量)";
- }
- }
- }
- toast(msg);
- renderPlans();
- refreshCertViews();
- }
- function openArchiveDlg(id) {
- const p = plans.find((x) => x.id === id);
- if (!p) return;
- if (p.status === "已作废") {
- toast("已作废计划不能归档");
- return;
- }
- if (p.status !== "已完成") {
- toast("请先在运输计划中将单据标记为已完成");
- return;
- }
- if (p.archived) {
- toast("该单据已归档");
- return;
- }
- if (p.ctrl && !p.certId) {
- toast("请先关联购买证 / 运输证");
- openLinkCertDlg(id);
- return;
- }
- const qty = Number(p.realQty || p.planQty);
- const cert = certs.find((c) => c.id === p.certId);
- if (cert && remain(cert, p.id) + 1e-9 < qty) {
- toast(`购买证余量不足:剩余 ${remain(cert, p.id)} ${cert.unit},本计划 ${qty} ${p.unit}`);
- return;
- }
- if (cert && transRemain(cert, p.id) + 1e-9 < qty) {
- toast(`运输证余量不足:剩余 ${transRemain(cert, p.id)} ${cert.unit},本计划 ${qty} ${p.unit}`);
- return;
- }
- archivingPlanId = id;
- const form = document.getElementById("archiveForm");
- const wh = warehouseForPlan(p);
- const whRow = document.getElementById("archiveWhRow");
- if (isDirectSale(p)) {
- document.getElementById("archiveHint").textContent = `${p.no} 为直销单,无中转仓库,审核通过后仅归档,不记入库存。`;
- document.getElementById("archiveWhHint").textContent = "直销单不经过硫酸库 / 液碱库 / 分装库 / 固体库。";
- whRow.style.display = "grid";
- document.getElementById("archiveWhName").value = "无中转仓库";
- form.warehouseId.value = "";
- } else {
- const action = isInboundPlan(p) ? "入库计入" : "出库扣减";
- document.getElementById("archiveHint").textContent =
- `${p.no} 为${planTypeName(p)},按品名「${p.product}」自动对应仓库,审核通过后${action}库存。`;
- document.getElementById("archiveWhHint").textContent = isInboundPlan(p)
- ? "入库单:按品名进入对应仓库(进货入库)。"
- : "出库单:按品名从对应仓库发出(销货出库)。";
- whRow.style.display = "grid";
- document.getElementById("archiveWhName").value = wh ? whNameOf(wh) : "未匹配到仓库,请核对品名";
- form.warehouseId.value = wh || "";
- }
- document.getElementById("archiveNo").value = p.no;
- document.getElementById("archiveProd").value = `${planTypeName(p)} · ${p.product}${p.spec ? " / " + p.spec : ""}`;
- document.getElementById("archiveUnit").value = p.unit;
- form.realQty.value = qty;
- document.getElementById("archiveDialog").showModal();
- }
- function closeArchiveDlg() {
- document.getElementById("archiveDialog").close();
- archivingPlanId = null;
- }
- function recordWhFromArchive(p) {
- if (isDirectSale(p) || !p.warehouseId) return;
- const qty = Number(p.realQty || p.planQty);
- const inbound = isInboundPlan(p);
- const stock = matchStock(p.warehouseId, p.product);
- const price = stock ? stock.cost : 0;
- if (inbound) {
- if (stock) stock.qty += qty;
- WH_LEDGER.unshift({
- date: today, doc: "进货单", no: p.no, wh: p.warehouseId, product: p.product,
- inQty: qty, inCost: qty * price, outQty: 0, outCost: 0,
- balQty: stock ? stock.qty : qty, price, balCost: stock ? stock.qty * price : qty * price,
- note: "运输归档审核入库"
- });
- } else {
- if (stock) stock.qty = Math.max(0, +(stock.qty - qty).toFixed(3));
- WH_LEDGER.unshift({
- date: today, doc: "销货单", no: p.no, wh: p.warehouseId, product: p.product,
- inQty: 0, inCost: 0, outQty: qty, outCost: qty * price,
- balQty: stock ? stock.qty : 0, price, balCost: stock ? stock.qty * price : 0,
- note: "运输归档审核出库"
- });
- }
- }
- function applyArchive(p, qty) {
- if (!p || p.archived) return "skip";
- if (p.status !== "已完成") return "未完成";
- if (p.ctrl && !p.certId) return "待关联证件";
- const n = Number(qty != null ? qty : (p.realQty || p.planQty));
- const cert = certs.find((c) => c.id === p.certId);
- if (cert && remain(cert, p.id) + 1e-9 < n) return `购买证余量不足(剩 ${remain(cert, p.id)})`;
- if (cert && transRemain(cert, p.id) + 1e-9 < n) return `运输证余量不足(剩 ${transRemain(cert, p.id)})`;
- p.realQty = n;
- p.status = "已完成";
- if (isDirectSale(p)) {
- p.warehouseId = null;
- p.archived = true;
- p.archiveDate = today;
- if (cert) settleCertOnComplete(p);
- return "直销";
- }
- const wh = suggestWarehouse(p.product, p.spec) || p.warehouseId;
- if (!wh) return "未匹配仓库";
- p.warehouseId = wh;
- p.archived = true;
- p.archiveDate = today;
- recordWhFromArchive(p);
- if (cert) settleCertOnComplete(p);
- return isInboundPlan(p) ? "入库" : "出库";
- }
- function confirmArchive() {
- const p = plans.find((x) => x.id === archivingPlanId);
- if (!p) return;
- const form = document.getElementById("archiveForm");
- if (!form.reportValidity()) return;
- const qty = Number(form.realQty.value);
- const result = applyArchive(p, qty);
- if (result === "未匹配仓库") {
- toast("未能按品名匹配仓库,请核对品名后重试");
- return;
- }
- if (result === "待关联证件") {
- toast("请先关联购买证 / 运输证");
- closeArchiveDlg();
- openLinkCertDlg(p.id);
- return;
- }
- if (result !== "入库" && result !== "出库" && result !== "直销" && result !== "skip") {
- toast(result);
- return;
- }
- if (result === "直销") toast(`${p.no} 已审核归档,直销单无中转仓库`);
- else if (result === "skip") toast("该单据已归档");
- else toast(`${p.no} 已审核归档,${result} ${whNameOf(p.warehouseId)}`);
- closeArchiveDlg();
- renderPlans();
- showMod("archive");
- }
- function archiveOps(p) {
- if (p.archived) return `<button class="op-link" type="button" onclick="showMod('warehouse')">看仓储</button>`;
- return `<button class="op-link" type="button" onclick="openArchiveDlg(${p.id})">审核归档</button>`;
- }
- function toggleArchiveSel(id, on) {
- if (on) selectedArchiveIds.add(id);
- else selectedArchiveIds.delete(id);
- }
- function toggleArchiveSelAll(on) {
- document.querySelectorAll("#archiveTable tbody input[type=checkbox]").forEach((el) => {
- el.checked = on;
- toggleArchiveSel(+el.dataset.id, on);
- });
- }
- function batchReviewArchive() {
- const pending = plans.filter((p) => p.status === "已完成" && !p.archived);
- const picked = pending.filter((p) => selectedArchiveIds.has(p.id));
- const list = picked.length ? picked : pending.filter((p) => archiveTab === "待审核" || archiveTab === "全部");
- if (!list.length) { toast("没有待审核的已完成单据"); return; }
- if (list.length === 1) {
- openArchiveDlg(list[0].id);
- return;
- }
- let ok = 0;
- const fail = [];
- list.forEach((p) => {
- const r = applyArchive(p, p.realQty || p.planQty);
- if (r === "入库" || r === "出库" || r === "直销") ok += 1;
- else if (r !== "skip") fail.push(`${p.no} ${r}`);
- });
- selectedArchiveIds.clear();
- toast(fail.length ? `已归档 ${ok} 条;${fail[0]}` : `已审核归档 ${ok} 条`);
- renderPlans();
- renderArchive();
- }
- function renderArchive() {
- const sel = document.getElementById("archiveWh");
- const keep = sel.value;
- sel.innerHTML = `<option value="">全部仓库</option><option value="none">无中转仓库</option>` + warehouseSelectHtml(keep);
- if (keep && ![...sel.options].some((o) => o.value === keep)) sel.value = "";
- else sel.value = keep;
- const q = (document.getElementById("archiveQ").value || "").trim();
- const type = document.getElementById("archiveType").value;
- const wh = sel.value;
- const done = plans.filter((p) => p.status === "已完成");
- const rows = done.filter((p) => {
- if (archiveTab === "待审核" && p.archived) return false;
- if (archiveTab === "已归档" && !p.archived) return false;
- if (type && p.type !== type) return false;
- if (wh === "none") { if (!isDirectSale(p)) return false; }
- else if (wh && warehouseForPlan(p) !== wh) return false;
- const blob = [p.no, p.product, p.customer, p.settler, p.tractor, p.driver, warehouseForPlan(p) ? whNameOf(warehouseForPlan(p)) : "无中转"].join(" ");
- if (q && !blob.includes(q)) return false;
- return true;
- });
- const ym = today.slice(0, 7);
- const yr = today.slice(0, 4);
- const inMonth = (d) => (d || "").slice(0, 7) === ym;
- const inYear = (d) => (d || "").slice(0, 4) === yr;
- const waitN = done.filter((p) => !p.archived && inMonth(p.date || p.deliverDate)).length;
- const monthArchN = done.filter((p) => p.archived && inMonth(p.archiveDate || p.date)).length;
- const unArchN = done.filter((p) => !p.archived).length;
- const yearArchN = done.filter((p) => p.archived && inYear(p.archiveDate || p.date)).length;
- document.getElementById("archiveStats").innerHTML = [
- ["当月待审核", waitN, "待人工审核"],
- ["当月已归档", monthArchN, "审核通过"],
- ["未归档", unArchN, "已完成尚未归档"],
- ["本年度归档数", yearArchN, "本年审核通过"]
- ].map(([k, v, s]) => `<div class="stat"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");
- const hint = document.getElementById("archiveHintBar");
- if (hint) hint.innerHTML = `当前 <b>${archiveTab}</b> 共 <b>${rows.length}</b> 条`;
- document.getElementById("archiveCount").textContent = archiveTab === "待审核" ? `待审核 ${rows.length} 条` : `共 ${rows.length} 条`;
- const vis = rows.filter((p) => !p.archived);
- const allOn = vis.length && vis.every((p) => selectedArchiveIds.has(p.id));
- document.getElementById("archiveTable").innerHTML = rows.length ? `<table class="erp-table" style="min-width:1480px"><thead><tr>
- <th class="chk"><input type="checkbox" ${allOn ? "checked" : ""} onchange="toggleArchiveSelAll(this.checked)" /></th>
- <th class="idx">序号</th>
- <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><tbody>${rows.map((p, i) => `<tr>
- <td class="chk">${p.archived ? "" : `<input type="checkbox" data-id="${p.id}" ${selectedArchiveIds.has(p.id) ? "checked" : ""} onchange="toggleArchiveSel(${p.id}, this.checked)" />`}</td>
- <td class="idx">${i + 1}</td>
- <td class="cell-no">${p.no}</td>
- <td>${p.archived ? `<span class="st-pill arch">已归档</span>` : `<span class="st-pill wait">待审核</span>`}</td>
- <td>${planTypeName(p)}</td>
- <td>${productCell(p)}</td>
- <td>${p.spec || "—"}</td>
- <td class="num qty">${p.realQty != null ? p.realQty : p.planQty} ${p.unit}</td>
- <td>${warehouseCell(p)}</td>
- <td>${stockActCell(p)}</td>
- <td>${p.settler || p.customer || "—"}</td>
- <td>${p.tractor || "—"}</td>
- <td>${p.driver || "—"}</td>
- <td>${archiveOps(p)}</td>
- </tr>`).join("")}</tbody></table>` : `<div class="empty">${archiveTab === "待审核" ? "暂无待审核单据。请在运输计划中将已安排单据点「完成」。" : "没有符合条件的归档记录。"}</div>`;
- updateBadges();
- }
- function openLinkCertDlg(planId) {
- linkingPlanId = planId;
- const p = plans.find((x) => x.id === planId);
- if (!p) return;
- document.getElementById("linkCertHint").textContent =
- `${p.no} · ${p.product} ${p.planQty}${p.unit} · ${p.ctrl || "普通"}。请选择购买证下的一张运输证;关联后按该证车辆计划量预警。`;
- const pairs = certs.flatMap((c) => transOf(c).map((t) => ({ c, t })));
- document.getElementById("linkCertTable").innerHTML = pairs.length ? `<table><thead><tr>
- <th>购买证</th><th>运输证 / 公司</th><th>品名</th><th>购买证余量</th><th>运输证余量</th><th>分派车辆</th><th></th>
- </tr></thead><tbody>${pairs.map(({ c, t }) => {
- const mismatch = c.product && p.product && !String(p.product).includes(c.product) && !String(c.product).includes(String(p.product).replace(/\d+%/g, "").trim());
- const short = remainGap(c, p.id) < p.planQty || transItemGap(t, p.id) < p.planQty;
- return `<tr>
- <td class="cell-no">${c.buyNo}</td>
- <td>${t.transNo}<div class="muted">${t.carrier || "—"}</div></td>
- <td>${c.product}${mismatch ? `<div class="muted">与计划品名不同</div>` : ""}</td>
- <td>${remainHtml(c)}</td>
- <td>${transItemRemain(t)} ${c.unit}${transItemWarn(t) ? `<div class="muted">≤${fmtTon(transWarnQty(t))}${c.unit}预警</div>` : ""}</td>
- <td>${(t.dispatches || []).map((d) => d.plate).join("、") || "未分派"}</td>
- <td><button class="btn btn-sm btn-primary" type="button" onclick="linkCert(${c.id}, ${t.id})">关联此证</button>${short ? `<div class="muted">余量低于本计划</div>` : ""}</td>
- </tr>`;
- }).join("")}</tbody></table>` : `<div class="empty">没有可用证件,请先上传购买证及运输证。</div>`;
- document.getElementById("linkCertDialog").showModal();
- }
- function closeLinkCertDlg() { document.getElementById("linkCertDialog").close(); linkingPlanId = null; }
- function attachPlanToCert(p, c, t, allowOver, plate) {
- const qty = occupyQty(p) || Number(p.planQty || 0);
- if (!allowOver && remainGap(c, p.id) + 1e-9 < qty) return `购买证余量 ${remain(c, p.id)}${c.unit} 不足`;
- if (!allowOver && transItemGap(t, p.id) + 1e-9 < qty) return `运输证 ${t.transNo} 余量 ${transItemRemain(t, p.id)}${c.unit} 不足`;
- p.certId = c.id;
- p.transId = t.id;
- const want = plate || p.tractor || "";
- const d = want
- ? ((t.dispatches || []).find((x) => x.plate === want) || { plate: want, driver: p.driver })
- : pickVehicleForTrans(t, qty, p.tractor);
- if (d && d.plate) {
- p.tractor = d.plate;
- p.driver = d.driver || p.driver;
- }
- const calc = calcVehPlanQty(c, t, p.tractor, p);
- p.vehPlanQty = calc.qty;
- p.linkBound = calc.room;
- return "";
- }
- function linkCert(certId, transId) {
- const p = plans.find((x) => x.id === linkingPlanId);
- const c = certs.find((x) => x.id === certId);
- const t = c ? findTrans(c, transId) : null;
- if (!p || !c || !t) return;
- const err = attachPlanToCert(p, c, t, true);
- if (err) { toast(err); return; }
- closeLinkCertDlg();
- const over = p.tractor && t.dispatches && vehicleOver({ plate: p.tractor, qty: ((t.dispatches || []).find((d) => d.plate === p.tractor) || {}).qty || 0 }, t);
- toast(`已关联 ${c.buyNo} / ${t.transNo}${p.tractor ? " · " + p.tractor : ""}${over ? "(该车计划量已超分派,已预警)" : ""}`);
- renderPlans();
- if (currentMod === "cert") renderCertDetail();
- }
- function unlinkCertPlan(planId) {
- const p = plans.find((x) => x.id === planId);
- if (!p) return;
- p.certId = null;
- p.transId = null;
- p.shipCertId = null;
- p.procureCertId = null;
- toast(`${p.no} 已取消关联`);
- if (currentMod === "cert") {
- if (document.getElementById("view-cert-detail")?.classList.contains("hidden")) renderCertList();
- else renderCertDetail();
- } else renderPlans();
- updateBadges();
- }
- function certAllVehOptions(c, plate) {
- const ds = c ? allDispatches(c) : [];
- const plates = [...new Set(ds.map((d) => d.plate).filter(Boolean))];
- if (plate && !plates.includes(plate)) plates.unshift(plate);
- const opts = [`<option value="">未指定车辆</option>`];
- plates.forEach((pl) => {
- const d = ds.find((x) => x.plate === pl);
- const cap = d ? Number(d.qty || 0) : 0;
- const tno = d && d.transNo ? d.transNo : "";
- opts.push(`<option value="${pl}" ${pl === plate ? "selected" : ""}>${pl}${cap ? " · 分派 " + cap + "吨" : ""}${tno ? " · " + tno : ""}</option>`);
- });
- return opts.join("");
- }
- function certLinkVehOptions(t, plate) {
- const fromPlates = t ? String(t.plates || "").split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean) : [];
- const plates = t ? [...new Set([...(t.dispatches || []).map((d) => d.plate), ...fromPlates, plate].filter(Boolean))] : (plate ? [plate] : []);
- const opts = [`<option value="">未指定车辆</option>`];
- plates.forEach((pl) => {
- const d = t && (t.dispatches || []).find((x) => x.plate === pl);
- const cap = d ? Number(d.qty || 0) : 0;
- opts.push(`<option value="${pl}" ${pl === plate ? "selected" : ""}>${pl}${cap ? " · 分派 " + cap + "吨" : ""}</option>`);
- });
- return opts.join("");
- }
- function openCertLinkPlanDlg(certId, focusPlanIds) {
- const c = certs.find((x) => x.id === certId);
- if (!c) return;
- linkingCertId = certId;
- currentCertId = certId;
- linkingCertFocusIds = (focusPlanIds || []).map(Number);
- const list = transOf(c);
- if (!list.length) {
- toast("请先上传运输证");
- return;
- }
- document.getElementById("certLinkPlanHint").textContent =
- `购买证 ${c.buyNo} · ${c.product} · 证载 ${c.buyQty}${c.unit}。勾选计划并指定运输证、车辆;余量 = 证载 − Σ关联车辆计划量,≤ 自设预警量时预警。`;
- const qEl = document.getElementById("certLinkPlanQ");
- if (qEl) qEl.value = "";
- const cands = plans.filter((p) =>
- p.status !== "已作废" &&
- (p.certId === c.id || !p.certId) &&
- (p.ctrl || /硫酸|硝酸|铵/.test(p.product || ""))
- ).sort((a, b) => {
- const fa = linkingCertFocusIds.includes(a.id) ? 1 : 0;
- const fb = linkingCertFocusIds.includes(b.id) ? 1 : 0;
- if (fa !== fb) return fb - fa;
- const ma = planMatchesCert(a, c) ? 1 : 0;
- const mb = planMatchesCert(b, c) ? 1 : 0;
- if (ma !== mb) return mb - ma;
- const la = a.certId === c.id ? 1 : 0;
- const lb = b.certId === c.id ? 1 : 0;
- return lb - la;
- });
- document.getElementById("certLinkPlanTable").innerHTML = cands.length ? `<table class="erp-table"><thead><tr>
- <th class="chk"></th><th>计划编号</th><th>状态</th><th>品名</th><th>客户</th><th>计划量</th><th>两证可运</th><th>占用运输证</th><th>关联车辆</th>
- </tr></thead><tbody>${cands.map((p) => {
- const on = p.certId === c.id || linkingCertFocusIds.includes(p.id);
- const match = planMatchesCert(p, c);
- const tid = (suggestTransForPlan(c, p) || list[0]).id;
- const t = findTrans(c, tid);
- const blob = [p.no, p.product, p.customer, p.settler, p.tractor, p.ctrl].join(" ");
- return `<tr data-blob="${blob}">
- <td class="chk"><input type="checkbox" data-plan="${p.id}" ${on ? "checked" : ""} onchange="previewCertLinkPlans()" /></td>
- <td class="cell-no">${p.no}${linkingCertFocusIds.includes(p.id) ? `<span class="match-rec">待关联</span>` : ""}</td>
- <td>${planStHtml(p)}</td>
- <td>${p.product}${match ? `<span class="match-rec">匹配</span>` : ""}<div class="muted">${p.ctrl || ""}</div></td>
- <td class="clip">${p.customer || p.settler || "—"}</td>
- <td class="num">${p.planQty} ${p.unit}</td>
- <td class="num" data-bound-for="${p.id}">${fmtTon(calcVehPlanQty(c, t, p.tractor, p).qty)} ${c.unit}</td>
- <td><select data-trans-for="${p.id}" onchange="syncCertLinkRow(${p.id})">${list.map((x) =>
- `<option value="${x.id}" ${x.id === tid ? "selected" : ""}>${x.transNo} · 余 ${fmtTon(transItemRemain(x))}${c.unit}</option>`
- ).join("")}</select></td>
- <td><select data-veh-for="${p.id}" onchange="previewCertLinkPlans()">${certLinkVehOptions(t, p.tractor)}</select></td>
- </tr>`;
- }).join("")}</tbody></table>` : `<div class="empty">没有可关联的易制毒 / 易制爆计划。</div>`;
- document.getElementById("certLinkPlanDialog").showModal();
- previewCertLinkPlans();
- }
- function filterCertLinkPlanRows() {
- const q = (document.getElementById("certLinkPlanQ")?.value || "").trim();
- document.querySelectorAll("#certLinkPlanTable tbody tr").forEach((tr) => {
- const blob = tr.getAttribute("data-blob") || "";
- tr.style.display = !q || blob.includes(q) ? "" : "none";
- });
- }
- function syncCertLinkRow(planId) {
- const c = certs.find((x) => x.id === linkingCertId);
- if (!c) return;
- const transSel = document.querySelector(`#certLinkPlanTable [data-trans-for="${planId}"]`);
- const vehSel = document.querySelector(`#certLinkPlanTable [data-veh-for="${planId}"]`);
- const p = plans.find((x) => x.id === planId);
- const t = findTrans(c, +(transSel && transSel.value));
- if (vehSel) vehSel.innerHTML = certLinkVehOptions(t, (vehSel.value || (p && p.tractor) || ""));
- previewCertLinkPlans();
- }
- function certLinkDrafts() {
- const c = certs.find((x) => x.id === linkingCertId);
- if (!c) return [];
- return [...document.querySelectorAll("#certLinkPlanTable [data-plan]:checked")].map((el) => {
- const p = plans.find((x) => x.id === +el.dataset.plan);
- if (!p) return null;
- const transSel = document.querySelector(`#certLinkPlanTable [data-trans-for="${p.id}"]`);
- const vehSel = document.querySelector(`#certLinkPlanTable [data-veh-for="${p.id}"]`);
- const t = findTrans(c, +(transSel && transSel.value));
- const plate = (vehSel && vehSel.value) || p.tractor || "";
- return { p, t, plate, qty: occupyQty(p) };
- }).filter((x) => x && x.t);
- }
- function previewCertLinkPlans() {
- const box = document.getElementById("certLinkPreview");
- const c = certs.find((x) => x.id === linkingCertId);
- if (!box || !c) return;
- const drafts = certLinkDrafts();
- drafts.forEach(({ p, t, plate }) => {
- const cell = document.querySelector(`[data-bound-for="${p.id}"]`);
- if (cell) cell.textContent = `${fmtTon(calcVehPlanQty(c, t, plate, p).qty)} ${c.unit}`;
- });
- const shown = new Set([...document.querySelectorAll("#certLinkPlanTable [data-plan]")].map((el) => +el.dataset.plan));
- const keep = linkedPlansOf(c).filter((p) => !shown.has(p.id));
- const used = drafts.reduce((s, x) => s + x.qty, 0) + keep.reduce((s, p) => s + occupyQty(p), 0);
- const buyLeft = Number(c.buyQty || 0) - used;
- const byTrans = {};
- const veh = {};
- drafts.forEach((x) => {
- byTrans[x.t.id] = (byTrans[x.t.id] || 0) + x.qty;
- if (x.plate) veh[x.t.id + "|" + x.plate] = (veh[x.t.id + "|" + x.plate] || 0) + x.qty;
- });
- keep.forEach((p) => {
- if (p.transId) byTrans[p.transId] = (byTrans[p.transId] || 0) + occupyQty(p);
- if (p.transId && p.tractor) veh[p.transId + "|" + p.tractor] = (veh[p.transId + "|" + p.tractor] || 0) + occupyQty(p);
- });
- const transTips = Object.entries(byTrans).map(([tid, hold]) => {
- const t = findTrans(c, +tid);
- if (!t) return "";
- const left = Number(t.transQty || 0) - hold;
- if (left < -1e-6) return `${t.transNo} 超 ${(-left).toFixed(2)}${c.unit}`;
- if (lowRemain(left, transWarnQty(t))) return `${t.transNo} 余 ${left}${c.unit}(≤${fmtTon(transWarnQty(t))})`;
- return `${t.transNo} 余 ${left}${c.unit}`;
- });
- const vehTips = [];
- Object.entries(veh).forEach(([key, hold]) => {
- const [tid, plate] = key.split("|");
- const t = findTrans(c, +tid);
- const d = t && (t.dispatches || []).find((x) => x.plate === plate);
- const cap = d ? Number(d.qty || 0) : 0;
- if (cap > 0 && hold - cap > 1e-6) vehTips.push(`${plate} 计划 ${hold}${c.unit} > 分派 ${cap}${c.unit}`);
- else if (cap <= 0 && hold > 1e-6) vehTips.push(`${plate} 计划 ${hold}${c.unit},运输证尚未分派该车`);
- });
- let cls = "";
- const bits = [
- `已勾选 ${drafts.length} 条,各车运输计划量 = min(计划量, 购买证余量, 运输证余量, 车辆分派)`,
- `关联合计 ${fmtTon(used)}${c.unit}`,
- `购买证 ${fmtTon(c.buyQty)} − 已完成/在途 = 余 ${fmtTon(buyLeft)}${c.unit}`,
- ...transTips
- ];
- const transOver = Object.entries(byTrans).some(([tid, hold]) => {
- const t = findTrans(c, +tid);
- return t && Number(t.transQty || 0) - hold < -1e-6;
- });
- if (buyLeft < -1e-6 || transOver || vehTips.length) {
- cls = "over";
- if (buyLeft < -1e-6) bits.push(`购买证超 ${(-buyLeft).toFixed(2)}${c.unit}`);
- bits.push(...vehTips);
- } else if ((buyLeft > 0 && buyLeft <= buyWarnQty(c)) || transTips.some((s) => s.includes("≤"))) {
- cls = "warn";
- bits.push(`余量 ≤ 预警量 ${fmtTon(buyWarnQty(c))}${c.unit},保存后预警`);
- } else if (vehTips.length === 0) {
- bits.push("车辆计划量未超分派");
- }
- box.className = "link-preview" + (cls ? " " + cls : "");
- box.textContent = bits.filter(Boolean).join(" · ");
- }
- function closeCertLinkPlanDlg() {
- document.getElementById("certLinkPlanDialog").close();
- linkingCertId = null;
- linkingCertFocusIds = [];
- }
- function saveCertLinkPlans() {
- const c = certs.find((x) => x.id === linkingCertId);
- if (!c) return;
- const drafts = certLinkDrafts();
- const missing = [...document.querySelectorAll("#certLinkPlanTable [data-plan]:checked")].length - drafts.length;
- if (missing > 0) { toast("请为勾选的计划选择运输证"); return; }
- const picked = drafts.map((x) => x.p.id);
- const shown = [...document.querySelectorAll("#certLinkPlanTable [data-plan]")].map((el) => +el.dataset.plan);
- let n = 0;
- let warn = 0;
- plans.forEach((p) => {
- if (p.certId === c.id && shown.includes(p.id) && !picked.includes(p.id)) {
- p.certId = null;
- p.transId = null;
- }
- });
- drafts.forEach(({ p, t, plate }) => {
- attachPlanToCert(p, c, t, true, plate);
- n += 1;
- const d = (t.dispatches || []).find((x) => x.plate === p.tractor) || { plate: p.tractor, qty: 0 };
- if (p.tractor && vehicleOver(d, t)) warn += 1;
- });
- closeCertLinkPlanDlg();
- toast(warn ? `已关联 ${n} 条计划,已按两证计算各车运输计划量,其中 ${warn} 车超分派预警` : `已人工关联 ${n} 条计划,已按购买证 / 运输证计算各车运输计划量`);
- showMod("cert");
- showCertView("detail", c.id);
- updateBadges();
- }
- document.getElementById("subnav").addEventListener("click", (e) => {
- const b = e.target.closest("button");
- if (b) showView(b.dataset.view);
- });
- document.getElementById("certSubnav").addEventListener("click", (e) => {
- const b = e.target.closest("button");
- if (!b || b.disabled) return;
- showCertView(b.dataset.view);
- });
- document.getElementById("expTabs").addEventListener("click", (e) => {
- const b = e.target.closest("button");
- if (!b) return;
- setExpTab(b.dataset.tab);
- });
- document.getElementById("planTabs").addEventListener("click", (e) => {
- const b = e.target.closest("button");
- if (!b) return;
- planCardFilter = "";
- planTab = b.dataset.tab;
- document.querySelectorAll("#planTabs button").forEach((x) => x.classList.toggle("on", x === b));
- renderPlans();
- });
- document.getElementById("planDateTabs").addEventListener("click", (e) => {
- const b = e.target.closest("button");
- if (!b) return;
- planCardFilter = "";
- planDateTab = b.dataset.tab;
- document.querySelectorAll("#planDateTabs button").forEach((x) => x.classList.toggle("on", x === b));
- renderPlans();
- });
- document.getElementById("quoteTabs").addEventListener("click", (e) => {
- const b = e.target.closest("button");
- if (!b) return;
- quoteTab = b.dataset.tab;
- quotePage = 1;
- document.querySelectorAll("#quoteTabs button").forEach((x) => x.classList.toggle("on", x === b));
- renderQuotes();
- });
- document.getElementById("ctTabs").addEventListener("click", (e) => {
- const b = e.target.closest("button");
- if (!b) return;
- ctTab = b.dataset.tab;
- ctPage = 1;
- document.querySelectorAll("#ctTabs button").forEach((x) => x.classList.toggle("on", x === b));
- renderContracts();
- });
- document.getElementById("archiveTabs").addEventListener("click", (e) => {
- const b = e.target.closest("button");
- if (!b) return;
- archiveTab = b.dataset.tab;
- document.querySelectorAll("#archiveTabs button").forEach((x) => x.classList.toggle("on", x === b));
- renderArchive();
- });
- document.getElementById("taskTabs").addEventListener("click", (e) => {
- const b = e.target.closest("button");
- if (!b) return;
- taskTab = b.dataset.tab;
- document.querySelectorAll("#taskTabs button").forEach((x) => x.classList.toggle("on", x === b));
- renderTasks();
- });
- document.querySelector(".sidebar").addEventListener("click", (e) => {
- const tog = e.target.closest("[data-toggle]");
- if (tog) {
- const group = tog.closest(".nav-group");
- const willOpen = !group.classList.contains("open");
- group.classList.toggle("open");
- if (willOpen && !group.querySelector(".side-sub.active")) {
- const first = group.querySelector("[data-mod], [data-page]");
- if (first?.dataset.mod) {
- if (first.dataset.whView) currentWhView = first.dataset.whView;
- if (first.dataset.fleetView) currentFleetView = first.dataset.fleetView;
- showMod(first.dataset.mod);
- } else if (first?.dataset.page) showPage(first.dataset.page);
- }
- return;
- }
- const leaf = e.target.closest("[data-mod], [data-page]");
- if (!leaf) return;
- if (leaf.dataset.mod) {
- if (leaf.dataset.whView) currentWhView = leaf.dataset.whView;
- if (leaf.dataset.fleetView) currentFleetView = leaf.dataset.fleetView;
- showMod(leaf.dataset.mod);
- } else showPage(leaf.dataset.page);
- });
- document.getElementById("globalSearch").addEventListener("keydown", (e) => {
- if (e.key !== "Enter") return;
- const q = e.target.value;
- if (currentMod === "cert") {
- document.getElementById("certQ").value = q;
- showCertView("list");
- } else if (currentMod === "plan") {
- document.getElementById("planQ").value = q;
- renderPlans();
- } else if (currentMod === "product") {
- document.getElementById("prodQ").value = q;
- prodPage = 1;
- renderProducts();
- } else if (currentMod === "quote") {
- document.getElementById("quoteQ").value = q;
- quotePage = 1;
- renderQuotes();
- } else if (currentMod === "contract") {
- document.getElementById("ctQ").value = q;
- ctPage = 1;
- renderContracts();
- } else if (currentMod === "archive") {
- document.getElementById("archiveQ").value = q;
- renderArchive();
- } else if (currentMod === "fleet") {
- document.getElementById("fleetQ").value = q;
- showView("fleet");
- } else if (currentMod === "expense") {
- setExpTab("全部");
- const box = document.getElementById("expQ");
- if (box) box.value = q;
- if (typeof renderExpenseList === "function") renderExpenseList();
- } else if (currentMod === "staff") {
- const box = document.getElementById("staffQ");
- if (box) box.value = q;
- staffPage = 1;
- renderStaff();
- }
- });
- fillPlates();
- updateBadges();
- bindCertDrop(document.getElementById("buyOcrBox"), "buy");
- bindCertDrop(document.getElementById("transOcrBox"), "trans");
- document.getElementById("certForm")?.addEventListener("input", (e) => {
- const n = e.target && e.target.name;
- if (n !== "buyQty" && n !== "transQty") return;
- const box = e.target.closest(".form") || e.target.closest(".extra-trans") || e.target.closest("#buyFormCol");
- const warnEl = box && box.querySelector("[name=warnQty]");
- if (warnEl) warnEl.value = defaultWarnQty(e.target.value);
- });
- showMod("cert");
- document.addEventListener("click", () => {
- document.getElementById("ctExportDrop")?.classList.remove("open");
- document.getElementById("prodExportDrop")?.classList.remove("open");
- });
- </script>
- </body>
- </html>
|