Supabase Authentication에 등록된 계정만 사용할 수 있습니다.
DashboardWISE LOGISTICS QUOTATION SYSTEM

Dashboard

FCL Quotation

FCL 견적 작성 및 출력

LCL Quotation

LCL / AIR 견적 작성 및 출력

History

기존 저장 히스토리는 현재 브라우저와 선택 폴더 기준으로 유지됩니다.

WISE EXPRESS LOGISTICS PTY LTDQuotation System
선택 양식: AIR_SPD TBA / AT COST 선택 · 운임구간 자동계산 저장 폴더 미선택
① Quotation 기본 정보
② Shipment Details
AIR는 Chargeable KG = Max(실중량 KG, CBM×167) 기준, SEA는 CBM × Per CBM으로 자동 계산됩니다.
③ Destination Clearance / Handling
SPD/CTN은 Final Local Delivery TBA, LTL은 팔렛 수 기준 자동 계산, AIR_PLT는 공통 LCL 기준 $250 적용됩니다.
④ Entry Charge / Internal GST 참고
계산식 참고: FOB: (CI + Freight + Insurance 0.25% + Local) × 10%
CI Value가 0 또는 공란이면 견적서의 Entry Charge와 Import GST는 TBA로 표시됩니다.
⑤ LCL 견적서 TBA 표시
체크한 항목은 고객용 견적서에 TBA로 표시됩니다. CBM/KG/BOXES/PALLET 입력칸에는 직접 TBA 입력도 가능합니다.
⑥ LCL 견적서 AT COST 표시
AT COST로 선택한 항목은 고객용 견적서에 AT COST로 표시되고 숫자 합계에서는 제외됩니다. 같은 항목의 TBA와 AT COST는 동시에 선택되지 않습니다.
Origin Charges
TBA
Freight
TBA
Clearance
$0.00
Handling
$0.00
Local Delivery
TBA
Approx. Total
$0.00
저장 기록
처음에는 전체 기록이 표시됩니다. 시작일과 종료일을 지정하면 해당 기간만 볼 수 있습니다. 목록은 최대 5개 높이로 표시되며 스크롤바를 드래그해 이전 기록을 계속 확인할 수 있습니다.
전체 0개
저장일시Customer양식총액HTMLExcel불러오기관리
기록 없음
`} // ===== Folder-save web version: no IP, no server, no EXE/HTA ===== let folderHandle = null; const DB_NAME='wise_quote_folder_db_v1'; const STORE='handles'; const LOCAL_HISTORY_KEY='wise_quote_save_history_v2'; const LOCAL_PAYLOAD_KEY='wise_quote_payload_map_v1'; const LOCAL_FOLDER_NAME_KEY='wise_quote_folder_name_v1'; function openDB(){return new Promise((resolve,reject)=>{let req=indexedDB.open(DB_NAME,1);req.onupgradeneeded=()=>req.result.createObjectStore(STORE);req.onsuccess=()=>resolve(req.result);req.onerror=()=>reject(req.error)})} async function setStoredHandle(handle){try{let db=await openDB();let tx=db.transaction(STORE,'readwrite');tx.objectStore(STORE).put(handle,'quoteFolder');return new Promise(res=>{tx.oncomplete=res;tx.onerror=res})}catch(e){}} async function getStoredHandle(){try{let db=await openDB();let tx=db.transaction(STORE,'readonly');let req=tx.objectStore(STORE).get('quoteFolder');return await new Promise(res=>{req.onsuccess=()=>res(req.result||null);req.onerror=()=>res(null)})}catch(e){return null}} async function verifyPermission(handle, write=true, ask=true){ if(!handle)return false; try{ let opts=write?{mode:'readwrite'}:{}; if((await handle.queryPermission(opts))==='granted')return true; if(!ask)return false; return (await handle.requestPermission(opts))==='granted'; }catch(e){return false} } function folderNameText(handle){return handle&&handle.name?String(handle.name):''} function updateFolderStatus(text, ok=false){let el=$('folderStatus'); if(!el)return; el.textContent=text; el.style.background=ok?'#e9fff3':'#f2f6ff'; el.style.color=ok?'#087143':'#234bd3'} async function chooseFolder(){ if(!window.showDirectoryPicker){alert('이 브라우저는 폴더 직접 저장을 지원하지 않습니다. Chrome 또는 Edge 최신버전으로 열어주세요. 지금은 다운로드 저장 버튼을 사용하세요.');return} try{ folderHandle=await window.showDirectoryPicker({id:'wiseQuoteFolder',mode:'readwrite'}); if(!(await verifyPermission(folderHandle,true,true)))throw new Error('폴더 쓰기 권한이 허용되지 않았습니다.'); await setStoredHandle(folderHandle); try{localStorage.setItem(LOCAL_FOLDER_NAME_KEY,folderNameText(folderHandle))}catch(e){} updateFolderStatus('저장 폴더 선택됨'+(folderNameText(folderHandle)?' · '+folderNameText(folderHandle):''),true); await loadRecords(); }catch(e){alert('폴더 선택 취소 또는 실패: '+e.message)} } async function getWritableFolder(ask=true){ if(folderHandle && await verifyPermission(folderHandle,true,ask))return folderHandle; let saved=await getStoredHandle(); if(saved){ folderHandle=saved; try{if(folderNameText(saved))localStorage.setItem(LOCAL_FOLDER_NAME_KEY,folderNameText(saved))}catch(e){} if(await verifyPermission(saved,true,ask)){updateFolderStatus('저장 폴더 선택됨'+(folderNameText(saved)?' · '+folderNameText(saved):''),true);return folderHandle} } return null; } async function writeTextFile(dir, filename, text){let fh=await dir.getFileHandle(filename,{create:true});let w=await fh.createWritable();await w.write(text);await w.close()} async function readTextFile(dir, filename){try{let fh=await dir.getFileHandle(filename,{create:false});let file=await fh.getFile();return await file.text()}catch(e){return ''}} function csvEscape(v){v=String(v??'');return /[",\r\n]/.test(v)?'"'+v.replace(/"/g,'""')+'"':v} function nowStamp(){let d=new Date();return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`} function normalizeHistoryRow(row){let r=Array.isArray(row)?row.map(v=>String(v??'')):[];while(r.length<7)r.push('');return r.slice(0,7)} function deriveJsonName(row){let r=normalizeHistoryRow(row);if(r[6])return r[6];let base=r[4]||r[5]||'';if(/_웹견적서\.html$/i.test(base))return base.replace(/_웹견적서\.html$/i,'_GIBAEK.txt');if(/_견적서\.xls$/i.test(base))return base.replace(/_견적서\.xls$/i,'_GIBAEK.txt');return ''} function getLocalHistory(){try{let arr=JSON.parse(localStorage.getItem(LOCAL_HISTORY_KEY)||'[]');return Array.isArray(arr)?arr.map(normalizeHistoryRow):[]}catch(e){return []}} function setLocalHistory(rows){try{localStorage.setItem(LOCAL_HISTORY_KEY,JSON.stringify(rows.map(normalizeHistoryRow).slice(-300)))}catch(e){}} function addLocalHistory(row){let arr=getLocalHistory();arr.push(normalizeHistoryRow(row));setLocalHistory(arr)} function getLocalPayloadMap(){try{let obj=JSON.parse(localStorage.getItem(LOCAL_PAYLOAD_KEY)||'{}');return obj&&typeof obj==='object'?obj:{}}catch(e){return {}}} function setLocalPayload(name,text){try{if(!name||!text)return;let obj=getLocalPayloadMap();obj[name]=String(text);let keys=Object.keys(obj);while(keys.length>300){delete obj[keys.shift()];}localStorage.setItem(LOCAL_PAYLOAD_KEY,JSON.stringify(obj));}catch(e){}} function getLocalPayload(name){try{return getLocalPayloadMap()[name]||''}catch(e){return ''}} async function appendSaveHistory(dir,row,inputJson){ let safe=normalizeHistoryRow(row); let hist=await readTextFile(dir,'저장기록.csv'); if(!hist.trim()) hist='저장일시,Customer,양식,총액,HTML,Excel,GIBAEK\n'; hist += safe.map(csvEscape).join(',')+'\n'; await writeTextFile(dir,'저장기록.csv',hist); addLocalHistory(safe); setLocalPayload(deriveJsonName(safe),inputJson); } function renderRecords(arr,msg){ arr=(arr||[]).map(normalizeHistoryRow).slice(0,50); if(!arr.length){$('recordsBody').innerHTML=''+esc(msg||'기록 없음')+'';return} $('recordsBody').innerHTML=arr.map((r,i)=>{let json=deriveJsonName(r);let disabled=json?'':' disabled';return `${esc(r[0])}${esc(r[1])}${esc(r[2])}${esc(r[3])}${esc(r[4])}${esc(r[5])}`}).join(''); } function setFieldValue(id,val){let el=$(id);if(!el)return;el.value=(val===undefined||val===null)?'':String(val)} function setFieldCheck(id,val){let el=$(id);if(!el)return;el.checked=!!val} function loadLclInput(d){ d=d||{}; if($('quoteType')) $('quoteType').value='LCL'; ['mode','delivery','customer','attention','issueDate','expiryDate','issuedBy','pol','pod','cbm','kg','boxes','pallet','shipDetails','incoterms','service','route','ccf','ior','labelQty','labelRate','repalletRate','ciValue','gstLocal'].forEach(id=>setFieldValue(id,d[id])); const t=d.tba||{}; setFieldCheck('lclTbaCbm',t.cbm);setFieldCheck('lclTbaKg',t.kg);setFieldCheck('lclTbaBoxes',t.boxes);setFieldCheck('lclTbaPallet',t.pallet); setFieldCheck('lclTbaFreight',t.freight);setFieldCheck('lclTbaCcf',t.ccf);setFieldCheck('lclTbaIor',t.ior);setFieldCheck('lclTbaEntry',t.entry);setFieldCheck('lclTbaImportGst',t.importGst); setFieldCheck('lclTbaLabelling',t.labelling);setFieldCheck('lclTbaRepallet',t.repallet);setFieldCheck('lclTbaOrder',t.order);setFieldCheck('lclTbaHandlingOut',t.handlingOut);setFieldCheck('lclTbaPacking',t.packing);setFieldCheck('lclTbaLocal',t.local); setFieldCheck('lclTbaFreightSubtotal',t.subtotalFreight);setFieldCheck('lclTbaClearanceSubtotal',t.subtotalClearance);setFieldCheck('lclTbaHandlingSubtotal',t.subtotalHandling);setFieldCheck('lclTbaLocalSubtotal',t.subtotalLocal);setFieldCheck('lclTbaTotal',t.total); const a=d.atcost||{}; setFieldCheck('lclAtCostOrigin',a.origin);setFieldCheck('lclAtCostFreight',a.freight);setFieldCheck('lclAtCostCcf',a.ccf);setFieldCheck('lclAtCostIor',a.ior);setFieldCheck('lclAtCostEntry',a.entry);setFieldCheck('lclAtCostImportGst',a.importGst); setFieldCheck('lclAtCostLabelling',a.labelling);setFieldCheck('lclAtCostRepallet',a.repallet);setFieldCheck('lclAtCostOrder',a.order);setFieldCheck('lclAtCostHandlingOut',a.handlingOut);setFieldCheck('lclAtCostPacking',a.packing);setFieldCheck('lclAtCostLocal',a.local); setFieldCheck('lclAtCostFreightSubtotal',a.subtotalFreight);setFieldCheck('lclAtCostClearanceSubtotal',a.subtotalClearance);setFieldCheck('lclAtCostHandlingSubtotal',a.subtotalHandling);setFieldCheck('lclAtCostLocalSubtotal',a.subtotalLocal);setFieldCheck('lclAtCostTotal',a.total); updateAll(); } function applySavedQuotePayload(text){ let obj=JSON.parse(text); let d=obj.input||obj; if(d && (d.type==='FCL' || Array.isArray(d.rows))){ if($('quoteType')) $('quoteType').value='FCL'; if(window.fclLoadInput){window.fclLoadInput(d);return;} throw new Error('FCL 입력폼이 아직 준비되지 않았습니다. 화면 새로고침 후 다시 눌러주세요.'); } loadLclInput(d); } async function loadSavedQuoteFromButton(btn){ if(!btn)return; const jsonName=btn.dataset.json||''; $('saved').style.display='none';$('err').style.display='none'; if(!jsonName){$('err').textContent='불러올 GIBAEK 입력파일명이 저장기록에 없습니다.';$('err').style.display='block';return;} btn.disabled=true; const oldText=btn.textContent; btn.textContent='불러오는 중'; try{ let text=''; let dir=await getWritableFolder(true); if(dir) text=await readTextFile(dir,jsonName); if(!text) text=getLocalPayload(jsonName); if(!text) throw new Error('GIBAEK 입력파일을 찾을 수 없습니다. 저장 폴더를 다시 선택한 뒤 불러오기를 눌러주세요.'); applySavedQuotePayload(text); $('saved').innerHTML='불러오기 완료
'+esc(jsonName)+'
저장 당시 입력값과 금액이 화면에 복원되었습니다.'; $('saved').style.display='block'; window.scrollTo({top:0,behavior:'smooth'}); }catch(e){ $('err').textContent='불러오기 실패: '+(e.message||e); $('err').style.display='block'; }finally{ btn.disabled=false; btn.textContent=oldText; } } function makePayload(){updateAll();let q=buildQuoteHtml();let d=q.c.d;let fileBase=`${fileDate(d.issueDate)}_${cleanFile(d.customer)}_${sheetName()}`;let quoteHtml=`${fileBase}
${q.html}
`;return {fileBase,customer:d.customer||'NO NAME',mode:d.mode,delivery:d.delivery,total:money(q.c.total),quoteHtml,excelHtml:buildExcelHtml(),inputJson:JSON.stringify(d,null,2),inputObj:d}} async function saveQuote(){ $('saved').style.display='none';$('err').style.display='none'; let dir=await getWritableFolder(); if(!dir){ $('err').innerHTML='저장 폴더가 아직 선택되지 않았습니다. 저장 폴더 선택을 먼저 누르거나, 다운로드 저장을 사용하세요.'; $('err').style.display='block';return; } try{ let p=makePayload(); let htmlName=`${p.fileBase}_웹견적서.html`; let xlsName=`${p.fileBase}_견적서.xls`; let jsonName=`${p.fileBase}_GIBAEK.txt`; await writeTextFile(dir, htmlName, p.quoteHtml); await writeTextFile(dir, xlsName, p.excelHtml); await writeTextFile(dir, jsonName, p.inputJson); await appendSaveHistory(dir,[nowStamp(),p.customer,`${p.mode}_${p.delivery}`,p.total,htmlName,xlsName,jsonName],p.inputJson); $('saved').innerHTML=`저장 완료
${p.fileBase}
선택한 폴더에 HTML / Excel / GIBAEK / 저장기록.csv가 저장되었습니다.`; $('saved').style.display='block'; await loadRecords(); }catch(e){$('err').textContent='저장 실패: '+e.message;$('err').style.display='block'} } function downloadBlob(name, text, type){let blob=new Blob([text],{type:type||'text/plain;charset=utf-8'});let a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();setTimeout(()=>{URL.revokeObjectURL(a.href);a.remove()},500)} function downloadQuote(){let p=makePayload();downloadBlob(`${p.fileBase}_웹견적서.html`,p.quoteHtml,'text/html;charset=utf-8');downloadBlob(`${p.fileBase}_견적서.xls`,p.excelHtml,'application/vnd.ms-excel;charset=utf-8');downloadBlob(`${p.fileBase}_GIBAEK.txt`,p.inputJson,'text/plain;charset=utf-8')} function syncQuoteDocumentTitle(){ document.title='WISE EXPRESS'; } function getQuotePdfTitle(){ try{ if($('quoteType')&&$('quoteType').value==='FCL'&&window.fclMakePayload){let p=window.fclMakePayload();if(p&&p.fileBase)return p.fileBase;} }catch(e){} try{let p=makePayload();if(p&&p.fileBase)return p.fileBase;}catch(e){} try{ let d=new Date();let dt=`${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; let cust=cleanFile(($('customer')&&$('customer').value)||'QUOTE'); let sheet=(typeof sheetName==='function')?sheetName():'QUOTE'; return `${dt}_${cust}_${sheet}`; }catch(e){return 'WISE_QUOTATION'} } function cleanPrint(){ updateAll(); const oldTitle=document.title; const pdfTitle=getQuotePdfTitle(); if(pdfTitle)document.title=pdfTitle; document.documentElement.classList.add('clean-print-mode'); const restore=()=>{document.title='WISE EXPRESS';document.documentElement.classList.remove('clean-print-mode');window.removeEventListener('afterprint',restore)}; window.addEventListener('afterprint',restore,{once:true}); setTimeout(()=>window.print(),50); setTimeout(restore,2500); } function printQuote(){cleanPrint()} function resetForm(){['customer','attention','cbm','kg','boxes','pallet','service','route','labelQty','ciValue'].forEach(id=>$(id).value='');document.querySelectorAll('.lcl-tba-grid input[type="checkbox"]').forEach(e=>e.checked=false);$('issueDate').value=today();$('expiryDate').value=addDays(14);$('issuedBy').value='Gibaek Kim';$('pod').value='AUSYD';$('shipDetails').value='General Cargo - Non Hazardous';$('incoterms').value='FOB';$('ccf').value='200';$('ior').value='150';$('labelRate').value='0.8';$('repalletRate').value='33.5';$('gstLocal').value='0';$('pol').value=defaultPol();updateAll()} function parseCSV(text){let lines=text.split(/\r?\n/).filter(Boolean);if(lines.length<=1)return [];let out=[];for(let i=1;i{ resetForm(); try{ let saved=await getStoredHandle(); let savedName=''; try{savedName=localStorage.getItem(LOCAL_FOLDER_NAME_KEY)||''}catch(e){} if(saved){ folderHandle=saved; if(await verifyPermission(folderHandle,true,false)){ updateFolderStatus('저장 폴더 선택됨'+(folderNameText(folderHandle)?' · '+folderNameText(folderHandle):''),true); }else{ updateFolderStatus(savedName?'이전 저장 폴더 권한 재확인 필요 · '+savedName:'준비완료 · 저장 폴더를 먼저 선택하세요',false); } }else if(savedName){ updateFolderStatus('이전 저장 폴더 다시 선택 필요 · '+savedName,false); } }catch(e){} loadRecords(); }); /* ===== v5.0 FCL summary category cards ===== */ /* ===== v3.3 FCL module: card input + auto 10% VAT/GST ===== */ (function(){ let OLD = {}; const FCL_LOGO = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXQAAACOCAYAAAAhHfOYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAADJ2SURBVHhe7d15fNx3fefx1+83933plqzDlmxLthMfcRI7jmMn5CYhQAkJkNBSaIASaBe2sLCUsKW7QJceu5BylAJpgAYCSUgc27kc5/Ad37J1y7pH0kgzo7nP3/4xo9HMSLIdSOpm9vt8PH5/eC5rZn7z/n1/n+/xkxRFURAEQRDe8eTiGwRBEIR3JhHogiAIJUIEuiAIQokQgS4IglAiRKALgiCUCBHogiAIJUIEuiAIQokQgS4IglAiRKALgiCUCBHogiAIJUIEuiAIQokQgS4IglAiRKALgiCUCBHogiAIJUIEuiAIQokQgS4IglAiRKALgiCUCBHogiAIJUIEuiAIQokQgS4IglAiRKALgiCUCBHogiAIJUIEuiAIQokQgS4IglAiRKALgiCUCBHogiAIJUIEuiAIQokQgS4IglAiRKALgiCUCBHogiAIJUIEuiAIQokQgS4IglAiRKALgiCUCBHogiAIJUIEuiAIQokQgS4IglAiRKALgiCUCBHogiAIJUIEuiAIQokQgS4IglAiRKALgiCUCBHogiAIJUJSFEUpvlGYb9Od/1Z8kyAIQoH9v7uv+Kb/UKKFLgiCUCJEoAuCIJQIEeiCIAglQgS6IAhCiRCBLgiCUCJEoAuCIJQIEeiCIAglQgS68KZIEug0MlajBptJi1GvRq2Sih/2jqdVy5gNaqxGDUadGtUf+B4lCTRqGatBg82oxahVoZL/sNdcjFYlY9Fn/h+TXo1G9Z/7Z66SJYw6NXazFotBg0Yl8/t+MjqtCotRg9WoxfAWfG/vNGJi0UV6qycWSRJYjVpWrSzHZNTkbo8n0kx6wpzp8RQ8XqOSqas2s6zJWbCznzo7yeR0mFR67mvUalS0NbtwOg0FoTHti9DT5yUQSVBu07NieRk6rSp3/5Q3wtnuKSKxZO42AJVKwmrSUeEyUu4y4rTrMOrUSLJEPJnGH4zj8YRxTwSZ9EYK/hYAu1lHfa2VygpTwe0XEgjG6TnnxeONFN+VY7foaKi1UlGeee14IsXRk25CkSTpN7Fry5KEQa+i0mWizGWkzKnHpNegynuPE9n3OOWPki56jwuZ/Y7LnUbKyoy4nHosOg2SLBGLp/DOxBifDOH2hPAGYigLvGZ9jZWlDXY0aplUWmFiIkT3gJdYPJV7jCxJGHQqKlwmKspNlNn1mAwa1CqJRDKNPxRn3BNmzB3AU/S3V1eYaWlyFOwHb8a4J0R37zQpBVoaHFRUmFCrJJLJNH1DfgaG/cVPAcCgU1Nm11NRZsLpMGA1a9BrVCRTafyhBJ7pCO6JIB5vhGjeey2mkiXMRg1VZWbKygw47frMvilJxBIpfDMxxidCDLsDBMJx3sQu8Xu51BOLRKBfpLc60GVJYkWdja/+1bU0Ndhzt/tnYrx+YJi/+e6+gsdXOgy895bl3H/PGiRpLqS//u1XePXICKFoJoRlSaLCZeBrn9tCW2sZ2uwPNZlMs//wMN//6VFGpsJsX1/LZx7YiMtlBEBRYP/hYb713f1M+KK51zfq1dRWmVndUs4Vq6toay2nqqowmIPBOL19XvYfHuH148P0D80UhPr6lRXcfcdKrru2oeB5F3LmrIcf/Pw4h06OFd+Vs2FlBffc1caWzUsAmPZG+MyXdjM0ESKZShc/fEF6rYpyp5GWRjtXX1bDmtUV1NRYcp+dokAgEKeza4qDbwzz+okxRieCxM8TNHqtiopyI6uWudiwqprVbRXU11sKvrvp6Qhnzk6y740RDrePMzoeLAhbWZa4585W/vSeyzAaNUSjSXY/38cPf3WCaX/mO9KqZVx2PSuWOrnqshrWXV5NTY0ZjWauVR4KJejumeb1g4O8fGQYtydMMplGliTe/a5l/PlH12O16nKPv1iKAi+9eo5//NFh0sDnP3YlmzfVoter8Xqj/OzXp3jsmY6C56hkCafdQEu9jfWrKlm3pprGRjtGozr3mFgsRf85H0eOjXLg2Ahn+72Es/t3Pr1OTU25iVUtLq66vIY1aypxOvSo1Zn3nk4r+P0xTrdPsGffAIdOufEFYvMaHG+lSx3o/7nPxUqYgoIvECVVFDoqWcoFySxJgmX1DlqbXQWBQHanVuedUuu0Mg3VFi5bU1HwOh5PmDPd0/S5A6hkiZpqK2r13P2pVJpAOMF0IJa7TaOWWdtSxifvXcvn/uwKrt/eOC/MAcxmLZdfVsl996zhgQ+tx2HRIef9nXabHqfDUPCcizHmDhIIxotvLuBwGLBZ9ZD9AUeiKca9YVLpiwtzrVbF8kYH972nja/+ly3c8e7lNDbaCz47SQKrVcvGK6r51Mev4JMfXEtjtWXRUpNGLdPa6OCBe9bylw9cxe23NtPQYJ333TmdBrZcU88n7l/H/e9djbMoVE16NQ6rDmP2DC6VUhge8ZNMZt6bSpaoqzDxvhtb+MrnruGuO1fQ0GBFrZZIpRTSaQVFAZNJw9rLK/mzP1nPh9/dRrndgCSBXivjsOp+rzAnu8/MhON4QzE0GhVNjfZcmI5PhJianmsYkG1sVDqN3H3LCr7wqav58AfX0NZWVhDmADqdipUrXNz7gVV84sPrWNtShlxUntJoZFY0Orj/vav4qwc3c8P1TVSUG5HlufcuyxIOh54t19TzXx/cxPUb67CYtAWvU2pEoF8iigKecJxEUWtBpZIxGOZKMJCpWbe1lrF6VUXB7QBmvQZt9kcEYDXr2LimhqLsoLNniiNHR0DJ/B+NdbaCVpzfH2N8PEQyNff3bFhZwcc+sparr6pDpyv80S3EZNLQttLFndubCwLR6TBQlj0TeDMmJkOEwucP9DKXEYc9E+jRWIqBQT+JZCbILkiCq9ZU8fF7Lue2W5rR6y/8HlUqiS1blnD7tmXUVliK70aWJDa0lPG5B65k6zX1mM0XDhC7Tc+mK2q457bWgtB3mHVYjXPPT6XTjLoDuUCvdhp5z03Lufv9qzDl/T/hcIKxsSAeT4REYu4sQqNRccftLaxa5sSk12Az67CZf78wB/B6o0xOhFHSYDSqqa+35AJ9aiqM3z9XKpNlCadFy19+fCPvuX05VVXmvFdamEol09ZazkfvvYwKm65gn17V5OLud6/kXdcvRa2eu8PvjzE2FsTni+U+J0kCvV7F/R9aS22NuaTr6qqHHnrooeIbhfl+/MuTxTf9wZS0wtYrl1BRZsr9EBRFwT8TZcdLvbnHtTY62XZ1Pc3LHPNaeafbJ+gd9BGIJJBkicYlNu7/wBpstkzIAfh8UfbuH+KVI8MkUwpGg4YPvXdVpsae3bnH3EHeOOHmbO8UkiThsGj59J9soHW5K6/0oDAzE+fwkTFOnhxnyhPBbNZiMMwF4ewB6YXXzxGPp1CpJK65qo4r19XkvUcYGwvSf87H0EiA0bHggtv+I8P0D/sXr6FKsP2aBtatqUSjUREMxjl63M2hk+6Lqp9vaK3gvbeuYP3aKrRaFclkGrc7yK9+c4ZHn2jnsafPsuulPvr6vOi1GqoqM2cnsixRXWmmu2+akbFA7hReq5ZZvsTO5z55FU2NdjQaFem0Ql+flyee7uSRx0/yi6fOsOulPvp7vTjsehwOA7IsodOpsTv07D84RDiWRFGgvtrKhsuqciW5SDTJ48904JmJIgG3bl/KTduW4nQYSKcVPJ4wP/rpMf718dM8/WIPu/f20X52Eo0sU7/ElvvbBwZ8nBuZYdIXpbN/mh0v9PKbZztz26v7B6muNFNVkQndwSE///Kz43z3kaMFj3vmxV5OnJ2AtMKyWhu3vqs599meODXOsfZxpvxRJClzUP+Lj21k/eVVmM1aZFlifCLMK68N8MhjJ3n0yXZ+s7OLY0fH0GnU2G16dLpMx7ECTHuidA/6SCsKdpOWO29pYfu1jej1amKxFF3d0zz8r4f5+VNn2fFSLy+80s/g0AxOq56yMiOSlPmMT5xw454MEU9c3Bncm/Xxey8vvuk/lGihX0KKkqmjRvM6IYtb6LIksXZNFc3LnPPCHMBg0KDWZALXYdKyot4+r/XTfmaS9o5JYvEUKlnCatRQWWUqaNnMBGJMekIAqFUSa1dU0LDEhl4/97cMDvn56S9O8KPHTvDzZ87yg38/ztO7uxkemck9Rq2Wqaow5U6RbUYNdpO2oMWeTiu88HIfP/rlCb73yNFFtyPt4wTCidzzilkNGhxWXa5lHYsnGRmd4WK6hVxWPTdta+Ky1RXo9Wqi0SRnOjz8048P88zePo6fmaCrz0t79xTPvz7Ab3Z10tk5lXu+3aGnqdaKM1vukSVwOfR8+I9W01Bvyx0gDhwe5qePneTpl3s50eGh55yPsz3TPL9vgB3P99LT64Vsy99m0bG01pYbleK06XPlJCWtEI2mcHvDpFIKFQ49y5c6cgcZnz/Kjt3dvHRwiJ5zXobHAgyOznDw5BgHi/og9Fo1KlkimUoz7YvSP+Qv2EKBOEremVoonKCjxzPvcUOjM/hmYui0KiqdhWdgPn80Vy6zm3Vct76OK9dX58K8q3uax393lp8/dYYDJ9109XnpOefjwIkxfvFkO+0dk5n+BAmMBi2ty8tz+9TSOhtNS+yYs+UTrz/CY0+2s/+Em74hP8PuAH1Dfo6cGOPYSXfub1KppNx7L1Ui0C+xaW+UWF6HT6a1NjekrabMyMpmJ+VlC5csTEYt2mygV5abWLe6qqCUEg4nONE5Se+QDyVb3y2369FnRwLMCgbjeH2ZU2S1SmZVSzkmg6bgNNfnj3H89DjdA14GRmfoOufltUNDPPtcD08/28XTz3bxzM4uXtrbTyLbqnZY9FhM2oLXURSFs92TnO3x0NE3teg25Y+et2PTZdVjNmpyP/R4PIV7InhR5Zb1bZW0LS/HZtWjKDA8GuCpnZ0cPOHGPRUmEkuRSiskkmk8vgjtnZMcPDqSe74sS7icRizZUofJoGHVUhcb19eg02W+j9PtEzy/t59DJ92MT4WJxlMoCiRSaTz+KEdOjvHy6wPsOzDMvgPDHDk6VvB+XS4jjuyZVjyRZnwiRDiaIq0oVDqMuOwGNNnvPh5PMTQ8gzcQI5UNY0WBQCjOgWOj/O8fHMptew8P48vrKylW7jRiyCuxReNJ3N5wwWPy6bRqKssLGxHeYIxAOI5KlqiuMHHz9qVYLDpkWWJqKsLe/YO8tH+A/hE/wUiCVFpBSSsEIgnae6bo7vfmDggajUxlhSm3D9VUWnDY9Ll/x+Mp+gd9BMOJXKdyWlFwe0LserW/4L2f7PIQiS1yxlcCRKBfYl5vhFjeDiZJmZaERi0hSbCurZL6ukyLbyFmoxaNRoVOo6K+1sbqtsI6e1f3NJ1903izPw6tWkWVyzivxh6MxJmeyXRizYaVKq82T7YTb9PGOta3VlBfacFs1DA4FuD5187xq6c7+NXTHfz6mU6eer47N6zOaTNgzqsDz4on0lhNWsps+gW3xToc87kcBkx5ZzPxZAr3VAiFxRM90xmo4pqr6qgoz3wOfn+Uk+0T7Ds6WjAcMN9MME5X33TBbSajFn02+Fx2A5uvqMNi0SJJEuFwgv1vjHK0fYJAZOGzjDFPiL0HB3nsd2d57HdneXJ3F+fGAiRSaSQJysuMuc7kaCzJwKA/F1hajRpVXme4yahhw9pqLlteRqXDgFadGcutKDA8FuA3Ozpz24muSYKL/E0AFeVmDNmO2Hg8M/TPH1q8L0OnV1NTPdefEAol8AZiROIpLEYNLY0OVq5w5fa5E6fGOXxijDFPaMGDbzSe4tCJMR5/JrNPPbmri/1Hh+dKWzpVrnwHYDJp2XL1EpY3OHBYdLl9JxJP0XnOW/De+4b9xPL6FUqNCPRLzD8TJRYvHJIlSRJ6rQqzQcMV62qozI6xTiTSRCKFjzWaMi30MruelgY7ldlT8NnHHzg6wuCwPzfGWatVUVVhKQj0VErBH4rnQl9RFIKhGOmi1nFttYUPvKeVe+9s464bm7l5ayOrW8sxGLVM+iP0jvrpGfHTn1dXdjkNWC2FgS7JEhsur+bmrY3cvn1pwXbbtiZuvKYhF5TnU+40YsoeLNJphUgkyYQvsmBIzJIliWqXkRXLXVgsmQ7B4ZEZjp104ztPaCWT6XkhqJIlJFlCLUtUlpvYsK4md9/AoJ+zfVN48joGi0XiKfpGZjh02s2h027eODvB2FSmpKLXqHA59Zizn10slmRoZK6c5A1kShqzrXGLRcv26xq5+/aV3H79MrZtqmdtWyUN1VZsRWdIF1JTbc59rsFgnPHxMInk4h+qQa9mSa019+/x8RD+mTiptEKZw8iq5rnhs4lEisPHRxkcnTnv93TszDiP/PY0D//sKD/4+XF+u7sr18k57Y0UdJbbrXre/+5W3n/Lcm7d1sSWjXWsXl5ObYUZ00V0dJcSEeiXmH8mOm88sySBUadmZYOdlmUOLNkf9fR0mN6iVqLFokWnU9FYZ2dls6vgPrc7yKluD5PZUgrZQK+pLhxCFwol8PpjhLO1/GQqzcmz43h9hcMq54aBLeHeu1fzhU9fzRceuIr77mpj8/oaqsrnauezystMWLN14FkqWeJDH1jNA3+ygU9+rHD7xEfX8/47WjHk1e4XU1FuztVRY7EUU94oM6HFW55k66jL6x0Y9epcyI1PhugZyNSyFyPJoCk6a4hGkyTiKYx6NTXlRioq5spip9onmJgM50JLkkCvU7FymWvRrbnRgSbbsnZadFiM2lyndTyeYswdyJ17jEwE6eydxu0OkkymkSQJo1HDdVsb+MRH1/HFP7+aBz+6gbtvXcHmddUsqbYUjIZajCxL1FRbMJkyn/9MII57PFj8sBxJypwd1NbOtdBH3UGC2caB02Ggeakzd5/HE6F/LMDMBUYvpVIK8XiaWDxFLJ4q6MTsOjdNZ+80vuwkKZVKwuUycPutLTz4iY188dNX86kPr+XOG5pZ31ZJhSsznPH/Bxf+hoW3VSAYJ150CigjYdRpuO7qRux5Y6zP9kyxa09vweQTi1mLxaRhebOD1hVludsVBV7aey4zCiOvg0uvU1O/xEr+dNNJT5ipqQizaZFIpnnl+AgHjowyPhEmHk8V/J/5qipN3HjDUj7zsSv48J1t2POGz0kSVFSYsNsufmhcMpmmu9dLItsaO5/qanPuYBcMxhkZCRQ/ZB6VLLOsyYlOm2m5KUqm3uueznQIL0ajVmEtGuIXCMWIRJNYTTqqywpryKPuAMHQXJ1aJUssrbPxk3+4bdHtO3+9Hact02noshkw5R3U4okUbk8w10IPx1PsfKmXp3Z2MTwSIBJNkkikc9+T0aihtdXF++5ayX/59NU8eP8Gmqqt5x2yJwFmvYryckOuozkYjDGR7SxfiFajwm7RYsv7jicmgoRCcWQJnE49TU1zE+e6ezO18fO1zi9k1BPmd7u62f1CL9PeKNFokmQynXtNu13P+nVV3H/vGr744Cbuvyszxj9/bkSpEoF+iQVCsXmBrlbJ1JabufaaJbkfyvBIgOOnJ+gdnsmEb5bFomV5g4OmWluu7qkoCtFogtePDePNziiEzC9WZ1DRUG8r2LmnpsN486bXKwrEYml+8Nhx/uGfD/L8i/1MTCzeKQbgcOi5cn0Nt25dmrvNqFVT5jJgehOTORLJNIMDPlIXCHStRqaq0pQb5x0Kn78lOUuWJcpdxtwIn0gkQSAQJxo///9n1Kupqywcdz7pDeMPxtDr1Viz5ZtZ3kC0YLilXqui0rn45KpkMs3MTBzPTGYmY5nTmJtQBBBPpjMdk3lBOOGP8uvdnXzlWy/z8A8P88qrg0xNReZNVjOZtFyxvpovfPpq7BYdizVWJUmi2mVCmzfhLBROMDW9+Hdv0qkptxW+L483RDiSQKvJlA3z5zB4JkPEipaW+H30jc3wr78+xZe/sYdHHj3JkaNugsH4vIaH06nnhusa+NMPXH5R8wze6USgX2L+UGLemFiTScMt72rGasuMCgDo6PZw/LSbSDTJ5OTcD8xq1bFhbTXNS525EkI0muKFl84xOhkmkffj1mtUlFn1udPpWT5fBH+2QzRfJJrkaMcEP3zsBF/8X3v4q6+/yHe/f4gdO7sZy57qz5IkifJyIxs3zNWRy3PrauRuIhiK8/SObn7yyHH+5afH5m2P/OIk+4+PzOtXKFZu02PUq3OfTzicYGLywoEuSWCz6ZHlzK6fSKQv6mzAYTdw2eqq3L/9/hjDkyH84ThajaogfAHiqXTBWHhZkpEViTeOuXPb6Ojc8gGRSJK+fh9K9hhQXm7KHQjj8RRebwxfIF7Q3asoCtF4iiF3kOf2D/J/Hz3KZ7/+Al/+mz385N+O09HpIZHIdLDqdGrq662sWeYqGMGST5KgusycGzUFEIokmMor2RUzGTS4imYBT3ojhKIJdBpVdjTV3H3J1FxL+g+Rzo6I6Rr08eSeHv7uhwf5zF8/x9e/9Qq/faqDoeHMUFpJkrBadWzZvIQqp6FgVnUpKu139w4QiCYIR5MFrSq9Xs26tVW5H5bbHaK9a4pBd4B4PMWkZy7QVSqJpkY71dmx54qSaa0+/2ofwaLFiAxaNeX2zESWfP5AjJlADL1WRWujg099aF1mu3cdH33fGhprrYyMBThyeoLn9g3wi6fP8p0fHKT97CTRvCGXOq0Kp9OQa/2XO4wYilpFoXCCnS/38szePnYssD33+jm6h3wk8spEC6l0GNFnyyYA4WiSyfMMrcuXPzRQr1djNKoLxuQXs5m0rGiws3z5XC34+Ak3Y+5gZpq5opAq6jS0GDQFLd1ILMnJnikefvQYDz96jH/59xN09c/1h4TCcTq6JnMlleoqc64zORRKMOrOdDTLEpTZ9FQ6DFQ6DDjMWmQJZkJxxj0hzg35OdI+wdN7+njsybMcO5EZgy5Jme+n0mkqWPIhnyRlJkzNDnvNLAcRZzq4+BBHk1FLuWuuIz6ZSDHljxKJZRZHK15wzGzSFoxQWcjG1VW8910t3H3LSu66oYWr1lQjZWdMl+e9d6tJS0pJ4w3EGJ0I0t3vY/+JMR57tpMnd3bT1Z35fGVZwmLW4swbAVOqzv/JCm+7ZEphJhArGL0iyxK2vNZ5+9kJOnoy42fj8dS8U2CjUZM7nQyF4pxqn6TjnLdg2jfZ0QhlC6yp4gtkhqXpNSqaaq3cesPSgm1tWwVWk5ZILMmkL0rfsJ83Trnx+efGPJNt7fr9sdywwYoyE8a8WaTptEIwGKdr0MuYJ4R7Kjxvm/BGCEWTF5wcVFluzo33BgjHMiNcLiStZKasz4a6VitT7jBSVTQxZpZWI9Pa7GLrpvrcmPNYLMWBE6OMZ2vL8Xhq3hIFyxoc2PPWSEmk0kz6Ipzp9nCm2wMKGA2ZoYdKWsHnj3K8YwJFAY1aorrKnCsnBUNxRscCSBIYdCruvauVT96/jk/ev447bmimvmpuhAlK5rMY84To7p9mfGKu/q0omVr8Yp+tLENdrS3XvxAMxJnyFpaOipnNWqoq5/oPJiYjzAQTJFMK8WSKcCxZUD5raXHitOsWndyztMbKLduaeN/tK7jrtuVce1VdZm0gWaKu3MyH3tuWe+83bmnEkdevoaQVAuEEg2Mz9J3zMjWdvz9k5hQs9t5LhQj0/wR8vhihRUZn+P0xjp+dYGA0cwqZSKSZPs9ysl5/lL37BglGEhT3YxqNGirKChfXikSSeAPZES5SppOrvNxYsK2/rIotV9SxstFJU7WVlY0Otl5ZT3WVGa12bheamYnR2eXJnRVUVZoxGubq56mUQiKRprbSQku9g+ULbMvqbDgs2gt2YFVXWXK12Xg8hT8Qw7/IZ5gvlUpzbsCbm/gkSRLLGuxsuaKOMqsu11ksSeAwa9mwqpKbtjWxJruOTiqlcPT4GKe7PcxkhzkGQjHGizpV119ezeWtFbjseqTsR5QJZDWrW8q4bftSljbYUakkvL4opzs89Ge/Y5tJi806N7s2HE4wMZEpJ6lVMtduqueWG5Zxyw3LuPG6pVzeWoEubzIZgEGror7Olgvb2TAfGg/M67OZJckSDfW23IFy2hvF45kbqVNMypb8Zs8Oyc4mDkcz30M8mWbaH2U8r/+lfomNq9bW0FBjRZNtqUvZuQHL6+3c8a5mNq6vpnmZg5pqM2kU+kf9SJKEy6rjus0Nufe+bVM9LQ2OeWecDrOWhjprbjJeKpXG44ng8UcL1ioqRWItl4v0dqzlMmvZEjvNjQ4cjsLhfQDHTozzwr5zDLozIzi0GpnqMhObr84sF5svGk3S0T3Fr3d0LLjc6JJqK9uurqe6eu4HOOYO8tqhYfqGMj8ap0PP1qvrC06LXU4j1RVmKp1GltbbWNtWyU3blrKkzpoLnWQyTd85H88+38PwRBAkePcNzTQ3OXLBm05nShMOk5a2ZhdtLfO3pUtsTE2GmA7G5nVw5bvzhmaWNtrR6dREIknGxoNMTUWoKjMtuBn0amaCcVAU0skUG9bWYLNmWn4Wiw6Xw4CkgN6oocJhpLHWyvpVldyybRkb12WmrCcSaQYG/Tz6+Gm6z3mJZfs+kqk0JqOW9asqc7V0l9OASa/O1NcNmTXR62usrFlRxi1bm9h0VR1Oh4FoNMmJ9gmefq6HIXcmtJuqrWzdVJ9b0GxoeIZX9g8y7o0gIXHjtU04bHrk7BowGq2KSDSBQafBYdVTXWFm1fIybri2kctWVaLTqYjFknR2T/Pk890Ew8l5U6+k7PDDj7xvNTabDkmS6D/n5fBxNwNjc0s75NOqZS5bWc72LQ2ZMw0FDh4a4fjZicySDQroNWoqnUaWLXVAdmmIMocRnVaFVqvCaTNQW2WhbbmL669u4Mbrl+KwZ9am6R/w8eLrA+w7NgoKWPUatl7TgNWSWahLrc5MMIrGU1gMWpx2PXVVFq5YVcX2LQ2saHEhyxKhUJy9rw3y6tGRRQ9mb5VLvZaLWA/9Ir3V66Hnu2lzI3ffuZJVbeUFt8fjKf7P9w+x5+BQbllbs0HDVasr+cZXtxc8FuDcgJ8ndnTyq12dxXchSXDNFXV8/hMbC9Z6OXh4lEd+e5qj7eNIEixdYuMrf76Z5Stci54WF0ul0rjHQ7z4yjl+9vgpwvEUarXEPz30Li5rq7hgzTSfZyrM1/72ZU6f8xJfpLNSJUv88/+8mRXNTrRaVbbUE2W6aLnWWamUwhunxvjRYyeIJ9KoVRJ/8bGNXLe5HpdTnxuTn0ymmZ6O4vdHcTr0WG36XD05FksxNOznt8908eyrffNmlNZVWbjntpW8+9YWtFpVriMwHk/h98eYCcQx6DXU1MydIUWjSbp6pnnmxR6e3duXq8Nfv6GOT3x0HY2NmeF++w4N851/PsjoVKal+4U/vZLt19bjzI6aSaXS+Hwxevu8BENxHHYDTY22XDAnEmnODfp49LHT7DkytGAnsEYt01Bt5nvfvCU3YmfPy+f45VNnONU7t4ZNPqdFx3tvaeHj962DbCft975/mF2vDzCV7WS3mTRsXlfDg5+4sqDBoiiZtcq93lhm/Z8qU+6zTiTSuMeD7Hihh9893403EEeSwKzT8Ldf3MqaVZn1d8h+L5OT4ewqm2mqKkwsWWLNdfxnDmRTfOfhg/SOFq7T/3YQ66ELBIIxIkWzEBVFYWDAz6neabx5nVKJZBrfAmuEp9MKQ6N+Dh4dLr4LALUsYbdqqSi6atDkZCg3CURRYGIyzGO/bScQyCw/er7DvaJk6pITk2Fe2NvP4zs6CMdTSBKUWfVYLRfuACuWTKUZmQot+sOTJQmbUUuZy5A7O9BoZMrKjCxf7lxwq6uzoNOrc6OJkimFf3+yneOn3ASDc+t/qNUyFRVGWlqcuMqMaDQyqZRCJJqku2eKn/3yFE/smVvWIJ97MsgTz3fR0TlFJDr3mlptpoS1bKk9F+aptEI8nuJU+yS/fPIMu1/pL+hUrao0585qFAVCsSSewNzB6tkXu+kf9BGPZ+rhKpWMy2Xgyo01XL+tkXVrK7HbM2vUJBJpBof8PLOrmxcPDSwY5sxeEctlLih1+YMx/Hn/bzGrSYujaLjmuCdUMLXeH0pwpH2C3zzVQTSaJJXKLG0sSRJ2u56mJhtLlljQaGTSaYVEIsXQkJ9f/qadp3Znwpzs5xCIJnhmdw8Dg5k14RUls3Z6XZ2FazbXsW1rPStXujCZNKTTCtFYkrOdHn7y8xN0DfsX3adKyZv7tQlvi1AoXjBahGxAP/t8D56pwvUuEqk003kTVmaNuYOcPOthKG8ETD6LQYPTPNfROmtqOkwob8p7MJrgleOjPPTNvbz86iD+mfn/16yJiTA7d/bw7e/u5+dPn2Uq+1hJkqh2GtEsMppiMalUmplAnMnsWOyFqFQSteXGNzVawe+PMpHXOQgwOhXmB/92lJ/+4gRdXdMFnbuz4vEU7Wcm+eFPj/GN/7OPvUeHYeE8JJlSGBgL8LV/fJXHfn2G4eEZEoss0drRMcX3f/wGf//Dg7x+dGTesNXaamuuBeqfiTExESaeN06+c9DH939ylGd2dhcMYS02Ohbkyac7+c7DB3hqTy/nWecsu0qmuWD/mAnHF2w8zLKYddishZ3sY97wvLVSpnxRHt/dyd/9wz7az3jm7etkz6Lc7iC/+PfTfP0fX2fna/34F5hNuufoEP/y6HFe2zdIYJEFxhKJNN3d0zzyi1P87+8f5EjHZPFDSpYouVykt7PkYjVpaKyxUp53EQhFUTjVPYV3gY4co17N1WuqkGQp14kXCCYYHg8wOrnwrD69Vqam3ExTdl3sWX1DfsY8IaJFC4TptSrsNgPlTj2VDiMOsw6NJlMnjcSTTPujuL0RfP4oM4E40XhmDe/Z55fZ9LQ0OArWSr+QdFohGE5w+PR48V05sizhNGtpbZ5bp/1CQuEEIxPBXI16llolYTFpcdkNVLkyI12MBg3JlEIoEmd0OsTkdBSfP0ownDjvyo+zZFnCYdHjcOqpKzNRZsusbAkQCMcZnbrwa65e5sKVvZRaPJ5idCJEb9G1OXUaFTarjkqXkWqXEadNj16rJp1WCMcSTPijTEyFmfZGmQnGzjtShWzJZUmlmfqaudmkfcN+BkYDi/ZlOK06llRZKJudMKXAgZNuwtHEvDM7WZIwGdQ47AZqy01UOgy5ZRvC0SQeX4SRqRA+X4yZUGzRgyHZ/d9h01NVlnnvNrMOjSazXHEwFMftizDpjeD1RQmE4ouelbwdLnXJRQT6RXo7A12WJTRqGXVR6zmyyJR7ScrMwsyfvj+71OtiLVtZyrTCiq8AH0+mzzvZQ51dQ1qnkbOTcRSSKYVoPEksb6p5MbVKQqueqyVfrFQ6M1nmfN7sa6cVhWRSKZhklU+WJLQaGaNOjVqdOWglskPuMkPdip9xYZKcGfev16hyqyImkinC0exrFj8hj147t3yyomTKUIv1J8x+P7PPUbJlq0gsRSyx8P6zEEnKhHr+/hFPnn/SlVoloVHJBa362YtznI9eo0KvU+XO4JKpNNF4ikg8WTAT9nyk7EHIoFOh1aiQZYl0OvMZR2Ip4pdoiKII9HeItzPQBUEoDZc60EUNXRAEoUSIQBcEQSgRItAFQRBKhAh0QRCEEiECXRAEoUSIQBcEQSgRItAFQRBKhBiHfpHEOPS33+wM1ZWNLpYtdWDQa/BMhznT7WFwbOGrxDstOpY1OGist2MwqJiainL05BiT/si8GbZ6rYrGait1tTYOHB8luMDU8lnVTiMrml0MjwfpGfAiS+Cw69m+qZF9h4aY8EZyszwloKrCzLUb6wouvg0wPhGkq2+a0aLrcq5ZXo5WLTPsDjC+wCXebCYNLY3O7PvSMDUd5vSZCYYmg/M+B71WRX2VhdWtFew9MIAvECeVVli9vByLQUPvgPei1ooX/nBiHLogZBl0alqXurh5axPNS2w01Vq5YXM9V15ehaXoEm8ADouOzetr2LqxjpWNDlY0Otl+VT23bVuam1aez2jQcMWqSjavrS248MZCltbauGZ9LfU1mYtHqNUy9VUW7r2zNbNee/4vRwKjXkVDlYWWJTbuvmMlV66pYlmdjQqHAf0CSxRsXl/DhrYKnHkXwZhlNmi4dmMdWzbU0trkpLXJwdYrlnD79csw6jXzZshajBrWriznnve0sWFVFXqdGrUsceXl1VxzRS3WBT47oTSJQL+EJCkz7V9VglvxImAXw2HVs2V9HS3NTl54tZ/Hnmqnb8iHXqvCWHQdTAlYt7KCTRvqSKUVdrzYw293dDDiDvDeO1ZS7jKgKvob9Fo1DUsczASiCy7Gla+6yoKSzixxS/a5VU4jJrMGXyg+7/njkyGe2NnB8y/3YjKpeePkGE/u7GDvoSFGpwpb5ypZorHBjkYjE1lgoaq2ZU62bWogmVbYtaeP3+zoYHBshttubqHaNf+6mEa9lppKKzarjlu2LaXMYcBq1FBdacRgVDNznjORhchS5m8sxa34YFhqRMnlIr0dJRejQU2Fy4gm76K8pUEhEIgz5Yuedy2QYo21Vu57zyoqK8w8+tRpBkcDRCIJorH568bo1DJf+exmfFMRduzpo3PQi16rYnWzk7//xk3814de4ESHp2BdmKY6G1984Cpe2tvPrv0DuSsOLeSz921AUhRePDDI6R4PFQ4DN26q56Ybmnnway8ws8B1NnUaFa0Ndr7x37fzN3//Gm+0j88r+5Atp/zdX1/PgcMjPPVcd26VSrIH+a98ahPRSJJn9/Zxpm8KWZa4fFU53/zSNr7xrVc40jFJJO99tS518cFbV1JTa6Gp0ca3/u9+JieCfOA9bXj8UR5+5Oi8FR3Px2nTYbVkFrwqNZNTYYKh+ILfy1vhUpdcRKBfpLc60GVZYt0yF5//7Cbq6wpXQHynS6bSPLuzh5892c54Uev0fMx6NWtXlPPAH1/BkjoLvX0+duzu4rU3RpjwF9aAGypMfPXz17L3wCBP7uomEElg0Kq4rKWMv/sfN/DgV57jTM907oAiSdDa7OLbX97Ot//xdY50TGYuu7eIb/+3bfT2TLP71X7OuQM0VFu5c/syypxGvv3jQ4SK1q8HMOnVXLeulg++fxX/8+EDdPbNXQQ638olNv7bX1zDE891sePFXhLZtdAlCXQ6mZ/9wx08s6OT3a8NMOGLoJIl1q2q4BtfvI6v/s0eTvVNE81bovaKNVXcc3srR46OsGZ1FaFQnL5z0yxvdtE7MsPPn2zP+98v7IO3rOSu25ezpC7vWqUl4p8ePsBLB4cKDqJvJRHo7xBvdaBL2WtWLltiw1BUTninUxSY9EY4Nxa44MqJ+SQJtGoVJouW+nITN17TRFWlhedf72fXK/0Fj13V5OALn9nEs3t6eWp3N/FEGqdNzy2bG7nt5ha+9M2XGXEHcov3GXQqNq6p4st/cQ2f+dIuBtzBBc8eJMCoU/G9b97MSy/3s/Plfib9UdqaXXz4jjZ6+6b5xa5OogscDGxmHR+4aTlNDXZ++KsTDIwsfOm2rWtr+KM7W3niuW5ePjCY+xtlWcJl0/KDb9/Gzx87yYv7h/CF4riserZftYR7/mgVD3xxJ96ZeO5sRZJh29X1/PH7V/PDf32DpKLw2U9eic8bY2o6yr6jw+x6tfCzu5D6SjNVLiP6vItwl4q+kRkmvZHc5QPfaiLQ3yHe6kAnW0vVauQLXhD5nShz1feLX8K0wmHg6strWNVWzj/9+DCySuKDt7bS0uhkz8FBdr9WGEp15Ua+9oVr6eye5oldXXhnoqxvq+Du97Tx8t5zPPNqf0FJxWnRcd3GOu675zL+5PM7mAnG540WIXtQsRo0fO+bN3P06Fj2GpwJbthUzy3vauZ7PzrMsS7PgkvxljkMPPiR9UxOhnn8hS7cRSNbyL7+3TeuoHV5GU+/3Msbp90F9+n1Kn74v27l+Ak3O/b0MjEdZsOqKm7Y0sjAoJ8f//ZkQfnErFdz83VN3HXbcr78t3vwzsT5/J9dxdrVlXR2eXhidzcHT43lHn8xNGoZtUrm9+gG+U/vQstF/6FEoL9DvB2BLsyxW7Rs2VDLR96/hrO9U6TSaWxGPWc6J3nl8NC8CzzotSo+dEcrl7dVkALCkQSkFMbcAZ54vocJb7igTlpbbuaO7Uv5o/e1se+NEVLZS+e9fGCQsz2F18zUqmX+/L71LF/mJBzL1PClFPQN+Pj1zk5mwokFD1TVFWa+/aVtPL2zkxcODjK9wGm9JMFf/vFG0sk0u17rp6O/sCwjyxL33dHK+suqSSpporEk6aTC8EiA517r51zR8M0qh4Hbti1l48Y6/vKhF4jGU2zdUMuffmQtvb3T/OrpDjoGvPn/hfA2utSBrnrooYceKr5RmO/HvzxZfJPwFspclCFJKJpEBsLhBF39Xo6ccjM0Fph34Y5kSsHnjxLNXsQhEIzTec7L64eGGZoIki5qQKvVEmqVjC8QJRxOEI0m8fmiDI7MMO0vvG5mKq0wMxMjnkhlLsDsj9LePcW+IyNMeBcfz62SJYwGNW+0u5mcnj8OnmxJx2LW0j/sY3AsQKSodKMoMO2NEEulSKcUAqE4Xf1eDh4fpW/EP69lqVHLIMHYZIj27syByR+IEYun6Oqbpm/IRyTvalTC2+vj915efNN/KNFCv0iihf4fQMpcBcdq0JJKpwnGkgUXT16IVi3nxnkHY0nSC4Qo2bDVaTOlhFnplEI0kTkFX4hBq0KvVZFMK4RjmQscn48sS5gNasKxzIFgsV+WUa8mnU4TTyqLXlFIrZYw6jQoikIkllzw4ED2fWnUMpJEQXAbdCrSSuai4ov9H8Jb71K30EWgXyQR6IIgXMilDnQxsUgQBKFEiEAXBEEoESLQBUEQSoQIdEEQhBIhAl0QBKFEiEAXBEEoESLQBUEQSoQYhy4IglAiRAtdEAShRIhAFwRBKBEi0AVBEEqECHRBEIQSIQJdEAShRIhAFwRBKBEi0AVBEEqECHRBEIQSIQJdEAShRIhAFwRBKBEi0AVBEEqECHRBEIQSIQJdEAShRIhAFwRBKBEi0AVBEEqECHRBEIQSIQJdEAShRIhAFwRBKBEi0AVBEEqECHRBEIQSIQJdEAShRIhAFwRBKBEi0AVBEEqECHRBEIQSIQJdEAShRIhAFwRBKBEi0AVBEEqECHRBEIQSIQJdEAShRIhAFwRBKBEi0AVBEEqECHRBEIQSIQJdEAShRIhAFwRBKBEi0AVBEEqECHRBEIQSIQJdEAShRIhAFwRBKBEi0AVBEEqECHRBEIQSIQJdEAShRPw/Cei3DRVlackAAAAASUVORK5CYII='; let FCL_ROWS = []; const FCL_TERMS = `* All imports subject to Customs and Quarantine Duties, Inspections, Fees and taxes where applicable. * Above does not include any storage, insurance, wharf/CFS detention. * Payment terms: Import customs clearance charges must be paid in full prior to commencement of customs clearance. * Clearance will not proceed without payment. * Freight and delivery charges must be paid in full prior to dispatch of the cargo from our warehouse to the final destination. * The declared customs value may be subject to variation, and the exact amount can only be confirmed at the time of import declaration. * This quotation is provided as an estimate only. * Pickup at origin is not included and is not available.`; const FCL_DEFAULTS = { '20FT': [ {section:'Origin Charge', desc:'OCEAN FREIGHT\n20FT', qty:1, cost:3500, costCur:'USD', sell:5762.12, sellCur:'AUD', gst:'GST Free', fixed:false, show:true, fxMultiplier:1.0865714286}, {desc:'Origin TRML Handling Fee', qty:1, cost:160345, costCur:'KRW', sell:168.28, sellCur:'AUD', gst:'GST Free', fixed:false, show:true, fxMultiplier:1.03}, {desc:'THC (KRW)', qty:1, cost:150000, costCur:'KRW', sell:157.42, sellCur:'AUD', gst:'GST Free', fixed:false, show:false, fxMultiplier:1.03}, {desc:'WHARFAGE (KRW)', qty:1, cost:10000, costCur:'KRW', sell:10.49, sellCur:'AUD', gst:'GST Free', fixed:false, show:false, fxMultiplier:1.03}, {desc:'PORT SECURITY FEE (KRW)', qty:1, cost:86, costCur:'KRW', sell:0.09, sellCur:'AUD', gst:'GST Free', fixed:false, show:false, fxMultiplier:1.03}, {desc:'PORT SAFETY (KRW)', qty:1, cost:259, costCur:'KRW', sell:0.27, sellCur:'AUD', gst:'GST Free', fixed:false, show:false, fxMultiplier:1.03}, {desc:'Seal Charge', qty:1, cost:10000, costCur:'KRW', sell:10.49, sellCur:'AUD', gst:'GST Free', fixed:true, show:true, fxMultiplier:1.03}, {desc:'Doc Fee', qty:1, cost:50000, costCur:'KRW', vat10:true, sell:57.72, sellCur:'AUD', gst:'GST Free', fixed:true, show:true, fxMultiplier:1.03}, {desc:'Operation handling fee', qty:1, cost:0, costCur:'AUD', sell:52.47, sellCur:'AUD', gst:'GST Free', fixed:true, show:true, fxMultiplier:null}, {desc:'Container Packing (if applicable)', qty:1, cost:0, costCur:'KRW', sell:0, sellCur:'AUD', gst:'', fixed:true, show:true, tba:false, infoOnly:'packing', fxMultiplier:1}, {desc:'Trucking Charge(Fuel Floating)', qty:1, cost:980000, costCur:'KRW', vat10:true, sell:1129.85, sellCur:'AUD', gst:'GST Free', fixed:false, show:true, fxMultiplier:1.03}, {desc:'Trucking Charge (KRW)+(Stuffing Charge)', qty:1, cost:980000, costCur:'KRW', vat10:true, sell:1129.85, sellCur:'AUD', gst:'GST Free', fixed:false, show:false, fxMultiplier:1.03}, {section:'Destination Charge', desc:'DEST TRML HANDLG', qty:1, cost:666.00, costCur:'AUD', sell:766.00, sellCur:'AUD', gst:'GST Free', fixed:false, show:true, fxMultiplier:1.15015015}, {desc:'DEST. DOC FEE', qty:1, cost:115.00, costCur:'AUD', sell:130.00, sellCur:'AUD', gst:'GST Free', fixed:true, show:true, fxMultiplier:1.13043478}, {desc:'Delivery order fee', qty:1, cost:0, costCur:'AUD', sell:70.00, sellCur:'AUD', gst:'GST Free', fixed:true, show:true, fxMultiplier:null}, {desc:'Sea cargo report automation', qty:1, cost:0, costCur:'AUD', sell:70.00, sellCur:'AUD', gst:'GST Free', fixed:true, show:true, fxMultiplier:null}, {section:'Destination Clearance', desc:'Customs Clearance', qty:1, cost:90.00, costCur:'AUD', vat10:true, sell:200.00, sellCur:'AUD', gst:'10%', fixed:true, show:true, fxMultiplier:2.02020202}, {desc:'TARIFF LINE', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'', fixed:true, show:false, tba:true}, {desc:'IOR-TNS', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'', fixed:true, show:false, tba:true}, {desc:'IMPORT GST_AT COST\nCIF X 10%\nTBA', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'', fixed:true, show:true, atcost:true}, {desc:'ENTRY CHARGE_AT COST\nTBA', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'', fixed:true, show:true, atcost:true}, {desc:'DUTY (If occurred)_AT COST', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'', fixed:true, show:true, atcost:true}, {section:'Cartage / Unpack', desc:'BNE port To WISE BNE WH\nCartage\n20FT Side loader (Fuel Floating)', qty:1, cost:1234.14, costCur:'AUD', vat10:true, sell:1649.14, sellCur:'AUD', gst:'10%', fixed:false, show:true, fxMultiplier:1.21419944}, {desc:'Unpack 20ft Container\n20FT (PLT) : A$300+GST\n20FT (Loose) : A$550+GST\nTBA', qty:1, cost:0, costCur:'AUD', sell:300.00, sellCur:'AUD', gst:'10%', fixed:false, show:true, fxMultiplier:null}, {section:'Final Delivery', desc:'Final Delivery Address :', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'10%', fixed:false, show:true, fxMultiplier:1}, {section:'Optional Handling', desc:'Local Delivery', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'10%', fixed:false, show:false}, {desc:'Labelling Fee - $0.8/Label', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'10%', fixed:false, show:false}, {desc:'Re-palletizing - $33.5/Plt', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'10%', fixed:false, show:false} ], '40FT': [ {section:'Origin Charge', desc:'OCEAN FREIGHT\n40FT', qty:1, cost:3500, costCur:'USD', sell:5524.64, sellCur:'AUD', gst:'GST Free', fixed:false, show:true, fxMultiplier:1.0891428571}, {desc:'Origin TRML Handling Fee', qty:1, cost:220674, costCur:'KRW', sell:223.32, sellCur:'AUD', gst:'GST Free', fixed:false, show:true, fxMultiplier:1.03}, {desc:'THC (KRW)', qty:1, cost:210000, costCur:'KRW', sell:212.52, sellCur:'AUD', gst:'GST Free', fixed:false, show:false, fxMultiplier:1.03}, {desc:'WHARFAGE (KRW)', qty:1, cost:10000, costCur:'KRW', sell:10.12, sellCur:'AUD', gst:'GST Free', fixed:false, show:false, fxMultiplier:1.03}, {desc:'PORT SECURITY FEE (KRW)', qty:1, cost:172, costCur:'KRW', sell:0.17, sellCur:'AUD', gst:'GST Free', fixed:false, show:false, fxMultiplier:1.03}, {desc:'PORT SAFETY (KRW)', qty:1, cost:502, costCur:'KRW', sell:0.51, sellCur:'AUD', gst:'GST Free', fixed:false, show:false, fxMultiplier:1.03}, {desc:'Seal Charge', qty:1, cost:10000, costCur:'KRW', sell:10.12, sellCur:'AUD', gst:'GST Free', fixed:true, show:true, fxMultiplier:1.03}, {desc:'Doc Fee', qty:1, cost:50000, costCur:'KRW', vat10:true, sell:55.68, sellCur:'AUD', gst:'GST Free', fixed:true, show:true, fxMultiplier:1.03}, {desc:'Operation handling fee', qty:1, cost:0, costCur:'AUD', sell:50.60, sellCur:'AUD', gst:'GST Free', fixed:true, show:true, fxMultiplier:null}, {desc:'Container Packing (if applicable)', qty:1, cost:0, costCur:'KRW', sell:0, sellCur:'AUD', gst:'', fixed:true, show:true, tba:false, infoOnly:'packing', fxMultiplier:1}, {desc:'Trucking Charge(Fuel Floating)', qty:1, cost:1100000, costCur:'KRW', vat10:true, sell:1224.50, sellCur:'AUD', gst:'GST Free', fixed:false, show:true, fxMultiplier:1.03}, {desc:'Trucking Charge (KRW)+(Stuffing Charge)', qty:1, cost:1100000, costCur:'KRW', vat10:true, sell:1224.50, sellCur:'AUD', gst:'GST Free', fixed:false, show:false, fxMultiplier:1.03}, {section:'Destination Charge', desc:'DEST TRML HANDLG', qty:1, cost:917.00, costCur:'AUD', sell:1017.00, sellCur:'AUD', gst:'GST Free', fixed:false, show:true, fxMultiplier:1.10905125}, {desc:'DEST. DOC FEE', qty:1, cost:115.00, costCur:'AUD', sell:130.00, sellCur:'AUD', gst:'GST Free', fixed:true, show:true, fxMultiplier:1.13043478}, {desc:'Delivery order fee', qty:1, cost:0, costCur:'AUD', sell:70.00, sellCur:'AUD', gst:'GST Free', fixed:true, show:true, fxMultiplier:null}, {desc:'Sea cargo report automation', qty:1, cost:0, costCur:'AUD', sell:70.00, sellCur:'AUD', gst:'GST Free', fixed:true, show:true, fxMultiplier:null}, {section:'Destination Clearance', desc:'Customs Clearance', qty:1, cost:90.00, costCur:'AUD', vat10:true, sell:200.00, sellCur:'AUD', gst:'10%', fixed:true, show:true, fxMultiplier:2.02020202}, {desc:'TARIFF LINE', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'', fixed:true, show:false, tba:true}, {desc:'IOR-TNS', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'', fixed:true, show:false, tba:true}, {desc:'IMPORT GST_AT COST\nCIF X 10%\nTBA', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'', fixed:true, show:true, atcost:true}, {desc:'ENTRY CHARGE_AT COST\nTBA', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'', fixed:true, show:true, atcost:true}, {desc:'DUTY (If occurred)_AT COST', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'', fixed:true, show:true, atcost:true}, {section:'Cartage / Unpack', desc:'BNE port To WISE BNE WH\nCartage\n40FT Side loader (Fuel Floating)', qty:1, cost:1274.14, costCur:'AUD', vat10:true, sell:1689.14, sellCur:'AUD', gst:'10%', fixed:false, show:true, fxMultiplier:1.20589715}, {desc:'Unpack 40ft Container\n40FT (PLT) : A$420+GST\n40FT (Loose) : A$800+GST\nTBA', qty:1, cost:0, costCur:'AUD', sell:420.00, sellCur:'AUD', gst:'10%', fixed:false, show:true, fxMultiplier:null}, {section:'Final Delivery', desc:'Final Delivery Address :', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'10%', fixed:false, show:true, fxMultiplier:1}, {section:'Optional Handling', desc:'Local Delivery', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'10%', fixed:false, show:false}, {desc:'Labelling Fee - $0.8/Label', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'10%', fixed:false, show:false}, {desc:'Re-palletizing - $33.5/Plt', qty:1, cost:0, costCur:'AUD', sell:0, sellCur:'AUD', gst:'10%', fixed:false, show:false} ] }; const FCL_TEMPLATE_META = { '20FT_STRA': {container:'20FT', wh:'STRA', label:'20FT - SYD STRA WH', cartCost:1234.14, cartSell:1649.14, unpackSell:300}, '20FT_WETHERILL': {container:'20FT', wh:'WETHERILL', label:'20FT - SYD WETHERILL WH', cartCost:1365.64, cartSell:1780.64, unpackSell:300}, '40FT_STRA': {container:'40FT', wh:'STRA', label:'40FT - SYD STRA WH', cartCost:1274.14, cartSell:1689.14, unpackSell:420}, '40FT_WETHERILL': {container:'40FT', wh:'WETHERILL', label:'40FT - SYD WETHERILL WH', cartCost:1274.14, cartSell:1689.14, unpackSell:420} }; const FCL_BRANCHES = { STRA: {label:'WISE EXPRESS (STRA)', prefix:'STRA', address:['Suite 41/50 Cosgrove Rd','Strathfield South NSW','2136']}, WETHERILL: {label:'WISE EXPRESS (WETHERILL)', prefix:'WETHRILL', address:['181 Newton Rd Wetherill Park NSW 2164']}, GIMPO: {label:'WISE EXPRESS (GIMPO)', prefix:'GIMPO', address:['99-46, Gahyeon-ro 85beon-gil, Tongjin-eup, Gimpo-si, Gyeonggi-do, Republic of Korea']} }; function fclPrefixQuoteNo(q, branchKey){ const branch=FCL_BRANCHES[branchKey]||FCL_BRANCHES.STRA; const prefix=branch.prefix||String(branchKey||'STRA'); let v=String(q||'TBA').trim(); if(!v) v='TBA'; const safePrefix=String(prefix).replace(/[.*+?^${}()|\[\]\\]/g,'\\$&'); const re=new RegExp('^'+safePrefix+'_','i'); return re.test(v)?v:(prefix+'_'+v); } function fclCustomerAttentionName(d){ const c=String((d&&d.customer)||'').trim(); const a=String((d&&d.attention)||'').trim(); if(c&&a) return c+'_'+a; return c||a||'KLN'; } function fclCompanyBlock(d){ const branch=FCL_BRANCHES[(d&&d.branch)||'STRA']||FCL_BRANCHES.STRA; const mob=String((d&&d.mob)||'+61 421 257 281').trim(); const email=String((d&&d.email)||'cs@wiseexpress.com.au').trim(); const addr=(branch.address||[]).map(x=>fclEsc(x)).join('
'); return 'WISE EXPRESS
TEL : +61 2 8004 7899
MOB : '+fclEsc(mob)+'
'+fclEsc(email)+'
'+addr; } function fclN(v){ let s=String(v??'').replace(/,/g,'').trim(); if(!s)return 0; if(/^[0-9+\-*/().\s]+$/.test(s) && /[+\-*/]/.test(s.replace(/^[-+]/,''))){ try{let n=Function('"use strict";return ('+s+')')();return isNaN(n)||!isFinite(n)?0:Number(n);}catch(e){} } let n=parseFloat(s);return isNaN(n)||!isFinite(n)?0:n; } function fclNumText(v){let n=fclN(v); if(!isFinite(n))return '0'; return String(Math.round(n*1000000)/1000000);} function fclCommitFormulaInput(el){if(!el)return;let k=el.dataset&&el.dataset.k;if(k==='cost'||k==='sell'||k==='qty'){el.value=fclNumText(el.value);}} function fclAud(x){return '$'+Number(x||0).toLocaleString('en-AU',{minimumFractionDigits:2,maximumFractionDigits:2})} function fclPct(x){return (Number(x||0)*100).toFixed(2)+'%'} function fclEsc(s){return String(s??'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))} function fclCurOptions(sel){return ['AUD','KRW','USD'].map(x=>``).join('')} function fclDateDisplay(v){if(!v)return '';let d=new Date(v+'T00:00:00');return isNaN(d)?v:d.toLocaleDateString('en-AU',{day:'2-digit',month:'short',year:'numeric'})} function fclRouteTextFromParts(pod, wh, container){ pod=String(pod||'').toUpperCase(); wh=wh||'STRA'; container=container||'20FT'; let port = pod.includes('SYD')?'SYD PORT':pod.includes('MEL')?'MEL PORT':pod.includes('BNE')?'BNE PORT':(pod?pod+' PORT':'DEST PORT'); let whText = pod.includes('SYD') ? `WISE WH (${wh})` : pod.includes('BNE') ? 'WISE BNE WH' : pod.includes('MEL') ? 'WISE MEL WH' : 'WISE WH'; return `${port} TO ${whText} Cartage ${container} Side loader (Fuel Floating)`; } function fclInitRow(r){ if(r.qty===undefined || r.qty===null || r.qty==='') r.qty=1; r.vat10 = (r.vat10!==undefined) ? !!r.vat10 : (fclN(r.costVat)>0); r.gst10 = (r.gst10!==undefined) ? !!r.gst10 : (r.gst==='10%'); // plus3는 사용자가 각 항목의 [+3% 적용] 버튼을 누른 경우에만 true입니다. // 기존 기본 데이터에 남아 있던 fxMultiplier 값은 자동 3%로 취급하지 않습니다. if(r.plus3===undefined) r.plus3 = false; if(r.fxMultiplier===undefined || r.plus3!==true) r.fxMultiplier = r.plus3 ? 1.03 : 1; if(r.autoFx===undefined) r.autoFx = true; r.tba=!!r.tba; r.atcost=!!r.atcost; if(r.atcost)r.tba=false; return r; } function fclBaseDefaults(key){ const m=FCL_TEMPLATE_META[key]||FCL_TEMPLATE_META['20FT_STRA']; const rows=JSON.parse(JSON.stringify(FCL_DEFAULTS[m.container])); rows.forEach(r=>{ fclInitRow(r); // 기본 템플릿 로딩 시에는 어떤 항목도 +3%가 켜져 있지 않게 초기화합니다. r.plus3=false; r.fxMultiplier=1; r.autoFx=true; if(fclIsPackingRow(r)){ r.infoOnly='packing'; r.desc='Container Packing (if applicable)'; r.qty=1; r.cost = r.cost || 0; r.sell = r.sell || 0; r.costCur = r.costCur || 'KRW'; r.sellCur = r.sellCur || 'AUD'; r.vat10 = !!r.vat10; r.gst10 = !!r.gst10; r.gst = r.gst10 ? '10%' : 'GST Free'; r.tba = false; r.fixed = !!r.fixed; r.show = true; } if(/Side loader|BNE port|Cartage\n/i.test(r.desc||'')){ r.routeCartage=true; r.desc=fclRouteTextFromParts('AUSYD',m.wh,m.container); r.cost=m.cartCost; r.costCur='AUD'; r.vat10=true; r.sell=m.cartSell; r.sellCur='AUD'; r.gst10=true; r.gst='10%'; r.show=true; r.fixed=false; r.fxMultiplier=1; r.plus3=false; r.autoFx=true; } if(/^Unpack/i.test(r.desc||'')){ r.desc=`Unpack ${m.container} Container ${m.container} (PLT) : A${fclAud(m.container==='20FT'?300:420)}+GST ${m.container} (Loose) : A${fclAud(m.container==='20FT'?550:800)}+GST TBA`; r.qty=1; r.sell=m.unpackSell; r.cost=0; r.vat10=false; r.sellCur='AUD'; r.gst10=true; r.gst='10%'; r.show=true; r.tba=false; r.fixed=false; } if(/^Final Delivery Address/i.test(r.desc||'')){r.sell=0; r.cost=0; r.vat10=false; r.gst10=true; r.gst='10%'; r.show=true; r.fixed=false;} }); return rows; } function fclRouteLabel(d){ const key=(d&&d.template)||($('fclTemplate')&&$('fclTemplate').value)||'20FT_STRA'; const m=FCL_TEMPLATE_META[key]||FCL_TEMPLATE_META['20FT_STRA']; return fclRouteTextFromParts((d&&d.pod)||($('fclPod')&&$('fclPod').value)||'AUSYD',m.wh,m.container); } function fclPackingGuide(container){ const c=String(container||($('fclContainer')&&$('fclContainer').value)||'20FT').toUpperCase().includes('40')?'40FT':'20FT'; if(c==='40FT'){ return [ '내부 참고 / 금액 입력 시 고객 견적서 표시', '40FT Forklift Handling: KRW 300,000 / CNTR', '40FT Manual Handling: KRW 470,000 / CNTR', '포크 작업이면 Forklift 금액, 루즈/수작업이면 Manual 금액 참고 후 필요한 청구 항목에 입력' ]; } return [ '내부 참고 / 금액 입력 시 고객 견적서 표시', '20FT Forklift Handling: KRW 200,000 / CNTR', '20FT Manual Handling: KRW 330,000 / CNTR', '포크 작업이면 Forklift 금액, 루즈/수작업이면 Manual 금액 참고 후 필요한 청구 항목에 입력' ]; } function fclIsPackingRow(r){return !!r && (r.infoOnly==='packing' || /^Container Packing/i.test(String(r.desc||'')));} function fclIsOriginTrmlRow(r){return !!r && /^Origin TRML Handling Fee/i.test(String(r.desc||''));} function fclUpdateRouteRow(){ const key=($('fclTemplate')&&$('fclTemplate').value)||'20FT_STRA'; const m=FCL_TEMPLATE_META[key]||FCL_TEMPLATE_META['20FT_STRA']; FCL_ROWS.forEach(r=>{if(r.routeCartage){r.desc=fclRouteLabel({template:key,pod:($('fclPod')&&$('fclPod').value)||'AUSYD'}); r.cost=m.cartCost; r.costCur='AUD'; r.vat10=true; r.sell=m.cartSell; r.sellCur='AUD'; r.gst10=true; r.gst='10%';}}); } function isFcl(){return $('quoteType') && $('quoteType').value==='FCL'} function fclToAud(v,cur,d){v=fclN(v);cur=cur||'AUD';if(cur==='KRW')return v/d.appliedRate;if(cur==='USD')return v/d.usdRate;return v;} function fclRowCostBaseAud(r,d){return fclToAud(r.cost,r.costCur,d)} function fclRowCostAud(r,d){let base=fclRowCostBaseAud(r,d);return base*(r.vat10?1.1:1)} function fclRowSellAud(r,d){let base=fclToAud(r.sell,r.sellCur,d);return (r.gst10 && (r.sellCur||'AUD')!=='AUD') ? base*1.1 : base} function fclRowGstAud(r,d){return (r.gst10 && (r.sellCur||'AUD')==='AUD' && !r.tba && !r.atcost) ? fclRowSellAud(r,d)*0.1 : 0} function fclCollect(){fclReadRows();let rate=fclN($('fclNaverRate')?.value);let applied=Math.max(rate-10,1);let usd=fclN($('fclUsdRate')?.value)||0.66;let key=$('fclTemplate')?$('fclTemplate').value:'20FT_STRA';let meta=FCL_TEMPLATE_META[key]||FCL_TEMPLATE_META['20FT_STRA'];let branchKey=($('fclBranch')&&$('fclBranch').value)||'STRA';return {type:'FCL',template:key,warehouse:meta.wh,branch:branchKey,branchLabel:(FCL_BRANCHES[branchKey]||FCL_BRANCHES.STRA).label,container:$('fclContainer').value,customer:$('fclCustomer').value,attention:$('fclAttention').value,issueDate:$('fclIssueDate').value,expiryDate:$('fclExpiryDate').value,issuedBy:$('fclIssuedBy').value,quoteNo:$('fclQuoteNo').value,quoteNoDisplay:fclPrefixQuoteNo($('fclQuoteNo').value,branchKey),reference:$('fclReference').value,pol:$('fclPol').value,pod:$('fclPod').value,mbl:$('fclMbl').value,pcs:$('fclPcs').value,finalAddress:$('fclFinalAddress').value,mob:($('fclMob')&&$('fclMob').value)||'+61 421 257 281',email:($('fclEmail')&&$('fclEmail').value)||'cs@wiseexpress.com.au',naverRate:rate,appliedRate:applied,usdRate:usd,targetMargin:fclN($('fclTargetMargin').value)/100,targetTotal:fclN($('fclTargetTotal').value),rows:FCL_ROWS};} function fclCalc(){let d=fclCollect();let cost=d.rows.reduce((s,r)=>s+(r.show?fclRowCostAud(r,d):0),0);let revenue=d.rows.reduce((s,r)=>s+(r.show&&!r.atcost&&!r.tba?fclRowSellAud(r,d):0),0);let gst=d.rows.reduce((s,r)=>s+(r.show?fclRowGstAud(r,d):0),0);let totalAud=revenue+gst;let profit=revenue-cost;let margin=revenue>0?profit/revenue:0;return {d,cost,revenue,gst,totalAud,profit,margin};} function fclApplyTemplate(){let key=$('fclTemplate').value;let m=FCL_TEMPLATE_META[key]||FCL_TEMPLATE_META['20FT_STRA'];$('fclContainer').value=m.container;if($('fclBranch')&&$('fclBranch').value!=='GIMPO')$('fclBranch').value=(m.wh==='WETHERILL'?'WETHERILL':'STRA');FCL_ROWS=fclBaseDefaults(key);fclRenderRows();fclUpdateAll();} function fclSyncTemplateFromContainer(){let c=$('fclContainer').value;let old=$('fclTemplate').value;let wh=(FCL_TEMPLATE_META[old]||{wh:'STRA'}).wh;$('fclTemplate').value=c+'_'+wh;fclApplyTemplate();} function fclBranchChanged(){ // Issue Office / Address 선택 즉시 FCL 미리보기의 주소와 Quote Number prefix를 갱신합니다. // FCL Type(20FT/40FT, STRA/WETHERILL cartage cost)은 사용자가 따로 선택한 값을 유지합니다. if($('fclBranch')){ const b=$('fclBranch').value; // WETHERILL/STRA 선택 시 FCL Type의 창고 suffix를 맞춰 주되, GIMPO는 발행 오피스만 바꾸고 운송 타입은 유지합니다. if(b==='STRA' || b==='WETHERILL'){ const cont=($('fclContainer')&&$('fclContainer').value)||((($('fclTemplate')&&$('fclTemplate').value)||'20FT_STRA').startsWith('40FT')?'40FT':'20FT'); const next=cont+'_'+b; if($('fclTemplate') && FCL_TEMPLATE_META[next]) $('fclTemplate').value=next; } } fclUpdateAll(); updateAll(); } function fclPodChanged(){fclUpdateRouteRow();fclRenderRows();fclUpdateAll();} function fclResetRows(){FCL_ROWS=fclBaseDefaults($('fclTemplate')?$('fclTemplate').value:'20FT_STRA');} function fclResetAll(){ if(!$('fclForm'))return; $('fclCustomer').value='';$('fclAttention').value='';$('fclIssueDate').value=today();$('fclExpiryDate').value=addDays(7);$('fclIssuedBy').value='Gibaek Kim';$('fclQuoteNo').value='TBA';$('fclReference').value='FCL_QUOTE';$('fclBranch').value='STRA';$('fclMob').value='+61 421 257 281';$('fclEmail').value='cs@wiseexpress.com.au';$('fclTemplate').value='20FT_STRA';$('fclContainer').value='20FT';$('fclPol').value='KRPUS';$('fclPod').value='AUSYD';$('fclMbl').value='TBA';$('fclPcs').value='TBA';$('fclFinalAddress').value='';$('fclNaverRate').value='1062.32';$('fclUsdRate').value='0.66';$('fclTargetMargin').value='20';$('fclTargetTotal').value='';fclResetRows();fclRenderRows();fclUpdateAll(); } function fclLoadInput(d){ if(!$('fclForm'))throw new Error('FCL 입력폼이 아직 준비되지 않았습니다.'); d=d||{}; quoteEditorImportContext(quoteEditorContextFromFcl(d),d.manualQuoteEdits); if($('quoteType')) $('quoteType').value='FCL'; setFieldValue('fclCustomer',d.customer);setFieldValue('fclAttention',d.attention);setFieldValue('fclIssueDate',d.issueDate);setFieldValue('fclExpiryDate',d.expiryDate);setFieldValue('fclIssuedBy',d.issuedBy); setFieldValue('fclQuoteNo',d.quoteNo);setFieldValue('fclReference',d.reference);setFieldValue('fclMob',d.mob);setFieldValue('fclEmail',d.email); if($('fclBranch')) $('fclBranch').value=d.branch||'STRA'; if($('fclTemplate')) $('fclTemplate').value=d.template||'20FT_STRA'; if($('fclContainer')) $('fclContainer').value=d.container||((String(d.template||'').startsWith('40FT'))?'40FT':'20FT'); setFieldValue('fclPol',d.pol);setFieldValue('fclPod',d.pod);setFieldValue('fclMbl',d.mbl);setFieldValue('fclPcs',d.pcs);setFieldValue('fclFinalAddress',d.finalAddress); setFieldValue('fclNaverRate',d.naverRate||'1062.32');setFieldValue('fclUsdRate',d.usdRate||'0.66');setFieldValue('fclTargetMargin',d.targetMargin!==undefined?Number(d.targetMargin)*100:'20');setFieldValue('fclTargetTotal',d.targetTotal||''); FCL_ROWS=Array.isArray(d.rows)?JSON.parse(JSON.stringify(d.rows)).map(fclInitRow):fclBaseDefaults(($('fclTemplate')&&$('fclTemplate').value)||'20FT_STRA'); FCL_ROWS.forEach(r=>{r.autoFx=false;if(fclIsPackingRow(r)){r.infoOnly='packing';r.show=true;r.gst=r.gst10?'10%':'GST Free';}}); fclRenderRows(); fclUpdateAll(); } function fclAddRow(){ FCL_ROWS.push(fclInitRow({section:'Optional',desc:'New Charge',qty:1,cost:0,costCur:'AUD',sell:0,sellCur:'AUD',gst10:false,fixed:false,show:true})); fclRenderRows();fclUpdateAll(); setTimeout(()=>{const inputs=$('fclRows')?.querySelectorAll('.fcl-row-name-input');const input=inputs&&inputs[inputs.length-1];if(input){input.focus();input.select();}},0); } function fclSetReceivableFromCost(r,d,multiplier){ fclInitRow(r); if(r.atcost || r.tba || !r.show) return false; let costAud=fclRowCostAud(r,d); if(!(costAud>0)) return false; let mult=(typeof multiplier==='number' && multiplier>0)?multiplier:1; r.plus3 = Math.abs(mult-1.03)<0.000001; r.fxMultiplier = r.plus3 ? 1.03 : 1; r.autoFx=true; r.sell=Number((costAud*mult).toFixed(2)); r.sellCur='AUD'; r.gst10=false; r.gst='GST Free'; return true; } function fclRefreshLinkedReceivables(){ // 자동 환산 모드(autoFx !== false)인 항목만 현재 환율로 다시 계산합니다. // +3% 적용 버튼을 누른 항목은 fxMultiplier=1.03을 유지합니다. let d=fclCollect(); fclApplyCurrentRateToRows(d,false); } function fclRefreshCardStats(card,r,d){ if(!card||!r)return; let cost=fclRowCostAud(r,d), sell=fclRowSellAud(r,d), profit=sell-cost, margin=sell?profit/sell:0; let pay=card.querySelector('[data-stat="payable"]'); if(pay)pay.textContent=fclAud(cost); let rec=card.querySelector('[data-stat="receivable"]'); if(rec)rec.textContent=fclAud(sell); let proCell=card.querySelector('[data-cell="profit"]'); let pro=card.querySelector('[data-stat="profit"]'); if(pro)pro.textContent=fclAud(profit); if(proCell){proCell.classList.toggle('good',profit>=0);proCell.classList.toggle('bad',profit<0);} let mar=card.querySelector('[data-stat="margin"]'); if(mar)mar.textContent=sell?fclPct(margin):'-'; let sellInput=card.querySelector('[data-k="sell"]'); if(sellInput && document.activeElement!==sellInput) sellInput.value=String(r.sell||0); let sellCur=card.querySelector('[data-k="sellCur"]'); if(sellCur) sellCur.value=r.sellCur||'AUD'; let gstBox=card.querySelector('[data-k="gst10"]'); if(gstBox) gstBox.checked=!!r.gst10; if(fclIsPackingRow(r)){r.infoOnly='packing'; r.show=true; r.gst=r.gst10?'10%':'GST Free';} card.classList.toggle('fcl-row-fixed',!!r.fixed); card.classList.toggle('fcl-hidden-row',!r.show); } function fclRefreshAllCardStats(d){ const box=$('fclRows'); if(!box)return; const data=d||fclCollect(); box.querySelectorAll('[data-i]').forEach(card=>{ const r=FCL_ROWS[Number(card.dataset.i)]; if(r) fclRefreshCardStats(card,r,data); }); } function fclManualRateChanged(){ // 수동으로 환율을 입력할 때는 적용환율 표시만 즉시 바꿉니다. // 실제 고객청구 금액 반영은 '수동환율 적용' 버튼으로 확정합니다. const rate=fclN($('fclNaverRate')?.value); const applied=Math.max(rate-10,1); if($('fclAppliedRate')) $('fclAppliedRate').value=applied.toFixed(2); if($('fclFxStatus')) $('fclFxStatus').textContent='수동 환율 입력됨: 적용하려면 [수동환율 적용]을 누르세요. 적용 AUD/KRW는 입력값 -10 입니다.'; fclRefreshAllCardStats({appliedRate:applied,usdRate:fclN($('fclUsdRate')?.value)||0.66,rows:FCL_ROWS}); } function fclApplyManualRate(){ fclReadRows(); const d=fclCollect(); const changed=fclApplyCurrentRateToRows(d,true); if($('fclAppliedRate')) $('fclAppliedRate').value=d.appliedRate.toFixed(2); fclRefreshAllCardStats(d); fclUpdateAll(false); if($('fclFxStatus')) $('fclFxStatus').textContent=`수동환율 적용 완료: AUD/KRW ${Number(d.naverRate).toFixed(2)} → 적용 ${Number(d.appliedRate).toFixed(2)} / USD ${Number(d.usdRate).toFixed(4)} (${changed}개 항목 반영)`; } function fclUpdateReceivableForCard(card){ if(!card)return; fclReadRows(); let i=Number(card.dataset.i), r=FCL_ROWS[i]; let d=fclCollect(); let ok=fclSetReceivableFromCost(r,d,1.03); if(ok){ const btn=card.querySelector('[data-action="rowFx"]'); if(btn) btn.textContent='이 항목 +3% 적용됨'; fclRefreshCardStats(card,r,d); fclUpdateAll(false); } return ok; } function fclRenderRows(){ const box=$('fclRows'); if(!box)return; const d={appliedRate:Math.max(fclN($('fclNaverRate')?.value)-10,1),usdRate:fclN($('fclUsdRate')?.value)||0.66}; box.innerHTML=FCL_ROWS.map((r,i)=>{ fclInitRow(r); const isPack=fclIsPackingRow(r); if(isPack){ r.infoOnly='packing'; r.show=true; r.gst=r.gst10?'10%':'GST Free'; } let cost=fclRowCostAud(r,d), sell=fclRowSellAud(r,d), profit=sell-cost, margin=sell?profit/sell:0; const rowName=(r.desc||'').split('\n')[0]||'Charge'; let title=(r.section?`${fclEsc(r.section)}`:'')+``; let packGuideHtml=''; if(isPack){ const guide=fclPackingGuide(($('fclContainer')&&$('fclContainer').value)||'20FT'); const guideList=guide.slice(1).map(x=>`
  • ${fclEsc(x)}
  • `).join(''); packGuideHtml=`
    ${fclEsc(guide[0])}
    Container Packing 금액은 고객용 견적서와 합계에 표시됩니다. 필요 없으면 금액을 0으로 두세요.
    `; }else if(fclIsOriginTrmlRow(r)){ packGuideHtml=`
    내부 참고 / 고객 견적서에는 미표시
    이 메모는 내부 화면에서만 보이고 고객용 PDF/HTML 견적서에는 표시되지 않습니다.
    `; } return `
    ${title}
    ${packGuideHtml}
    실제 원가 / Payable
    Payable AUD${fclAud(cost)}
    Profit${fclAud(profit)}
    고객 청구 / Receivable
    Receivable AUD${fclAud(sell)}
    Margin${sell?fclPct(margin):'-'}
    `; }).join(''); box.querySelectorAll('input,select,textarea').forEach(el=>{ const handler=()=>{ const card=el.closest('[data-i]'); const key=el.dataset.k; const action=el.dataset.action; if(card&&action==='rename-row'){ fclReadRows(); const i=Number(card.dataset.i),r=FCL_ROWS[i]; if(r){ const lines=String(r.desc||'').split('\n'); if(!lines.length)lines.push(''); lines[0]=el.value; r.desc=lines.join('\n'); const descBox=card.querySelector('[data-k="desc"]'); if(descBox)descBox.value=r.desc; } fclUpdateAll(false); return; } fclReadRows(); if(card){ let i=Number(card.dataset.i), r=FCL_ROWS[i]; if((key==='tba'||key==='atcost')&&el.checked){let otherKey=key==='tba'?'atcost':'tba';let other=card.querySelector('[data-k="'+otherKey+'"]');if(other)other.checked=false;r[otherKey]=false;r[key]=true;} let d=fclCollect(); if(fclIsPackingRow(r)){r.infoOnly='packing'; r.show=true; r.gst=r.gst10?'10%':'GST Free';} if(key==='cost'||key==='costCur'||key==='vat10'){ const mult=(r.plus3===true)?1.03:1; fclSetReceivableFromCost(r,d,mult); }else if(key==='sell'||key==='sellCur'||key==='gst10'){ r.plus3=false; r.fxMultiplier=1; r.autoFx=false; }else if(key==='desc'){ const nameInput=card.querySelector('[data-action="rename-row"]'); if(nameInput)nameInput.value=String(r.desc||'').split('\n')[0]||''; } fclRefreshCardStats(card,r,d); } fclUpdateAll(false); }; el.oninput=handler; el.onchange=handler; el.onblur=()=>{fclCommitFormulaInput(el);handler();}; el.onkeydown=(e)=>{if(e.key==='Enter' && el.tagName!=='TEXTAREA'){e.preventDefault();fclCommitFormulaInput(el);handler();el.blur();}}; }); box.querySelectorAll('[data-action="rowFx"]').forEach(btn=>{ btn.onclick=()=>{ const card=btn.closest('[data-i]'); const ok=fclUpdateReceivableForCard(card); if(!ok)alert('이 항목은 AT COST/TBA/숨김이거나 원가가 0이라 적용할 수 없습니다.'); }; }); } function fclReadRows(){ const box=$('fclRows'); if(!box)return; box.querySelectorAll('[data-i]').forEach(card=>{ let i=Number(card.dataset.i), r=FCL_ROWS[i]; if(!r)return; // template change 중 남아있는 DOM 행이 있어도 FCL 미리보기가 LCL로 멈추지 않게 방어 card.querySelectorAll('[data-k]').forEach(el=>{ let k=el.dataset.k; if(k==='fixed'||k==='show'||k==='vat10'||k==='gst10'||k==='tba'||k==='atcost')r[k]=el.checked; else if(k==='cost'||k==='sell')r[k]=fclN(el.value); else r[k]=el.value; }); r.gst=r.gst10?'10%':'GST Free'; card.classList.toggle('fcl-row-fixed',!!r.fixed); card.classList.toggle('fcl-hidden-row',!r.show); }); } function fclUpdateAll(){ const fclMode=isFcl(); document.querySelectorAll('.lcl-ui').forEach(el=>el.classList.toggle('hidden',fclMode)); if($('fclForm')) $('fclForm').style.display=fclMode?'block':'none'; if(!fclMode)return; try{ if(!window.__fclRateTried){window.__fclRateTried=true; setTimeout(()=>fclTryRate(true),150);} $('selectedSheet').textContent='FCL_'+(($('fclTemplate')&&$('fclTemplate').value)||($('fclContainer')&&$('fclContainer').value)||'20FT_STRA'); fclRefreshLinkedReceivables(); fclRefreshAllCardStats(); let c=fclCalc(); if($('fclAppliedRate')) $('fclAppliedRate').value=c.d.appliedRate.toFixed(2); if($('fclCostKpi')) $('fclCostKpi').textContent=fclAud(c.cost); if($('fclRevenueKpi')) $('fclRevenueKpi').textContent=fclAud(c.revenue); if($('fclProfitKpi')) $('fclProfitKpi').textContent=fclAud(c.profit); if($('fclMarginKpi')) $('fclMarginKpi').textContent=fclPct(c.margin); fclSetSummaryLabels(); if($('mOrigin')) $('mOrigin').textContent=fclAud(fclOriginChargeValue(c.d)); if($('mFreight')) $('mFreight').textContent=fclAud(fclDestinationChargeValue(c.d)); if($('mClearance')) $('mClearance').textContent=fclAud(fclDestinationClearanceValue(c.d)); if($('mHandling')) $('mHandling').textContent=fclAud(fclCartageUnpackValue(c.d)); if($('mLocal')) $('mLocal').textContent=fclAud(fclFinalDeliveryValue(c.d)); if($('mTotal')) $('mTotal').textContent=fclAud(c.totalAud); if($('quotePreview')){$('quotePreview').className='fcl-quote-page'; $('quotePreview').innerHTML=quoteEditorTransformHtml(fclBuildQuote(c),quoteEditorContextFromFcl(c.d));} }catch(e){ console.error('FCL preview update failed',e); if($('quotePreview')){$('quotePreview').className='fcl-quote-page'; $('quotePreview').innerHTML='
    FCL 미리보기 오류: '+fclEsc(e.message||e)+'
    ';} } } function fclSetSummaryLabels(){ const labels=['Origin Charge','Destination Charge','Destination Clearance','Cartage / Unpack','Final Delivery','Approx. Total']; document.querySelectorAll('.summary .metric .k').forEach((el,i)=>{ if(labels[i]) el.textContent=labels[i]; }); } function lclSetSummaryLabels(){ const labels=['Origin Charges','Freight','Clearance','Handling','Local Delivery','Approx. Total']; document.querySelectorAll('.summary .metric .k').forEach((el,i)=>{ if(labels[i]) el.textContent=labels[i]; }); } function fclVisibleSellSum(d,regex){return d.rows.filter(r=>r.show&®ex.test(r.desc)&&!r.atcost&&!r.tba).reduce((s,r)=>s+fclRowSellAud(r,d),0)} function fclVisibleSellSumBySection(d,sectionName){ let current=''; return d.rows.reduce((sum,r)=>{ if(r.section) current=r.section; if(current===sectionName && r.show && !r.atcost && !r.tba) return sum+fclRowSellAud(r,d); return sum; },0); } function fclOriginChargeValue(d){return fclVisibleSellSumBySection(d,'Origin Charge')} function fclDestinationChargeValue(d){return fclVisibleSellSumBySection(d,'Destination Charge')} function fclDestinationClearanceValue(d){return fclVisibleSellSumBySection(d,'Destination Clearance')} function fclCartageUnpackValue(d){return fclVisibleSellSumBySection(d,'Cartage / Unpack')} function fclFinalDeliveryValue(d){return fclVisibleSellSumBySection(d,'Final Delivery')} function fclFreightValue(d){return fclOriginChargeValue(d)} function fclClearanceValue(d){return fclDestinationClearanceValue(d)} function fclHandlingValue(d){return fclCartageUnpackValue(d)} function fclLocalValue(d){return fclFinalDeliveryValue(d)} function fclApplyFxReceivable(){ // 현재 환율을 모든 자동 항목에 즉시 반영합니다. +3%는 각 항목 버튼을 누른 행만 유지됩니다. let c=fclCalc(); let changed=fclApplyCurrentRateToRows(c.d,false); fclRenderRows(); fclUpdateAll(false); if(changed===0){alert('적용할 항목이 없습니다. AT COST/TBA/숨김 항목은 제외됩니다.');} } function fclApplyCurrentRateToRows(d,force){ let changed=0; FCL_ROWS.forEach(r=>{ fclInitRow(r); if(r.atcost || r.tba || !r.show) return; if(!force && r.autoFx===false) return; const mult=(r.plus3===true)?1.03:1; if(fclSetReceivableFromCost(r,d,mult)) changed++; }); return changed; } function fclDistribute(delta,totalMode){let d=fclCollect();let adjustable=FCL_ROWS.filter(r=>r.show&&!r.fixed&&!r.atcost&&!r.tba&&!fclIsPackingRow(r));let weights=adjustable.map(r=>Math.max(fclRowSellAud(r,d),fclRowCostAud(r,d),1));let sum=weights.reduce((a,b)=>a+b,0)||1;adjustable.forEach((r,idx)=>{let adjAud=delta*(weights[idx]/sum); if(totalMode && r.gst10 && (r.sellCur||'AUD')==='AUD') adjAud=adjAud/1.1; if((r.sellCur||'AUD')==='KRW') r.sell=Math.max(0,fclN(r.sell)+adjAud*d.appliedRate); else if((r.sellCur||'AUD')==='USD') r.sell=Math.max(0,fclN(r.sell)+adjAud*d.usdRate); else r.sell=Math.max(0,fclN(r.sell)+adjAud);});} function fclApplyTargetMargin(){let c=fclCalc();let targetMargin=Math.min(Math.max(c.d.targetMargin,0),0.8);let targetRevenue=c.cost/(1-targetMargin);fclDistribute(targetRevenue-c.revenue,false);fclRenderRows();fclUpdateAll(false);} function fclApplyTargetTotal(){let c=fclCalc();if(!c.d.targetTotal){alert('목표 총액 AUD를 먼저 입력하세요.');return;}let deltaTotal=c.d.targetTotal-c.totalAud;fclDistribute(deltaTotal,true);fclRenderRows();fclUpdateAll(false);} function fclParseRateNumber(x){ if(x===null||x===undefined)return 0; let t=String(x).replace(/,/g,'').trim(); let m=t.match(/\b(\d{3,4}(?:\.\d{1,4})?)\b/); if(!m)return 0; let n=Number(m[1]); return (n>800 && n<1400)?n:0; } function fclFindNaverKrw(obj){ const preferred=/rate|base|price|value|deal|close|now|today|매매|기준/i; let best=0; function walk(v,k){ if(best)return; if(v===null||v===undefined)return; if(typeof v==='number'){ if(preferred.test(String(k)) && v>800 && v<1400) best=v; return; } if(typeof v==='string'){ let n=fclParseRateNumber(v); if(n && (preferred.test(String(k)) || /1,0\d\d|10\d\d/.test(v))) best=n; return; } if(typeof v==='object'){ for(const kk in v) walk(v[kk],kk); } } walk(obj,''); return best; } function fclFindRateInText(txt){ txt=String(txt||''); const patterns=[ /class=["']today["'][^>]*>\s*([0-9,]+\.\d+)/i, /매매기준율[^0-9]{0,80}([0-9,]+\.\d+)/i, /FX_AUDKRW[\s\S]{0,400}?([0-9,]+\.\d+)/i, /AUD[\s\S]{0,80}?KRW[\s\S]{0,120}?([0-9,]+\.\d+)/i, /\b(1,0\d\d\.\d{1,4}|10\d\d\.\d{1,4})\b/ ]; for(const re of patterns){let m=txt.match(re); if(m){let n=fclParseRateNumber(m[1]); if(n)return n;}} return 0; } async function fclFetchTextNoCache(url,ms=9000){ const ctrl=new AbortController(); const timer=setTimeout(()=>ctrl.abort(),ms); try{ const res=await fetch(url,{cache:'no-store',signal:ctrl.signal}); if(!res.ok) throw new Error('HTTP '+res.status); return await res.text(); }finally{clearTimeout(timer);} } async function fclFetchNaverAudKrw(){ // 네이버 환율을 우선으로 가져옵니다. 로컬 HTML의 CORS 제한을 피하기 위해 네이버 원본 → Jina Reader → AllOrigins → CORS proxy 순서로 시도합니다. const naverUrls=[ 'https://m.stock.naver.com/front-api/v1/marketIndex/prices?page=1&category=exchange&pageSize=1&reutersCode=FX_AUDKRW', 'https://m.stock.naver.com/front-api/v1/marketIndex/prices?page=1&category=exchange&pageSize=10&reutersCode=FX_AUDKRW', 'https://m.search.naver.com/search.naver?query=AUD%20%ED%99%98%EC%9C%A8', 'https://search.naver.com/search.naver?query=AUD%20%ED%99%98%EC%9C%A8', 'https://m.stock.naver.com/front-api/v1/marketIndex/exchange/new?category=exchange&reutersCode=FX_AUDKRW', 'https://finance.naver.com/marketindex/exchangeDetail.naver?marketindexCd=FX_AUDKRW' ]; const wrapUrls=(u)=>[ u, 'https://r.jina.ai/'+u, 'https://api.allorigins.win/raw?url='+encodeURIComponent(u), 'https://api.allorigins.win/get?url='+encodeURIComponent(u), 'https://corsproxy.io/?'+encodeURIComponent(u) ]; let lastErr=''; for(const u of naverUrls){ for(const wu of wrapUrls(u)){ try{ const txt=await fclFetchTextNoCache(wu,12000); let body=txt; try{ const js=JSON.parse(txt); if(js && typeof js.contents==='string') body=js.contents; let n=fclFindNaverKrw(js); if(n>0) return {krw:n, name: wu===u ? 'Naver' : (wu.includes('r.jina.ai')?'Naver via Jina':'Naver via proxy')}; }catch(_){ } let n=fclFindRateInText(body); if(n>0) return {krw:n, name: wu===u ? 'Naver' : (wu.includes('r.jina.ai')?'Naver via Jina':'Naver via proxy')}; }catch(e){lastErr=e.message||String(e);} } } throw new Error(lastErr||'Naver fetch failed'); } function fclParseKrwRateFlexible(x,min,max){ if(x===null||x===undefined)return 0; let t=String(x).replace(/,/g,'').trim(); let m=t.match(/\b(\d{3,4}(?:\.\d{1,4})?)\b/); if(!m)return 0; let n=Number(m[1]); return (n>min && nmin && v[ u, 'https://r.jina.ai/'+u, 'https://api.allorigins.win/raw?url='+encodeURIComponent(u), 'https://api.allorigins.win/get?url='+encodeURIComponent(u), 'https://corsproxy.io/?'+encodeURIComponent(u) ]; let lastErr=''; for(const u of urls){ for(const wu of wrapUrls(u)){ try{ const txt=await fclFetchTextNoCache(wu,10000); let body=txt; try{ const js=JSON.parse(txt); if(js && typeof js.contents==='string') body=js.contents; let n=fclFindNaverKrwFlexible(js,900,2000); if(n>0) return {usdKrw:n,name: wu===u?'Naver USD/KRW':(wu.includes('r.jina.ai')?'Naver USD/KRW via Jina':'Naver USD/KRW via proxy')}; }catch(_){ } let n=fclFindNaverKrwFlexible(body,900,2000); if(n>0) return {usdKrw:n,name: wu===u?'Naver USD/KRW':(wu.includes('r.jina.ai')?'Naver USD/KRW via Jina':'Naver USD/KRW via proxy')}; }catch(e){lastErr=e.message||String(e);} } } throw new Error(lastErr||'Naver USD/KRW fetch failed'); } async function fclFetchUsdPerAud(audKrw){ // 1순위: 네이버 AUD/KRW ÷ 네이버 USD/KRW = AUD/USD 로 계산 try{ const u=await fclFetchNaverUsdKrw(); const a=Number(audKrw)||fclN($('fclNaverRate')&&$('fclNaverRate').value); if(a>0 && Number(u.usdKrw)>0){ return {rate:a/Number(u.usdKrw), source:u.name}; } }catch(e){} // 2순위: AUD/USD 공개 환율 API / 프록시 const urls=[ 'https://query1.finance.yahoo.com/v8/finance/chart/AUDUSD=X?range=1d&interval=1m', 'https://api.frankfurter.app/latest?from=AUD&to=USD', 'https://open.er-api.com/v6/latest/AUD' ]; const wrap=(u)=>[u,'https://api.allorigins.win/raw?url='+encodeURIComponent(u),'https://api.allorigins.win/get?url='+encodeURIComponent(u),'https://r.jina.ai/'+u]; for(const u of urls){ for(const wu of wrap(u)){ try{ const txt=await fclFetchTextNoCache(wu,8000); let body=txt; try{const j=JSON.parse(txt); if(j&&typeof j.contents==='string') body=j.contents;}catch(_){} const js=JSON.parse(body); let val=0; if(js.chart&&js.chart.result&&js.chart.result[0]){ const arr=js.chart.result[0].indicators&&js.chart.result[0].indicators.quote&&js.chart.result[0].indicators.quote[0]&&js.chart.result[0].indicators.quote[0].close; if(arr){for(let i=arr.length-1;i>=0;i--){if(Number(arr[i])>0){val=Number(arr[i]);break;}}} } if(!val && js.rates&&Number(js.rates.USD)>0) val=Number(js.rates.USD); if(val>0) return {rate:val,source:(u.includes('yahoo')?'Yahoo AUD/USD':u.includes('frankfurter')?'Frankfurter AUD/USD':'Open ER API AUD/USD')}; }catch(e){} } } return {rate:0,source:''}; } async function fclTryRate(silent){ const status=$('fclFxStatus'); const setStatus=(msg)=>{if(status)status.textContent=msg;}; const btns=[...document.querySelectorAll('button')].filter(b=>/네이버.*환율/.test(b.textContent||'')); btns.forEach(b=>{b.disabled=true; b.dataset.oldText=b.textContent; b.textContent='네이버 실시간 적용 중...';}); setStatus('네이버 실시간 AUD/KRW + AUD/USD 환율을 가져와서 견적 금액에 적용 중...'); try{ const r=await fclFetchNaverAudKrw(); $('fclNaverRate').value=Number(r.krw).toFixed(2); const usdObj=await fclFetchUsdPerAud(r.krw); const usd=Number(usdObj&&usdObj.rate)||0; if(usd>0)$('fclUsdRate').value=usd.toFixed(4); const d=fclCollect(); fclApplyCurrentRateToRows(d,false); fclRenderRows(); fclUpdateAll(false); const now=new Date().toLocaleString('ko-KR'); setStatus(`네이버 실시간 환율 적용 완료: AUD/KRW ${Number(r.krw).toFixed(2)} → 적용 ${Number(r.krw-10).toFixed(2)}${usd?` / AUD/USD ${Number(usd).toFixed(4)} (${usdObj.source})`:''} (${r.name}, ${now})`); if(!silent)alert('네이버 실시간 AUD/KRW와 AUD/USD를 고객청구 금액에 바로 적용했습니다. 적용 AUD/KRW는 네이버 환율 -10 입니다.'); return true; }catch(e){ setStatus('네이버 실시간 조회 실패: 회사 보안 또는 브라우저 CORS 차단입니다. 네이버 창을 열었습니다.'); if(!silent){ window.open('https://search.naver.com/search.naver?query=AUD%20%ED%99%98%EC%9C%A8','_blank'); alert('네이버가 브라우저 보안으로 막혀서 자동 적용이 실패했습니다. 네이버 숫자를 AUD/KRW 조회/수동 칸에 입력하면 즉시 적용됩니다.'); } return false; }finally{ btns.forEach(b=>{b.disabled=false; if(b.dataset.oldText)b.textContent=b.dataset.oldText;}); } } function fclQuoteRows(d){ let html='', current=''; d.rows.forEach(r=>{ if(!r.show)return; if(r.section && r.section!==current){current=r.section;html+=`${fclEsc(current)}`;} let desc=r.routeCartage?fclRouteLabel(d):r.desc; if(/^Final Delivery Address/i.test(r.desc||'')) desc='Final Delivery Address :\n'+(d.finalAddress||'TBA'); let sellAud=fclRowSellAud(r,d); let isStatus=!!(r.tba||r.atcost); let amountText=r.atcost?'AT COST':(r.tba?'TBA': (sellAud===0 ? '0.00' : sellAud.toLocaleString('en-AU',{minimumFractionDigits:2,maximumFractionDigits:2}))); let qty=fclN(r.qty)||1; let unit = isStatus ? '' : (sellAud===0 ? '0.00' : (sellAud/qty).toLocaleString('en-AU',{minimumFractionDigits:2,maximumFractionDigits:2})); let gstText = isStatus ? '' : ((r.gst10 && (r.sellCur||'AUD')==='AUD')?'10%':'GST Free'); let qtyText = isStatus ? '' : (r.qty||''); if(fclIsPackingRow(r)){desc='Container Packing (if applicable)';} html+=`${fclEsc(desc)}${qtyText}${unit}${fclEsc(gstText)}${amountText}`; }); return html; } function fclBuildQuote(c){let d=c.d;return `
    DRAFT QUOTE
    ${fclEsc(fclCustomerAttentionName(d))}
    WISE LOGISTICS SOLUTION
    Date${fclEsc(fclDateDisplay(d.issueDate)||'')}Expiry${fclEsc(fclDateDisplay(d.expiryDate)||'')}Quote Number${fclEsc(d.quoteNoDisplay||fclPrefixQuoteNo(d.quoteNo,d.branch))}Reference${fclEsc(d.reference||'')}ABN26 652 389 305Issued By${fclEsc(d.issuedBy||'')}
    ${fclCompanyBlock(d)}
    ${fclQuoteRows(d)}
    DescriptionQuantityUnit PriceGSTAmount AUD
    POL / POD ${fclEsc(d.pol)} / ${fclEsc(d.pod)} MBL ${fclEsc(d.mbl)} Pcs/Volume/Weight ${fclEsc(d.pcs)}
    ${fclEsc(FCL_TERMS)}
    Subtotal${c.revenue.toLocaleString('en-AU',{minimumFractionDigits:2,maximumFractionDigits:2})}
    TOTAL GST 10%${c.gst.toLocaleString('en-AU',{minimumFractionDigits:2,maximumFractionDigits:2})}
    TOTAL GST FREE0.00
    TOTAL AUD${c.totalAud.toLocaleString('en-AU',{minimumFractionDigits:2,maximumFractionDigits:2})}
    `;} function fclMakePayload(){ fclUpdateAll(); let c=fclCalc(); let d=c.d; const editContext=quoteEditorContextFromFcl(d); d.manualQuoteEdits=quoteEditorGetMap(editContext); let fileBase=`${fileDate(d.issueDate)}_${cleanFile(fclCustomerAttentionName(d))}_FCL_${d.template||d.container}`; const editedQuote=quoteEditorTransformHtml(fclBuildQuote(c),editContext); let quoteHtml=`${fileBase}
    ${editedQuote}
    `; return {fileBase,customer:d.customer||'NO NAME',mode:'FCL',delivery:d.container,total:fclAud(c.totalAud),quoteHtml,excelHtml:quoteHtml,inputJson:JSON.stringify({input:d,calc:{cost:c.cost,revenue:c.revenue,gst:c.gst,totalAud:c.totalAud,profit:c.profit,margin:c.margin}},null,2),inputObj:d}; } async function fclSaveQuote(){ $('saved').style.display='none';$('err').style.display='none';let dir=await getWritableFolder();if(!dir){$('err').innerHTML='저장 폴더가 아직 선택되지 않았습니다. 저장 폴더 선택을 먼저 누르거나, 다운로드 저장을 사용하세요.';$('err').style.display='block';return;} try{let p=fclMakePayload();let htmlName=`${p.fileBase}_웹견적서.html`;let xlsName=`${p.fileBase}_견적서.xls`;let jsonName=`${p.fileBase}_GIBAEK.txt`;await writeTextFile(dir,htmlName,p.quoteHtml);await writeTextFile(dir,xlsName,p.excelHtml);await writeTextFile(dir,jsonName,p.inputJson);await appendSaveHistory(dir,[nowStamp(),p.customer,`FCL_${p.delivery}`,p.total,htmlName,xlsName,jsonName],p.inputJson);$('saved').innerHTML=`저장 완료
    ${p.fileBase}
    선택한 폴더에 HTML / Excel / GIBAEK / 저장기록.csv가 저장되었습니다.`;$('saved').style.display='block';await loadRecords();}catch(e){$('err').textContent='저장 실패: '+e.message;$('err').style.display='block'} } function fclDownloadQuote(){let p=fclMakePayload();downloadBlob(`${p.fileBase}_웹견적서.html`,p.quoteHtml,'text/html;charset=utf-8');downloadBlob(`${p.fileBase}_견적서.xls`,p.excelHtml,'application/vnd.ms-excel;charset=utf-8');downloadBlob(`${p.fileBase}_GIBAEK.txt`,p.inputJson,'text/plain;charset=utf-8')} function installFclUI(){ if($('quoteType')) return; OLD.updateAll=window.updateAll; OLD.resetForm=window.resetForm; OLD.saveQuote=window.saveQuote; OLD.downloadQuote=window.downloadQuote; OLD.printQuote=window.printQuote; const toolbar=document.querySelector('.toolbar'); const typeLabel=document.createElement('label'); typeLabel.textContent='견적'; const typeSel=document.createElement('select'); typeSel.id='quoteType'; typeSel.className='small'; typeSel.innerHTML=''; typeSel.onchange=()=>window.updateAll(); toolbar.insertBefore(typeSel, toolbar.firstChild); toolbar.insertBefore(typeLabel, typeSel); const firstCard=document.querySelector('.wrap>.card:first-child'); Array.from(firstCard.children).forEach(el=>{if(el.id!=='saved'&&el.id!=='err') el.classList.add('lcl-ui')}); const fcl=document.createElement('div'); fcl.id='fclForm'; fcl.className='fcl-panel'; fcl.innerHTML=`
    ① FCL Quotation 기본 정보
    ② Shipment Details
    ③ 내부 원가 / 환율 / 마진
    네이버 실시간 환율 적용 또는 수동환율 적용 버튼으로 재계산
    실제 지출 AUD$0.00
    고객 청구 AUD$0.00
    예상 이익$0.00
    마진율0.00%
    각 항목의 QTY는 기본 1이며 직접 수정 가능합니다. 원가 통화는 AUD/KRW/USD 중 선택합니다. VAT 10% 체크 시 Payable AUD에 자동으로 10% 포함 원가가 반영됩니다. 고객 청구 Receivable은 원가/통화/VAT/환율 변경 시 기본 환율만 적용된 금액으로 자동 반영됩니다. 네이버 실시간 환율 적용을 누르면 현재 네이버 AUD/KRW를 가져와 전체 자동 항목에 즉시 반영합니다. 각 항목의 이 항목 +3% 적용 버튼을 누르면 해당 항목만 3%가 추가되며, 누르기 전에는 3%가 자동으로 붙지 않습니다. TBA 또는 AT COST를 항목별로 선택할 수 있습니다. 선택한 항목은 Quantity / Unit Price / GST가 공란으로 표시되고 Amount AUD에는 각각 TBA 또는 AT COST가 표시되며 숫자 합계에서 제외됩니다. Container Packing은 금액을 입력하면 고객용 견적서와 합계에 표시됩니다. 내부 원가/한국 VAT/마진 계산은 고객용 견적서에 표시되지 않습니다.
    `; firstCard.insertBefore(fcl,$('saved')); window.fclResetRows=fclResetRows; window.fclLoadInput=fclLoadInput; window.fclApplyTemplate=fclApplyTemplate; window.fclSyncTemplateFromContainer=fclSyncTemplateFromContainer; window.fclBranchChanged=fclBranchChanged; window.fclPodChanged=fclPodChanged; window.fclApplyFxReceivable=fclApplyFxReceivable; window.fclApplyTargetMargin=fclApplyTargetMargin; window.fclApplyTargetTotal=fclApplyTargetTotal; window.fclTryRate=fclTryRate; window.fclAddRow=fclAddRow; window.fclManualRateChanged=fclManualRateChanged; window.fclApplyManualRate=fclApplyManualRate; window.fclMakePayload=fclMakePayload; window.updateAll=function(){ if(isFcl()){ fclUpdateAll(); setTimeout(()=>{ if(isFcl()) fclUpdateAll(); },0); }else{ lclSetSummaryLabels(); document.querySelectorAll('.lcl-ui').forEach(el=>el.classList.remove('hidden')); if($('fclForm')) $('fclForm').style.display='none'; if($('quotePreview')) $('quotePreview').className='quote-page'; OLD.updateAll(); } }; // 견적 타입이 FCL인데 미리보기가 LCL 표로 남아있는 경우 자동 복구 setInterval(()=>{try{if(isFcl() && $('quotePreview') && !/DRAFT QUOTE/.test($('quotePreview').innerText||'')) fclUpdateAll();}catch(e){}},700); window.resetForm=function(){ if(isFcl()) fclResetAll(); else OLD.resetForm(); }; window.saveQuote=async function(){ if(isFcl()) await fclSaveQuote(); else await OLD.saveQuote(); }; window.downloadQuote=function(){ if(isFcl()) fclDownloadQuote(); else OLD.downloadQuote(); }; window.printQuote=function(){ cleanPrint(); }; } window.addEventListener('load',()=>{installFclUI();fclResetAll();setTimeout(()=>window.updateAll(),80);}); })(); /* ===== v5.7: direct quotation preview editing ===== */ let QUOTE_EDITOR_ON=false; /* ===== v5.8: LCL row controls matching the FCL workflow ===== */ const LCL_EDITOR_BASE={ collect:collect, calc:calc, buildRows:buildRows, buildQuoteHtml:buildQuoteHtml, loadLclInput:loadLclInput, resetForm:resetForm, updateAll:window.updateAll }; const LCL_EDITOR_SETS={}; let LCL_EDITOR_READY=false; let LCL_EDITOR_LAST_SHEET=''; // FCL v5.7 compatibility: FCL is already edited through its own cards. function quoteEditorContextFromFcl(){return 'FCL'} function quoteEditorGetMap(){return {}} function quoteEditorImportContext(){} function quoteEditorTransformHtml(html){return html} function lclEditorEsc(v){return String(v??'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))} function lclEditorSheet(){return (($('mode')&&$('mode').value)||'AIR')+'_'+(($('delivery')&&$('delivery').value)||'SPD')} function lclEditorClone(v){return JSON.parse(JSON.stringify(v))} function lclEditorMoneyNumber(text){ const s=String(text??'').trim(); if(!s||/^(TBA|AT COST|QUOTE)$/i.test(s))return null; const clean=s.replace(/,/g,'').replace(/\$/g,'').trim(); if(!/^-?\d+(?:\.\d+)?$/.test(clean))return null; const n=Number(clean);return Number.isFinite(n)?n:null; } function lclEditorMoney(n){return '$'+Number(n||0).toLocaleString('en-AU',{minimumFractionDigits:2,maximumFractionDigits:2})} function lclEditorStatusText(text){ const s=String(text??'').trim().toUpperCase(); if(s==='AT COST')return 'AT COST'; if(s==='TBA')return 'TBA'; if(s==='QUOTE')return 'Quote'; return ''; } function lclEditorBaseModel(c){ const t=document.createElement('template'); t.innerHTML=''+LCL_EDITOR_BASE.buildRows(c)+'
    '; const sections=[];let current=null,si=-1,ri=0; Array.from(t.content.querySelectorAll('tr')).forEach(tr=>{ if(tr.classList.contains('charge-head')){ si++;ri=0;current={id:'section_'+si,baseTitle:(tr.cells[0]?.textContent||'').trim(),rows:[],includeInGrand:si>0};sections.push(current);return; } if(!current||tr.classList.contains('subtotal')||tr.classList.contains('grand-total'))return; if(tr.classList.contains('subline')){ current.rows.push({id:current.id+'_sub_'+ri++,kind:'subline',baseDesc:(tr.textContent||'').trim(),baseCur:'',baseRate:'',baseUnit:'',baseAmount:''});return; } if(tr.cells&&tr.cells.length===5){ current.rows.push({id:current.id+'_row_'+ri++,kind:'line',baseDesc:(tr.cells[0].textContent||'').trim(),baseCur:(tr.cells[1].textContent||'').trim(),baseRate:(tr.cells[2].textContent||'').trim(),baseUnit:(tr.cells[3].textContent||'').trim(),baseAmount:(tr.cells[4].textContent||'').trim()}); } }); return sections; } function lclEditorNewRow(base){ return Object.assign({ id:'custom_'+Date.now()+'_'+Math.random().toString(36).slice(2,7),kind:'line',custom:true, baseDesc:'New Charge',baseCur:'AUD',baseRate:'',baseUnit:'Per Shipment',baseAmount:'', descOverride:null,curOverride:null,rateOverride:null,unitOverride:null,amountOverride:null, show:true,tba:false,atcost:false },base||{}); } function lclEditorMergeModel(c){ const key=lclEditorSheet(); const parsed=lclEditorBaseModel(c); let set=LCL_EDITOR_SETS[key]; if(!set)set=LCL_EDITOR_SETS[key]={version:2,sheet:key,sections:[]}; parsed.forEach(ps=>{ let ss=set.sections.find(x=>x.id===ps.id); if(!ss){ss={id:ps.id,baseTitle:ps.baseTitle,titleOverride:null,show:true,includeInGrand:ps.includeInGrand,rows:[]};set.sections.push(ss)} ss.baseTitle=ps.baseTitle;ss.includeInGrand=ps.includeInGrand; ps.rows.forEach(pr=>{ let sr=ss.rows.find(x=>x.id===pr.id); if(!sr){sr=lclEditorNewRow(Object.assign({},pr,{custom:false,id:pr.id}));ss.rows.push(sr)} sr.kind=pr.kind;sr.baseDesc=pr.baseDesc;sr.baseCur=pr.baseCur;sr.baseRate=pr.baseRate;sr.baseUnit=pr.baseUnit;sr.baseAmount=pr.baseAmount; }); }); set.sections=set.sections.filter(s=>parsed.some(p=>p.id===s.id)); set.sections.forEach(s=>{s.rows=s.rows.filter(r=>r.custom||parsed.some(p=>p.id===s.id&&p.rows.some(x=>x.id===r.id))) }); return set; } function lclEditorEff(row,field){ const map={desc:'baseDesc',cur:'baseCur',rate:'baseRate',unit:'baseUnit',amount:'baseAmount'}; const o=row[field+'Override'];return o===null||o===undefined?String(row[map[field]]??''):String(o); } function lclEditorRowAmount(row){ if(row.atcost)return 'AT COST'; if(row.tba)return 'TBA'; return lclEditorEff(row,'amount'); } function lclEditorSectionTitle(section){return section.titleOverride===null||section.titleOverride===undefined?section.baseTitle:String(section.titleOverride)} function lclEditorCompute(c){ const set=lclEditorMergeModel(c); const computed=[];let grand=0,grandHasNumeric=false,grandStatuses=[]; set.sections.forEach((section,index)=>{ const visible=section.show!==false?section.rows.filter(r=>r.show!==false):[]; let sum=0,hasNumeric=false,statuses=[]; visible.forEach(r=>{ if(r.kind==='subline')return; const text=lclEditorRowAmount(r);const n=lclEditorMoneyNumber(text); if(n!==null){sum+=n;hasNumeric=true}else{const st=lclEditorStatusText(text);if(st)statuses.push(st)} }); let subtotal=''; if(hasNumeric)subtotal=lclEditorMoney(sum); else if(statuses.includes('AT COST'))subtotal='AT COST'; else if(statuses.includes('TBA'))subtotal='TBA'; else if(statuses.includes('Quote'))subtotal='Quote'; else subtotal='$0.00'; if(section.show!==false&§ion.includeInGrand){ if(hasNumeric){grand+=sum;grandHasNumeric=true} grandStatuses.push(...statuses); } computed.push({section,index,visible,sum,hasNumeric,statuses,subtotal}); }); let grandText; if(grandHasNumeric)grandText=lclEditorMoney(grand); else if(grandStatuses.includes('AT COST'))grandText='AT COST'; else if(grandStatuses.includes('TBA'))grandText='TBA'; else grandText='$0.00'; const summaries=computed.map(x=>x.section.show===false?'-':x.subtotal); return {set,computed,grand,grandText,summaries}; } function lclEditorRowsHtml(c){ const m=lclEditorCompute(c);let html=''; m.computed.forEach(sec=>{ if(sec.section.show===false||!sec.visible.length)return; html+=`${lclEditorEsc(lclEditorSectionTitle(sec.section))}CURBase RateUnitAmount`; sec.visible.forEach(row=>{ if(row.kind==='subline')html+=`${lclEditorEsc(lclEditorEff(row,'desc'))}`; else html+=`${lclEditorEsc(lclEditorEff(row,'desc'))}${lclEditorEsc(lclEditorEff(row,'cur'))}${lclEditorEsc(lclEditorEff(row,'rate'))}${lclEditorEsc(lclEditorEff(row,'unit'))}${lclEditorEsc(lclEditorRowAmount(row))}`; }); html+=`SUB TOTAL${lclEditorEsc(sec.subtotal)}`; }); html+=`Approximate Total AUD${lclEditorEsc(m.grandText)}`; return html; } function lclEditorSerialize(){ const key=lclEditorSheet(),set=LCL_EDITOR_SETS[key]; return set?lclEditorClone(set):null; } function lclEditorPatchSummary(c){ const m=c&&c._lclEditor?c._lclEditor:lclEditorCompute(LCL_EDITOR_BASE.calc()); const vals=[m.summaries[0]||'-',m.summaries[1]||'-',m.summaries[2]||'-',m.summaries[3]||'-',m.summaries[4]||'-',m.grandText]; ['mOrigin','mFreight','mClearance','mHandling','mLocal','mTotal'].forEach((id,i)=>{if($(id))$(id).textContent=vals[i]}); const cards=$('quotePreview')?$('quotePreview').querySelectorAll('.quote-summary .qs-card .v'):[]; cards.forEach((el,i)=>{if(vals[i]!==undefined)el.textContent=vals[i]}); } collect=function(){ const d=LCL_EDITOR_BASE.collect(); // Old checkbox panels are retained only for backward compatibility. Charge statuses now come from row cards. d.tba=Object.assign({},d.tba||{},{freight:false,ccf:false,ior:false,entry:false,importGst:false,labelling:false,repallet:false,order:false,handlingOut:false,packing:false,local:false,subtotalFreight:false,subtotalClearance:false,subtotalHandling:false,subtotalLocal:false,total:false}); d.atcost={origin:false,freight:false,ccf:false,ior:false,entry:false,importGst:false,labelling:false,repallet:false,order:false,handlingOut:false,packing:false,local:false,subtotalFreight:false,subtotalClearance:false,subtotalHandling:false,subtotalLocal:false,total:false}; d.lclLineEditor=lclEditorSerialize(); return d; }; calc=function(){ const c=LCL_EDITOR_BASE.calc(); const m=lclEditorCompute(c);c._lclEditor=m; const totalAt=i=>m.computed[i]&&m.computed[i].hasNumeric?m.computed[i].sum:0; c.freight=totalAt(1);c.clearance=totalAt(2);c.handling=totalAt(3);c.local=totalAt(4);c.total=m.grand; return c; }; buildRows=function(c){return lclEditorRowsHtml(c)}; buildQuoteHtml=function(){ const q=LCL_EDITOR_BASE.buildQuoteHtml(); const t=document.createElement('template');t.innerHTML=q.html; const m=q.c._lclEditor||lclEditorCompute(q.c); const vals=[m.summaries[0]||'-',m.summaries[1]||'-',m.summaries[2]||'-',m.summaries[3]||'-',m.summaries[4]||'-',m.grandText]; t.content.querySelectorAll('.quote-summary .qs-card .v').forEach((el,i)=>{if(vals[i]!==undefined)el.textContent=vals[i]}); q.html=t.innerHTML;return q; }; function lclEditorRender(force){ if(!LCL_EDITOR_READY||($('quoteType')&&$('quoteType').value==='FCL'))return; const key=lclEditorSheet();if(!force&&key===LCL_EDITOR_LAST_SHEET&&$('lclEditorSections')?.children.length)return; const c=LCL_EDITOR_BASE.calc();const set=lclEditorMergeModel(c);const box=$('lclEditorSections');if(!box)return; box.innerHTML=set.sections.map((section,si)=>{ const rows=section.rows.map((row,ri)=>{ const fullDesc=lclEditorEff(row,'desc')||''; const title=fullDesc.split('\n')[0]||'(내용 없음)'; if(row.kind==='subline')return `
    `; return `
    ${row.amountOverride===null?'자동값':''}
    `; }).join(''); return `
    ${rows}
    `; }).join(''); LCL_EDITOR_LAST_SHEET=key; } function lclEditorCurrentRow(el){ const card=el.closest('.lcl-line-card'),sectionEl=el.closest('.lcl-editor-section');if(!card||!sectionEl)return null; const set=LCL_EDITOR_SETS[lclEditorSheet()];if(!set)return null; const si=Number(sectionEl.dataset.section),ri=Number(card.dataset.row);return {set,section:set.sections[si],row:set.sections[si]?.rows[ri],si,ri,card}; } function lclEditorRefreshQuote(){ LCL_EDITOR_BASE.updateAll(); try{lclEditorPatchSummary(calc())}catch(e){console.error(e)} } function lclEditorResetRow(row){ row.descOverride=row.curOverride=row.rateOverride=row.unitOverride=row.amountOverride=null;row.tba=false;row.atcost=false;row.show=true; } function lclEditorEvents(){ const box=$('lclEditorSections');if(!box)return; box.addEventListener('input',e=>{ const field=e.target.dataset.field; if(field){ const info=lclEditorCurrentRow(e.target);if(!info||!info.row)return; info.row[field+'Override']=e.target.value; if(field==='desc'){ const nameInput=info.card.querySelector('.lcl-line-name-input'); if(nameInput)nameInput.value=String(e.target.value||'').split('\n')[0]||''; } lclEditorRefreshQuote();return; } if(e.target.dataset.action==='row-name'){ const info=lclEditorCurrentRow(e.target);if(!info||!info.row)return; const lines=String(lclEditorEff(info.row,'desc')||'').split('\n'); if(!lines.length)lines.push(''); lines[0]=e.target.value; info.row.descOverride=lines.join('\n'); const descField=info.card.querySelector('[data-field="desc"]'); if(descField)descField.value=info.row.descOverride; lclEditorRefreshQuote();return; } if(e.target.dataset.action==='section-title'){ const sectionEl=e.target.closest('.lcl-editor-section'),set=LCL_EDITOR_SETS[lclEditorSheet()];if(!sectionEl||!set)return;set.sections[Number(sectionEl.dataset.section)].titleOverride=e.target.value;lclEditorRefreshQuote(); } }); box.addEventListener('change',e=>{ const action=e.target.dataset.action;if(!action)return; if(action==='section-show'){ const sectionEl=e.target.closest('.lcl-editor-section'),set=LCL_EDITOR_SETS[lclEditorSheet()];if(!sectionEl||!set)return;set.sections[Number(sectionEl.dataset.section)].show=e.target.checked;sectionEl.classList.toggle('is-hidden',!e.target.checked);lclEditorRefreshQuote();return; } const info=lclEditorCurrentRow(e.target);if(!info||!info.row)return; if(action==='row-show'){info.row.show=e.target.checked;info.card.classList.toggle('is-hidden',!e.target.checked)} if(action==='row-tba'){info.row.tba=e.target.checked;if(e.target.checked){info.row.atcost=false;const other=info.card.querySelector('[data-action="row-atcost"]');if(other)other.checked=false}} if(action==='row-atcost'){info.row.atcost=e.target.checked;if(e.target.checked){info.row.tba=false;const other=info.card.querySelector('[data-action="row-tba"]');if(other)other.checked=false}} lclEditorRefreshQuote(); }); box.addEventListener('click',e=>{ const action=e.target.dataset.action;if(!action)return; if(action==='reset-row'){ const info=lclEditorCurrentRow(e.target);if(!info||!info.row)return;lclEditorResetRow(info.row);lclEditorRender(true);lclEditorRefreshQuote();return; } if(action==='add-row'){ const sectionEl=e.target.closest('.lcl-editor-section'),set=LCL_EDITOR_SETS[lclEditorSheet()];if(!sectionEl||!set)return; const si=Number(sectionEl.dataset.section); set.sections[si].rows.push(lclEditorNewRow()); lclEditorRender(true);lclEditorRefreshQuote(); setTimeout(()=>{const input=document.querySelector(`.lcl-editor-section[data-section="${si}"] .lcl-line-card:last-child .lcl-line-name-input`);if(input){input.focus();input.select();}},0); } }); } function lclEditorHideLegacy(){ const first=document.querySelector('.wrap>.card:first-child');if(!first)return; Array.from(first.children).forEach((el,i,arr)=>{ if(el.classList.contains('section-title')&&(/⑤ LCL 견적서 TBA/.test(el.textContent)||/⑥ LCL 견적서 AT COST/.test(el.textContent))){el.classList.add('lcl-legacy-controls');if(arr[i+1])arr[i+1].classList.add('lcl-legacy-controls');if(arr[i+2])arr[i+2].classList.add('lcl-legacy-controls')} }); } function lclEditorInstall(){ if($('lclEditorBlock'))return; lclEditorHideLegacy(); const first=document.querySelector('.wrap>.card:first-child'),saved=$('saved');if(!first||!saved)return; const block=document.createElement('div');block.id='lclEditorBlock';block.className='lcl-editor-block lcl-ui'; block.innerHTML=`
    ⑤ LCL 견적 항목 관리
    FCL과 동일하게 각 항목의 이름과 내용, CUR, Base Rate, Unit, Amount를 직접 변경할 수 있습니다. 행 추가 후 New Charge 칸을 바로 원하는 항목명으로 바꿀 수 있습니다. 견적서 표시를 해제하면 빈 칸이 남지 않고 해당 행 전체가 견적서에서 사라집니다. TBA와 AT COST는 항목별로 선택할 수 있습니다.
    현재 양식:
    `; first.insertBefore(block,saved);LCL_EDITOR_READY=true;lclEditorEvents(); $('lclResetCurrentSheet').onclick=()=>{delete LCL_EDITOR_SETS[lclEditorSheet()];LCL_EDITOR_LAST_SHEET='';lclEditorRender(true);lclEditorRefreshQuote()}; lclEditorRender(true);if($('lclEditorSheetLabel'))$('lclEditorSheetLabel').textContent=lclEditorSheet(); } function lclEditorImport(data){ if(!data||!data.sections)return false; const key=data.sheet||((data.mode||'AIR')+'_'+(data.delivery||'SPD')); LCL_EDITOR_SETS[key]=lclEditorClone(data);LCL_EDITOR_SETS[key].sheet=key;return true; } function lclEditorApplyLegacy(d){ const set=LCL_EDITOR_SETS[(d.mode||'AIR')+'_'+(d.delivery||'SPD')];if(!set)return; const t=d.tba||{},a=d.atcost||{}; set.sections.forEach((s,si)=>s.rows.forEach(r=>{ const x=(r.baseDesc||'').toLowerCase();let key=''; if(si===0){if(a.origin)r.atcost=true;return} if(si===1&&r.baseAmount){key='freight'} else if(/customs clearance/.test(x))key='ccf';else if(/^ior/.test(x))key='ior';else if(/entry charge/.test(x))key='entry';else if(/import gst/.test(x))key='importGst'; else if(/labelling/.test(x))key='labelling';else if(/re-pallet/.test(x))key='repallet';else if(/order processing/.test(x))key='order';else if(/handling out/.test(x))key='handlingOut';else if(/packing material/.test(x))key='packing';else if(si===4&&r.baseAmount)key='local'; if(key){r.tba=!!t[key];r.atcost=!!a[key];if(r.atcost)r.tba=false} })); } loadLclInput=function(d){ d=d||{};const imported=lclEditorImport(d.lclLineEditor);const result=LCL_EDITOR_BASE.loadLclInput(d); const c=LCL_EDITOR_BASE.calc();lclEditorMergeModel(c);if(!imported)lclEditorApplyLegacy(d); LCL_EDITOR_LAST_SHEET='';lclEditorRender(true);lclEditorRefreshQuote();return result; }; resetForm=function(){ Object.keys(LCL_EDITOR_SETS).forEach(k=>delete LCL_EDITOR_SETS[k]);LCL_EDITOR_LAST_SHEET=''; const r=LCL_EDITOR_BASE.resetForm();setTimeout(()=>{lclEditorRender(true);lclEditorPatchSummary(calc())},0);return r; }; window.updateAll=function(){ const r=LCL_EDITOR_BASE.updateAll.apply(this,arguments); if(LCL_EDITOR_READY&&(!$('quoteType')||$('quoteType').value!=='FCL')){ if($('lclEditorSheetLabel'))$('lclEditorSheetLabel').textContent=lclEditorSheet(); const changed=LCL_EDITOR_LAST_SHEET!==lclEditorSheet();lclEditorRender(changed);try{lclEditorPatchSummary(calc())}catch(e){} } return r; }; window.addEventListener('load',()=>{setTimeout(()=>{lclEditorInstall();lclEditorRender(true);lclEditorPatchSummary(calc())},120)}); /* ===== v5.7 user-defined defaults + history management ===== */ const LOCAL_USER_DEFAULTS_KEY='wise_quote_user_defaults_v1'; const LOCAL_DELETED_HISTORY_KEY='wise_quote_deleted_history_v1'; let HISTORY_ALL_ROWS=[]; let HISTORY_EMPTY_MESSAGE='기록 없음'; let USER_DEFAULT_APPLYING=false; function wiseDeepClone(v){return JSON.parse(JSON.stringify(v));} function getUserDefaults(){try{const v=JSON.parse(localStorage.getItem(LOCAL_USER_DEFAULTS_KEY)||'{}');return v&&typeof v==='object'?v:{}}catch(e){return {}}} function setUserDefaults(v){try{localStorage.setItem(LOCAL_USER_DEFAULTS_KEY,JSON.stringify(v||{}))}catch(e){}} function currentDefaultKey(){ if($('quoteType')&&$('quoteType').value==='FCL') return 'FCL_'+(($('fclTemplate')&&$('fclTemplate').value)||($('fclContainer')&&$('fclContainer').value)||'DEFAULT'); return 'LCL_'+(($('mode')&&$('mode').value)||'AIR')+'_'+(($('delivery')&&$('delivery').value)||'SPD'); } function currentDefaultLabel(){return currentDefaultKey().replace(/^LCL_/,'').replace(/^FCL_/,'FCL ')} function updateDefaultStatus(){ const el=$('defaultStatus');if(!el)return; const exists=!!getUserDefaults()[currentDefaultKey()]; el.textContent=exists?'기본값 저장됨 · '+currentDefaultLabel():'기본값 없음 · '+currentDefaultLabel(); el.style.background=exists?'#e9fff3':'#f2f6ff';el.style.color=exists?'#087143':'#234bd3'; } function sanitizeLclDefault(raw){ const d=wiseDeepClone(raw||{}); d.customer='';d.attention='';d.issueDate='';d.expiryDate='';d.cbm='';d.kg='';d.boxes='';d.pallet='';d.labelQty='';d.ciValue=''; return d; } function sanitizeFclDefault(raw){ const d=wiseDeepClone(raw||{}); d.customer='';d.attention='';d.issueDate='';d.expiryDate='';d.quoteNo='TBA';d.mbl='TBA';d.pcs='TBA';d.finalAddress='';d.targetTotal=''; return d; } function captureCurrentDefault(){ if($('quoteType')&&$('quoteType').value==='FCL'){ if(!window.fclMakePayload)throw new Error('FCL 입력폼이 아직 준비되지 않았습니다.'); return {type:'FCL',data:sanitizeFclDefault(window.fclMakePayload().inputObj)}; } return {type:'LCL',data:sanitizeLclDefault(makePayload().inputObj)}; } function prepareDefaultForLoad(item){ const d=wiseDeepClone(item.data||{}); if(item.type==='FCL'){ d.customer='';d.attention='';d.issueDate=today();d.expiryDate=addDays(7);d.quoteNo=d.quoteNo||'TBA';d.mbl=d.mbl||'TBA';d.pcs=d.pcs||'TBA';d.finalAddress='';d.targetTotal=''; }else{ d.customer='';d.attention='';d.issueDate=today();d.expiryDate=addDays(14);d.cbm='';d.kg='';d.boxes='';d.pallet='';d.labelQty='';d.ciValue=''; } return d; } function showFeatureMessage(text,isError=false){ const ok=$('saved'),err=$('err');if(ok)ok.style.display='none';if(err)err.style.display='none'; const el=isError?err:ok;if(el){el.textContent=text;el.style.display='block';} } function saveCurrentDefaults(){ try{ const key=currentDefaultKey(),all=getUserDefaults();all[key]=captureCurrentDefault();setUserDefaults(all);updateDefaultStatus(); showFeatureMessage('현재 설정을 '+currentDefaultLabel()+' 기본값으로 저장했습니다. 고객명, 날짜, 수량, 화물별 정보는 새 견적마다 비워집니다.'); }catch(e){showFeatureMessage('기본값 저장 실패: '+(e.message||e),true)} } function clearCurrentDefaults(){ const key=currentDefaultKey(),all=getUserDefaults(); if(!all[key]){showFeatureMessage(currentDefaultLabel()+'에 저장된 사용자 기본값이 없습니다.',true);return;} if(!confirm(currentDefaultLabel()+' 사용자 기본값을 삭제하고 프로그램 기본값으로 돌아갈까요?'))return; delete all[key];setUserDefaults(all);updateDefaultStatus(); USER_DEFAULT_APPLYING=true;try{resetForm();}finally{USER_DEFAULT_APPLYING=false} showFeatureMessage(currentDefaultLabel()+' 사용자 기본값을 삭제했습니다.'); } function applySavedDefaultForCurrentSheet(showMessage=false){ if(USER_DEFAULT_APPLYING)return false; const key=currentDefaultKey(),item=getUserDefaults()[key];updateDefaultStatus();if(!item)return false; USER_DEFAULT_APPLYING=true; try{ const d=prepareDefaultForLoad(item); if(item.type==='FCL'){ if($('quoteType'))$('quoteType').value='FCL'; if(window.fclLoadInput)window.fclLoadInput(d);else return false; }else{ if($('quoteType'))$('quoteType').value='LCL'; loadLclInput(d); } updateDefaultStatus();if(showMessage)showFeatureMessage(currentDefaultLabel()+' 저장 기본값을 불러왔습니다.');return true; }catch(e){if(showMessage)showFeatureMessage('기본값 불러오기 실패: '+(e.message||e),true);return false} finally{USER_DEFAULT_APPLYING=false} } function installDefaultToolbar(){ const toolbar=document.querySelector('.toolbar');if(!toolbar||$('saveDefaultsBtn'))return; const resetBtn=Array.from(toolbar.querySelectorAll('button')).find(b=>/입력 초기화/.test(b.textContent)); const saveBtn=document.createElement('button');saveBtn.id='saveDefaultsBtn';saveBtn.className='btn';saveBtn.type='button';saveBtn.textContent='현재 설정 기본값 저장';saveBtn.onclick=saveCurrentDefaults; const clearBtn=document.createElement('button');clearBtn.id='clearDefaultsBtn';clearBtn.className='btn';clearBtn.type='button';clearBtn.textContent='기본값 삭제';clearBtn.onclick=clearCurrentDefaults; const status=document.createElement('span');status.id='defaultStatus';status.className='pill default-status-pill';status.textContent='기본값 확인 중'; toolbar.insertBefore(saveBtn,resetBtn||null);toolbar.insertBefore(clearBtn,resetBtn||null);toolbar.insertBefore(status,resetBtn||null); updateDefaultStatus(); } function bindDefaultChangeEvents(){ ['mode','delivery','quoteType','fclTemplate','fclContainer','fclBranch'].forEach(id=>{ const el=$(id);if(!el||el.dataset.defaultBound)return;el.dataset.defaultBound='1'; el.addEventListener('change',()=>{if(USER_DEFAULT_APPLYING)return;setTimeout(()=>{applySavedDefaultForCurrentSheet(false);updateDefaultStatus()},40)}); }); } function historyIdentity(row){const r=normalizeHistoryRow(row);return [r[0],deriveJsonName(r),r[4],r[5]].join('|')} function getDeletedHistorySet(){try{const a=JSON.parse(localStorage.getItem(LOCAL_DELETED_HISTORY_KEY)||'[]');return new Set(Array.isArray(a)?a:[])}catch(e){return new Set()}} function setDeletedHistorySet(set){try{localStorage.setItem(LOCAL_DELETED_HISTORY_KEY,JSON.stringify(Array.from(set).slice(-1000)))}catch(e){}} function removeLocalPayload(name){try{const obj=getLocalPayloadMap();delete obj[name];localStorage.setItem(LOCAL_PAYLOAD_KEY,JSON.stringify(obj))}catch(e){}} function historyDate(row){return String(normalizeHistoryRow(row)[0]||'').slice(0,10)} function historyFilteredRows(){ const from=($('historyDateFrom')&&$('historyDateFrom').value)||''; const to=($('historyDateTo')&&$('historyDateTo').value)||''; return HISTORY_ALL_ROWS.filter(r=>{const d=historyDate(r);if(from&&dto)return false;return true}); } function drawHistoryRows(){ const body=$('recordsBody');if(!body)return; const arr=historyFilteredRows();const count=$('historyCount');if(count)count.textContent=(($('historyDateFrom')&&$('historyDateFrom').value)||($('historyDateTo')&&$('historyDateTo').value))?'조회 '+arr.length+'개 / 전체 '+HISTORY_ALL_ROWS.length+'개':'전체 '+HISTORY_ALL_ROWS.length+'개'; if(!arr.length){body.innerHTML=''+esc(HISTORY_EMPTY_MESSAGE||'기록 없음')+'';return;} body.innerHTML=arr.map(r=>{const json=deriveJsonName(r),disabled=json?'':' disabled',key=encodeURIComponent(historyIdentity(r));return `${esc(r[0])}${esc(r[1])}${esc(r[2])}${esc(r[3])}${esc(r[4])}${esc(r[5])}`}).join(''); } renderRecords=function(arr,msg){ const deleted=getDeletedHistorySet();HISTORY_EMPTY_MESSAGE=msg||'기록 없음'; HISTORY_ALL_ROWS=(arr||[]).map(normalizeHistoryRow).filter(r=>!deleted.has(historyIdentity(r))); drawHistoryRows(); }; function applyHistoryDateFilter(){ const from=($('historyDateFrom')&&$('historyDateFrom').value)||'',to=($('historyDateTo')&&$('historyDateTo').value)||''; if(from&&to&&from>to){showFeatureMessage('히스토리 시작일은 종료일보다 늦을 수 없습니다.',true);return;} HISTORY_EMPTY_MESSAGE='선택한 기간에 저장 기록이 없습니다.';drawHistoryRows(); const box=$('historyTableScroll');if(box)box.scrollTop=0; } function resetHistoryDateFilter(){if($('historyDateFrom'))$('historyDateFrom').value='';if($('historyDateTo'))$('historyDateTo').value='';HISTORY_EMPTY_MESSAGE='기록 없음';drawHistoryRows();const box=$('historyTableScroll');if(box)box.scrollTop=0;} async function historyFileExists(dir,name){if(!name)return true;try{await dir.getFileHandle(name,{create:false});return true}catch(e){return false}} function historyCsvText(rows){let s='저장일시,Customer,양식,총액,HTML,Excel,GIBAEK\n';return s+(rows||[]).map(r=>normalizeHistoryRow(r).map(csvEscape).join(',')).join('\n')+((rows||[]).length?'\n':'')} loadRecords=async function(){ const deleted=getDeletedHistorySet();let dir=await getWritableFolder(false); if(dir){ try{ const text=await readTextFile(dir,'저장기록.csv'); if(text.trim()){ let chronological=parseCSV(text).map(normalizeHistoryRow); const checked=await Promise.all(chronological.map(async r=>({r,ok:!deleted.has(historyIdentity(r))&&await historyFileExists(dir,deriveJsonName(r))}))); const clean=checked.filter(x=>x.ok).map(x=>x.r); if(clean.length!==chronological.length){try{await writeTextFile(dir,'저장기록.csv',historyCsvText(clean))}catch(e){}} setLocalHistory(clean); if(clean.length){renderRecords(clean.slice().reverse());return} } }catch(e){} } const local=getLocalHistory().filter(r=>!deleted.has(historyIdentity(r))).reverse(); if(local.length){renderRecords(local);return} renderRecords([],(dir?'기록 없음':'저장 폴더를 선택하면 이곳에 기록이 표시됩니다.')); }; async function deleteHistoryRecordFromButton(btn){ if(!btn)return;const key=decodeURIComponent(btn.dataset.historyKey||'');const row=HISTORY_ALL_ROWS.find(r=>historyIdentity(r)===key);if(!row)return; if(!confirm('이 저장 기록을 히스토리에서 삭제할까요? 생성된 HTML / Excel 파일은 그대로 유지됩니다.'))return; btn.disabled=true; try{ const deleted=getDeletedHistorySet();deleted.add(key);setDeletedHistorySet(deleted); const remainingLocal=getLocalHistory().filter(r=>historyIdentity(r)!==key);setLocalHistory(remainingLocal); const jsonName=deriveJsonName(row);if(!remainingLocal.some(r=>deriveJsonName(r)===jsonName))removeLocalPayload(jsonName); const dir=await getWritableFolder(false); if(dir){ try{const text=await readTextFile(dir,'저장기록.csv');const rows=parseCSV(text).map(normalizeHistoryRow).filter(r=>historyIdentity(r)!==key);await writeTextFile(dir,'저장기록.csv',historyCsvText(rows));}catch(e){} } HISTORY_ALL_ROWS=HISTORY_ALL_ROWS.filter(r=>historyIdentity(r)!==key);drawHistoryRows();showFeatureMessage('선택한 저장 기록을 히스토리에서 삭제했습니다.'); }catch(e){showFeatureMessage('저장 기록 삭제 실패: '+(e.message||e),true);btn.disabled=false} } function installDefaultResetWrapper(){ const current=window.resetForm;if(!current||current._wiseUserDefaultWrapped)return; const baseReset=current; const wrapped=function(){ USER_DEFAULT_APPLYING=true; try{const result=baseReset.apply(this,arguments);setTimeout(()=>{USER_DEFAULT_APPLYING=false;applySavedDefaultForCurrentSheet(false);updateDefaultStatus()},30);return result} catch(e){USER_DEFAULT_APPLYING=false;throw e} }; wrapped._wiseUserDefaultWrapped=true;window.resetForm=wrapped; } installDefaultToolbar(); if($('historyDateFrom'))$('historyDateFrom').addEventListener('change',applyHistoryDateFilter); if($('historyDateTo'))$('historyDateTo').addEventListener('change',applyHistoryDateFilter); window.addEventListener('load',()=>{ let tries=0;const timer=setInterval(()=>{installDefaultToolbar();installDefaultResetWrapper();bindDefaultChangeEvents();updateDefaultStatus();if(++tries>12)clearInterval(timer)},120); setTimeout(()=>{installDefaultResetWrapper();bindDefaultChangeEvents();applySavedDefaultForCurrentSheet(false);updateDefaultStatus();loadRecords()},520); });