396 lines
22 KiB
HTML
396 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<title>Work Order — Print</title>
|
|
<script src="/auth-guard.js?v=4"></script>
|
|
<script src="/company-list.js"></script>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:Arial,Helvetica,sans-serif;background:#8a94a6;color:#000;font-size:10px}
|
|
#doc{padding:14px}
|
|
.loading{padding:40px;text-align:center;color:#fff;font-size:14px}
|
|
.toolbar{position:sticky;top:0;display:flex;gap:8px;justify-content:center;padding:8px;background:#0d1626;z-index:5}
|
|
.toolbar button{padding:7px 16px;border:none;border-radius:6px;font-size:12px;font-weight:700;cursor:pointer;font-family:inherit}
|
|
.tb-print{background:#1a6fff;color:#fff}
|
|
.tb-close{background:#243650;color:#c8d6e8}
|
|
.page{width:277mm;min-height:auto;background:#fff;margin:0 auto 16px;padding:6mm;border:1px solid #999}
|
|
table{border-collapse:collapse;width:100%}
|
|
td,th{border:1px solid #000;padding:2px 4px;vertical-align:middle;font-size:9px}
|
|
.mono{font-family:'Courier New',monospace}
|
|
.l{text-align:left}
|
|
/* Header band */
|
|
table.hdr td{text-align:center}
|
|
table.hdr .logo{width:120px}
|
|
table.hdr .logo img{max-height:46px;max-width:112px;object-fit:contain}
|
|
.co-name{font-size:16px;font-weight:800;letter-spacing:.3px}
|
|
.co-addr{font-size:10px}
|
|
table.hdr .k{font-weight:700;background:#f2f2f2;white-space:nowrap}
|
|
.title-bar{border:1px solid #000;border-top:none;text-align:center;font-weight:800;font-size:11px;padding:3px;background:#f2f2f2}
|
|
/* Info box */
|
|
table.info{border-top:none}
|
|
table.info .k{font-weight:700;background:#f2f2f2;white-space:nowrap;width:130px}
|
|
/* Materials */
|
|
.sol-cap{font-weight:700;font-size:9.5px;padding:3px 2px 1px;text-align:center;background:#f2f2f2;border:1px solid #000;border-bottom:none}
|
|
table.mt{margin-top:6px}
|
|
table.mt th{background:#f2f2f2;font-weight:700;text-align:center;font-size:8.5px;line-height:1.1}
|
|
table.mt td{text-align:center;font-size:8.5px;height:24px}
|
|
table.mt td.l{text-align:left}
|
|
.sec-gap{height:8px}
|
|
.sig-cell{text-align:center;vertical-align:middle}
|
|
.sig-cell img.sig-img{max-height:34px;max-width:110px;object-fit:contain;display:block;margin:0 auto 1px}
|
|
.sig-name{font-size:8px;font-weight:700}
|
|
.sig-dt{font-size:7.5px;color:#333}
|
|
/* Sign-off block */
|
|
table.signoff{margin-top:8px}
|
|
table.signoff .k{font-weight:700;width:230px;background:#f2f2f2;height:34px}
|
|
table.signoff .sig{height:34px}
|
|
table.signoff .sig img{max-height:30px;max-width:150px;object-fit:contain;vertical-align:middle}
|
|
table.signoff .sn{font-size:8.5px;font-weight:700}
|
|
table.signoff .sd{font-size:8px;color:#333}
|
|
@media print{
|
|
body{background:#fff}
|
|
.toolbar{display:none}
|
|
#doc{padding:0}
|
|
.page{width:auto;margin:0;border:none;padding:0}
|
|
.page + .page{page-break-before:always}
|
|
}
|
|
@page{size:A4 landscape;margin:8mm}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="toolbar">
|
|
<button class="tb-print" onclick="window.print()">🖨 Print / Save as PDF</button>
|
|
<button class="tb-close" onclick="window.close()">Close</button>
|
|
</div>
|
|
<div id="doc"><div class="loading">Loading work order…</div></div>
|
|
|
|
<script>
|
|
'use strict';
|
|
const BE='/api';
|
|
let _tok=null;
|
|
try{_tok=sessionStorage.getItem('portal_token');}catch(_e){}
|
|
if(!_tok)window.location.href='/';
|
|
const _params=new URLSearchParams(location.search);
|
|
const _id=_params.get('id');
|
|
const _co=window.__DEFAULT_COMPANY__||'';
|
|
|
|
function esc(v){return(v==null?'':String(v)).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
|
const _MON=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
function fmtD(iso){if(!iso)return '';const d=new Date(iso);if(isNaN(d))return '';return `${String(d.getDate()).padStart(2,'0')}-${_MON[d.getMonth()]}-${d.getFullYear()}`;}
|
|
function num3(v){if(v==null||v==='')return '';const n=Number(v);if(!isFinite(n)||n===0)return '';return String(Math.round(n*1000)/1000);}
|
|
|
|
// Raw Material's "Qty Req./100 ml" / "Qty Req." auto-scale for display —
|
|
// EXACT mirror of work-order.html's own "View" scaling (that's the
|
|
// reference this must match). Read-only here: this page never edits
|
|
// anything, it only needs to reproduce the same scaled numbers/units the
|
|
// app itself shows on-screen (Weight family mg/gm/Kg/Ton, Volume family
|
|
// µL/mL/Ltrs/KL — ratios are IDENTICAL between the two, so the same
|
|
// underlying number relabels cleanly as either), honoring each row's own
|
|
// saved display-unit choice (stdQtyDispUnit/qtyReqDispUnit) first and
|
|
// falling back to the auto-picked best-fit unit within the item's own
|
|
// family (volume for a liquid real UOM like LTR, weight otherwise).
|
|
function round3(n){return Math.round((Number(n)||0)*1000)/1000;}
|
|
function toStockQty(grams,uom){const u=(uom||'').toUpperCase().trim();
|
|
return (u.startsWith('KG')||u.startsWith('LTR')||u.startsWith('LITR')||u==='L'||u==='LT'||u==='KL')?grams/1000:grams;}
|
|
function fromStockQty(qty,uom){const u=(uom||'').toUpperCase().trim();
|
|
return (u.startsWith('KG')||u.startsWith('LTR')||u.startsWith('LITR')||u==='L'||u==='LT'||u==='KL')?qty*1000:qty;}
|
|
const UNIT_FAMILIES={
|
|
volume:{'µL':1,'mL':1000,'Ltrs':1e6,'KL':1e9},
|
|
weight:{'mg':1,'gm':1000,'Kg':1e6,'Ton':1e9},
|
|
};
|
|
function defaultRawFamily(uom){
|
|
const u=(uom||'').trim().toUpperCase();
|
|
return ['LTR','LTRS','LITRE','LITER','LITRES','LITERS','ML','KL','L'].includes(u)?'volume':'weight';
|
|
}
|
|
const RAW_UNIT_OPTIONS={...UNIT_FAMILIES.weight,...UNIT_FAMILIES.volume};
|
|
function scaleGrams(grams,uom){
|
|
const totalBase=(Number(grams)||0)*1000;
|
|
const units=UNIT_FAMILIES[defaultRawFamily(uom)];
|
|
const entries=Object.entries(units).sort((a,b)=>b[1]-a[1]);
|
|
for(const[u,mult] of entries){ if(totalBase>=mult) return {value:round3(totalBase/mult),label:u}; }
|
|
const smallest=entries[entries.length-1];
|
|
return {value:round3(totalBase/smallest[1]),label:smallest[0]};
|
|
}
|
|
function scaleGramsToUnit(grams,unit){
|
|
const mult=RAW_UNIT_OPTIONS[unit]!=null?RAW_UNIT_OPTIONS[unit]:1000;
|
|
return round3((Number(grams)||0)*1000/mult);
|
|
}
|
|
// Admin-configured per-item SOP display factor — see work-order.html's
|
|
// potencyFactor() for the full rationale. Applies ONLY to stdQty ("Qty
|
|
// Req./100 ml"); qtyReq ("Qty Req.") is never adjusted.
|
|
function potencyFactor(itemCode){
|
|
const p=parseFloat((_cfg.woRawPotencyFactors||{})[itemCode]);
|
|
return (p>0)?p/100:1;
|
|
}
|
|
// srcField lets a caller display a DIFFERENT row value (e.g. the persisted
|
|
// "qtyIssued") using 'field''s own unit/scaling conventions — used by the Qty
|
|
// Issued column for Raw Material rows (see rawTables() below).
|
|
function rawScaledDisp(r,field,noUnit,srcField){
|
|
const vf=srcField||field;
|
|
if(r.directRaw)return esc(r[vf]);
|
|
const dispField=field==='stdQty'?'stdQtyDispUnit':'qtyReqDispUnit';
|
|
const raw=parseFloat(r[vf])||0;
|
|
const grams=field==='stdQty'?raw*potencyFactor(r.itemCode):fromStockQty(raw,r.uom);
|
|
const unit=r[dispField]||scaleGrams(grams,r.uom).label;
|
|
const value=scaleGramsToUnit(grams,unit);
|
|
return noUnit?String(value):`${value} ${esc(unit)}`;
|
|
}
|
|
// Scaled unit label alone, for the UOM column — shows Qty Req./100 ml's
|
|
// unit (the user's own chosen one, if set) instead of the item's static SAP
|
|
// stock UOM.
|
|
function rawScaledUnit(r){
|
|
if(r.directRaw)return esc(r.uom);
|
|
const grams=(parseFloat(r.stdQty)||0)*potencyFactor(r.itemCode);
|
|
return esc(r.stdQtyDispUnit||scaleGrams(grams,r.uom).label);
|
|
}
|
|
|
|
async function api(p){
|
|
const r=await fetch(BE+p,{headers:{Authorization:'Bearer '+_tok}});
|
|
if(r.status===401){window.location.href='/';throw new Error('Session expired');}
|
|
const d=await r.json();
|
|
if(!r.ok||d.success===false)throw new Error(d.message||('HTTP '+r.status));
|
|
return d;
|
|
}
|
|
|
|
let _cfg={},_wo=null,_iss={qtyByItem:{}},_sig={},_newItemCode='';
|
|
let _woQtyIssuedSource='issue_for_production'; // Admin → System Settings — see issueRowCells()
|
|
|
|
// ── header / info / signoff (repeated per page) ────────────────────
|
|
function headerBand(){
|
|
const logo=_cfg.woLogo?`<img src="${_cfg.woLogo}"/>`:'';
|
|
return `<table class="hdr">
|
|
<tr>
|
|
<td class="logo" rowspan="2">${logo}</td>
|
|
<td colspan="6"><div class="co-name">${esc(_cfg.woCompanyName||'')}</div><div class="co-addr">${esc(_cfg.woCompanyAddress||'')}</div></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="k">FORM NO.</td><td>${esc(_cfg.woFormNo||'')}</td>
|
|
<td class="k">EFFECTIVE DATE</td><td>${esc(_cfg.woEffectiveDate||'')}</td>
|
|
<td class="k">REVIEW DATE</td><td>${esc(_cfg.woReviewDate||'')}</td>
|
|
</tr>
|
|
</table>`;
|
|
}
|
|
function titleBar(suffix){return `<div class="title-bar">PRODUCTION WORK ORDER${suffix||''}</div>`;}
|
|
function infoBox(){
|
|
const w=_wo;
|
|
const pn=[w.productName,w.productDesc].filter(Boolean).join(' — ');
|
|
return `<table class="info">
|
|
<tr><td class="k">Product Name & Description</td><td colspan="5" class="l">${esc(pn)}</td></tr>
|
|
<tr><td class="k">Generic Name</td><td colspan="5" class="l">${esc(w.genericName||'')}</td></tr>
|
|
<tr><td class="k">Batch Number</td><td class="l">${esc(w.batchNumber||'')}</td><td class="k">Batch Size</td><td class="l">${esc(w.batchSize||'')}</td><td class="k">Total Units</td><td class="l">${esc(w.totalUnits||'')}</td></tr>
|
|
<tr><td class="k">MFG. Date</td><td class="l">${esc(w.mfgDate||'')}</td><td class="k">EXP. Date</td><td class="l">${esc(w.expDate||'')}</td><td class="k">Pack Size</td><td class="l">${esc(w.packSize||'')}</td></tr>
|
|
<tr><td class="k">Reference</td><td class="l">${esc(w.reference||'')}</td><td class="k">Product code</td><td class="l">${esc(w.productCode||'')}${_newItemCode&&_newItemCode.trim().toUpperCase()!==(w.productCode||'').trim().toUpperCase()?` (${esc(_newItemCode)})`:''}</td><td class="k">Type</td><td class="l">${esc(w.type||'')}</td></tr>
|
|
<tr><td class="k">Market</td><td class="l">${esc(w.market||'')}</td><td class="k">Intimation No.</td><td colspan="3" class="l">${esc(w.intimationDocNo||'')}</td></tr>
|
|
</table>`;
|
|
}
|
|
// bottom five sign-offs, auto-filled from the WO workflow log. 'aliases'
|
|
// covers a step whose display name changed later (e.g. "Checked By
|
|
// Production" → "Checked By Store In-Charge") — older Work Orders'
|
|
// workflowLog entries were recorded with the OLD text at the time (an
|
|
// immutable historical record), so the lookup must still match it.
|
|
const STEP_LABELS=[
|
|
{step:'Prepared By QA', label:'Prepared By (QA)'},
|
|
{step:'Checked By QC', label:'Checked By (QC)'},
|
|
{step:'Checked By Store In-Charge', aliases:['Checked By Production'], label:'Checked By (Store In-Charge)'},
|
|
{step:'Checked By (Manager Production)',label:'Checked By (Manager Production)'},
|
|
{step:'Approved By (Manager QA)', label:'Approved By (Manager QA)'},
|
|
];
|
|
function logFor(step,aliases){
|
|
const log=Array.isArray(_wo.workflowLog)?_wo.workflowLog:[];
|
|
// last non-rejected, non-auto entry matching this step
|
|
for(let i=log.length-1;i>=0;i--){const l=log[i];if((l.step===step||(aliases||[]).includes(l.step))&&l.action!=='rejected'&&l.action!=='auto-skipped')return l;}
|
|
return null;
|
|
}
|
|
function signInline(user,name,at){
|
|
const img=user&&_sig[user]?`<img src="${_sig[user]}"/> `:'';
|
|
const nm=name?`<span class="sn">${esc(name)}</span>`:'';
|
|
const dt=at?` <span class="sd">${fmtD(at)}</span>`:'';
|
|
return img+nm+dt;
|
|
}
|
|
function signoffs(){
|
|
// A step this WO auto-skipped (e.g. "Checked By QC" while workOrderSkipQc
|
|
// is on) is left off the printed list — matches the stepper on the Work
|
|
// Order page itself and the server-generated PDF.
|
|
const log=Array.isArray(_wo.workflowLog)?_wo.workflowLog:[];
|
|
const skipped=new Set(log.filter(l=>l.action==='auto-skipped').map(l=>l.step));
|
|
const visible=STEP_LABELS.filter(s=>!skipped.has(s.step)&&!(s.aliases||[]).some(a=>skipped.has(a)));
|
|
return `<table class="signoff">${visible.map(s=>{
|
|
const l=logFor(s.step,s.aliases);
|
|
return `<tr><td class="k">${s.label}</td><td class="sig">${l?signInline(l.by,l.byName||l.by,l.at):''}</td></tr>`;
|
|
}).join('')}</table>`;
|
|
}
|
|
|
|
// ── issuance columns — Qty stays SAP-derived per item; Issued/Received/
|
|
// Verified are PER-ROW one-click stamps (see verify-work-order.html) since
|
|
// different items can be issued/received/verified on different days. ───────
|
|
function qi(code){return num3(_iss.qtyByItem?.[(code||'').trim()]);}
|
|
function rowSig(row,which){ const at=row[which+'At']; if(!at)return null; return {name:row[which+'ByName']||row[which+'By'],user:row[which+'By'],at}; }
|
|
function sigTd(sig){
|
|
if(!sig||!sig.name)return `<td></td>`;
|
|
const img=sig.user&&_sig[sig.user]?`<img class="sig-img" src="${_sig[sig.user]}"/>`:'';
|
|
return `<td class="sig-cell">${img}<div class="sig-name">${esc(sig.name)}</div><div class="sig-dt">${fmtD(sig.at)}</div></td>`;
|
|
}
|
|
function issueHeadCells(){return `<th>Qty issued</th><th>Issued by /date</th><th>Received by</th><th>Verified by</th>`;}
|
|
// Raw Material rows are never SAP Production Order lines themselves (they're
|
|
// the exploded recipe of a Solution/SFG item, which IS the PO line), so their
|
|
// Qty Issued always comes from the server-PERSISTED row.qtyIssued (per Admin
|
|
// → System Settings → "Raw Material Qty Issued Source"). Packing
|
|
// Material/Components rows ARE real PO lines, so under the default
|
|
// 'issue_for_production' they read live off qi()/iss.qtyByItem; only under
|
|
// 'mark_issued' do they ALSO switch to the persisted row.qtyIssued.
|
|
function issueRowCells(row,isRaw){
|
|
const usePersisted=isRaw||_woQtyIssuedSource==='mark_issued';
|
|
const qty=usePersisted?(row.qtyIssued?(isRaw?rawScaledDisp(row,'qtyReq',false,'qtyIssued'):num3(row.qtyIssued)):''):qi(row.itemCode);
|
|
return `<td>${qty}</td>${sigTd(rowSig(row,'issued'))}${sigTd(rowSig(row,'received'))}${sigTd(rowSig(row,'verified'))}`;
|
|
}
|
|
|
|
// ── material tables ────────────────────────────────────────────────
|
|
function rawTables(){
|
|
const map=new Map();
|
|
// solBatchSize can legitimately be 0 — check null/undefined, not truthiness.
|
|
(_wo.rawMaterials||[]).forEach(r=>{const k=r.solCode||'';if(!map.has(k))map.set(k,{name:r.solName||'',batchSize:r.solBatchSize!=null?r.solBatchSize:'',rows:[]});map.get(k).rows.push(r);});
|
|
return [...map.values()].map(g=>{
|
|
const cap=g.name?`<div class="sol-cap">${esc(g.name)}${g.batchSize!==''?` — Batch Size ${esc(g.batchSize)} Ltr`:''}</div>`:'';
|
|
return cap+`<table class="mt">
|
|
<thead><tr><th>Item code</th><th>Raw materials</th><th>Spec</th><th>Qty Req./100 ml</th><th>UOM</th><th>Ovg.</th><th>Qty Req.</th><th>Weighing Balance ID</th><th>AR No.</th>${issueHeadCells()}</tr></thead>
|
|
<tbody>${g.rows.map((r)=>`<tr>
|
|
<td class="mono">${esc(r.itemCode)}</td><td class="l">${esc(r.rawMaterial)}</td><td>${esc(r.spec)}</td><td>${rawScaledDisp(r,'stdQty',true)}</td><td>${rawScaledUnit(r)}</td><td>${esc(r.ovg)}</td><td>${rawScaledDisp(r,'qtyReq')}</td><td>${esc(r.weighingBalanceId)}</td><td>${esc(r.arNo)}</td>${issueRowCells(r,true)}
|
|
</tr>`).join('')}</tbody></table>`;
|
|
}).join('<div class="sec-gap"></div>');
|
|
}
|
|
function packTable(){
|
|
const rows=_wo.packingMaterials||[];
|
|
const body=rows.length?rows.map((r)=>{
|
|
const std=r.stdQtyUnit?`${esc(r.stdQtyPerUnit)} ${esc(r.stdQtyUnit)}`:esc(r.stdQtyPerUnit);
|
|
const qty=r.qtyUnitLabel?`${esc(r.qtyReqUnits)} ${esc(r.qtyUnitLabel)}`:esc(r.qtyReqUnits);
|
|
return `<tr><td class="mono">${esc(r.itemCode)}</td><td class="l">${esc(r.packingMaterial)}</td><td>${esc(r.artworkNo)}</td><td>${std}</td><td>${esc(r.uom||'-')}</td><td>${esc(r.ovgPercent)}</td><td>${qty}</td><td>${esc(r.arNo||'')}</td>${issueRowCells(r)}</tr>`;
|
|
}).join(''):`<tr><td colspan="12">—</td></tr>`;
|
|
return `<table class="mt">
|
|
<thead><tr><th>Item code</th><th>Packing Materials</th><th>Artwork No.</th><th>Std. Qty/Unit</th><th>UOM</th><th>Ovg.%</th><th>Qty Req.(Units)</th><th>AR No.</th>${issueHeadCells()}</tr></thead>
|
|
<tbody>${body}</tbody></table>`;
|
|
}
|
|
function componentsTable(){
|
|
const rows=_wo.packingMaterials||[]; // components-only mode stores rows here
|
|
const body=rows.length?rows.map((r)=>{
|
|
const std=r.stdQtyUnit?`${esc(r.stdQtyPerUnit)} ${esc(r.stdQtyUnit)}`:esc(r.stdQtyPerUnit);
|
|
const qty=r.qtyUnitLabel?`${esc(r.qtyReqUnits)} ${esc(r.qtyUnitLabel)}`:esc(r.qtyReqUnits);
|
|
return `<tr><td class="mono">${esc(r.itemCode)}</td><td class="l">${esc(r.packingMaterial)}</td><td>${std}</td><td>${esc(r.uom||'-')}</td><td>${esc(r.ovgPercent)}</td><td>${qty}</td><td>${esc(r.arNo||'')}</td>${issueRowCells(r)}</tr>`;
|
|
}).join(''):`<tr><td colspan="11">—</td></tr>`;
|
|
return `<table class="mt">
|
|
<thead><tr><th>Item code</th><th>Components</th><th>Std. Qty/Unit</th><th>UOM</th><th>Ovg.</th><th>Qty Req.</th><th>AR No.</th>${issueHeadCells()}</tr></thead>
|
|
<tbody>${body}</tbody></table>`;
|
|
}
|
|
|
|
function page(suffix,tablesHtml){
|
|
return `<div class="page">${headerBand()}${titleBar(suffix)}${infoBox()}${tablesHtml}${signoffs()}</div>`;
|
|
}
|
|
|
|
async function loadSignatures(){
|
|
const users=new Set();
|
|
(Array.isArray(_wo.workflowLog)?_wo.workflowLog:[]).forEach(l=>{if(l.by)users.add(l.by);});
|
|
[...(_wo.rawMaterials||[]),...(_wo.packingMaterials||[])].forEach(r=>{
|
|
['issuedBy','receivedBy','verifiedBy'].forEach(k=>{if(r[k])users.add(r[k]);});
|
|
});
|
|
await Promise.all([...users].map(async u=>{
|
|
try{const r=await api(`/users/signature/by-username/${encodeURIComponent(u)}`);if(r.signature)_sig[u]=r.signature;}catch(_e){}
|
|
}));
|
|
}
|
|
|
|
function render(){
|
|
const w=_wo;
|
|
// Authoritative flag, not inferred from rawMaterials.length — a
|
|
// componentsOnly save also writes a legacy-compatible copy of the same
|
|
// components into rawMaterials, so its length alone can't be used to tell
|
|
// components-only orders apart from real split Raw/Packing ones.
|
|
const componentsMode=!!w.componentsOnly;
|
|
let html;
|
|
if(componentsMode){
|
|
html=page('',componentsTable());
|
|
}else{
|
|
html=page(' (RAW MATERIAL)',rawTables())+page(' (PACKING MATERIAL)',packTable());
|
|
}
|
|
document.getElementById('doc').innerHTML=html;
|
|
document.title=`WO ${w.woNo||_id} — ${w.productCode||''}`;
|
|
}
|
|
|
|
// Work Order Header Profiles (Blood Bag / CAPD / Accessories / …) — resolve
|
|
// which profile applies from the WO's product's SAP item group, and use its
|
|
// Company Name/Address/FORM NO./dates/Logo instead of the old single flat
|
|
// System Settings values. Falls back to whichever profile is marked
|
|
// Default; if the profile list can't be loaded at all (e.g. very first
|
|
// request before the one-time migration seed has run), _cfg's own wo*
|
|
// fields are left untouched — appSettingsStore.js still returns the same
|
|
// legacy values as a last-resort fallback.
|
|
async function applyHeaderProfile(){
|
|
try{
|
|
const [profRes,grpRes]=await Promise.all([
|
|
api('/wo-header-profiles'),
|
|
api(`/sap/lookup/item-groups-for?codes=${encodeURIComponent(_wo.productCode||'')}&company=${encodeURIComponent(_co)}`),
|
|
]);
|
|
const profiles=profRes.data||[];
|
|
if(!profiles.length)return;
|
|
const groupCode=String((grpRes.data||{})[_wo.productCode]??'');
|
|
const match=profiles.find(p=>groupCode&&p.itemGroups.includes(groupCode))||profiles.find(p=>p.isDefault)||profiles[0];
|
|
if(match){
|
|
// Only override a field the matched profile actually set — an empty
|
|
// profile field (e.g. no logo ever uploaded to it) must NOT blank out
|
|
// the perfectly good value /config already put in _cfg (the legacy
|
|
// global System Settings fallback, which is also what the server-side
|
|
// "Download PDF" uses directly — that's why the PDF always had the
|
|
// logo while this print page silently lost it).
|
|
if(match.companyName) _cfg.woCompanyName=match.companyName;
|
|
if(match.companyAddress) _cfg.woCompanyAddress=match.companyAddress;
|
|
if(match.formNo) _cfg.woFormNo=match.formNo;
|
|
if(match.effectiveDate) _cfg.woEffectiveDate=match.effectiveDate;
|
|
if(match.reviewDate) _cfg.woReviewDate=match.reviewDate;
|
|
if(match.logo) _cfg.woLogo=match.logo;
|
|
}
|
|
}catch(_e){ /* fall back to whatever /config already put in _cfg */ }
|
|
}
|
|
|
|
// Logo (and signature) <img> tags can still be mid-load/decode the instant
|
|
// render() returns — a plain setTimeout() guess used to run print() before
|
|
// they'd actually painted, so the logo came out blank in the print/PDF-via-
|
|
// print-dialog output (the separate "Download PDF" path embeds the image
|
|
// server-side and never had this race). Wait for every <img> in the
|
|
// document to genuinely finish (or fail) loading instead of guessing.
|
|
function waitForImages(){
|
|
const imgs=[...document.querySelectorAll('#doc img')];
|
|
if(!imgs.length)return Promise.resolve();
|
|
return Promise.all(imgs.map(img=>{
|
|
if(img.complete&&img.naturalWidth>0)return Promise.resolve();
|
|
return new Promise(res=>{
|
|
img.addEventListener('load',res,{once:true});
|
|
img.addEventListener('error',res,{once:true});
|
|
setTimeout(res,2000); // safety net — never block printing indefinitely
|
|
});
|
|
}));
|
|
}
|
|
|
|
async function init(){
|
|
try{
|
|
const [cfg,wo]=await Promise.all([api('/config'),api(`/work-orders/${_id}?company=${encodeURIComponent(_co)}`)]);
|
|
_cfg=cfg||{};_wo=wo.data||wo;
|
|
if(_cfg.woQtyIssuedSource)_woQtyIssuedSource=_cfg.woQtyIssuedSource;
|
|
await applyHeaderProfile();
|
|
try{const is=await api(`/work-orders/${_id}/issuance-info?company=${encodeURIComponent(_co)}`);_iss=is.data||_iss;}catch(_e){}
|
|
// U_NewItemCode (OITM) for the header Product Code only — printed as
|
|
// "(code)" right after it, when SAP has a value.
|
|
try{const ni=await api(`/sap/lookup/new-item-codes?codes=${encodeURIComponent(_wo.productCode||'')}&company=${encodeURIComponent(_co)}`);
|
|
_newItemCode=(ni.data&&ni.data[_wo.productCode])||'';
|
|
}catch(_e){}
|
|
await loadSignatures();
|
|
render();
|
|
await waitForImages();
|
|
try{window.print();}catch(_e){}
|
|
}catch(e){
|
|
document.getElementById('doc').innerHTML=`<div class="loading">Could not load work order: ${esc(e.message)}</div>`;
|
|
}
|
|
}
|
|
if(!_id){document.getElementById('doc').innerHTML='<div class="loading">No work order id.</div>';}
|
|
else init();
|
|
</script>
|
|
</body>
|
|
</html>
|