Files
vda-to-edifact-converter/js/viewer.js
2026-03-13 09:53:40 +01:00

1389 lines
64 KiB
JavaScript

/**
* EDI Document Viewer
* Handles parsing and rendering of DELFOR and VDA documents in a human-readable format.
*/
window.EDIBridge = window.EDIBridge || {};
class Viewer {
constructor(app) {
this.app = app;
this.initElements();
this.bindEvents();
}
initElements() {
this.dropZone = document.getElementById('viewerDropZone');
this.fileInput = document.getElementById('viewerFileInput');
this.renderArea = document.getElementById('viewerDocumentRender');
this.btnPrint = document.getElementById('btnViewerPrint');
this.btnBack = document.getElementById('btnViewerBack');
this.dropText = document.getElementById('viewerDropText');
}
bindEvents() {
if (this.dropZone) {
this.dropZone.onclick = (e) => { e.stopPropagation(); if (this.fileInput) this.fileInput.click(); };
this.dropZone.ondragover = (e) => { e.preventDefault(); this.dropZone.classList.add('dragover'); };
this.dropZone.ondragleave = () => this.dropZone.classList.remove('dragover');
this.dropZone.ondrop = (e) => {
e.preventDefault();
this.dropZone.classList.remove('dragover');
this.handleFile(e.dataTransfer.files[0]);
};
}
if (this.fileInput) {
this.fileInput.onchange = (e) => this.handleFile(e.target.files[0]);
}
}
showViewer(html) {
this.renderArea.innerHTML = html;
this.dropZone.style.display = 'none';
this.renderArea.style.display = 'block';
if (this.btnPrint) this.btnPrint.style.display = 'inline-flex';
if (this.btnBack) this.btnBack.style.display = 'inline-flex';
if (window.lucide) {
try { lucide.createIcons(); } catch (e) { }
}
}
handleFile(file) {
if (!file) return;
this.fileName = file.name;
const reader = new FileReader();
reader.onload = (e) => {
this.processContent(e.target.result);
};
reader.readAsText(file);
}
processContent(content) {
const trimmed = content.trim();
const upper = trimmed.toUpperCase();
try {
if (upper.startsWith('UNA') || upper.startsWith('UNB')) {
// Determine EDIFACT type
if (upper.includes('DELFOR')) {
this.renderDelfor(trimmed);
} else if (upper.includes('DESADV')) {
this.renderDesadv(trimmed);
} else if (upper.includes('INVRPT')) {
this.renderInvrpt(trimmed);
} else {
alert('Dieser Nachrichtentyp wird aktuell vom Viewer nicht vollständig unterstützt. (Nur DELFOR, DESADV, INVRPT)');
// Fallback to basic view if possible?
}
} else if (upper.startsWith('711') || upper.startsWith('511')) {
// VDA Parsing
this.renderVDA(trimmed);
} else {
alert('Unbekanntes Dateiformat.');
}
} catch (error) {
console.error(error);
alert('Fehler beim Parsen der Datei: ' + error.message);
}
}
renderDelfor(content) {
const parser = window.EDIBridge.DelforParser;
if (!parser) throw new Error('DelforParser nicht geladen.');
const parsed = parser.parse(content);
// Extract basic data for the template
const data = this.extractDelforData(parsed);
this.renderTemplate(data);
}
extractDelforData(parsed) {
const segments = parsed.segments;
let header = {
docNo: '',
date: '',
buyer: { id: '', name: '', street: '', city: '' },
seller: { id: '', name: '', street: '', city: '' },
shipTo: { id: '', name: '', street: '', city: '', abladestelle: '' }
};
let articles = [];
let currentArticle = null;
const getVal = (seg, elIdx, compIdx) => {
if (!seg || !seg.elements) return '';
const el = seg.elements[elIdx];
if (!el) return '';
if (Array.isArray(el)) return el[compIdx] || '';
if (compIdx === 0) return el;
return '';
};
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
// Header extraction
if (seg.tag === 'BGM') {
header.docNo = getVal(seg, 1, 0); // e.g. 177754999
} else if (seg.tag === 'DTM') {
const qual = getVal(seg, 0, 0);
if (qual === '137') {
const dateStr = getVal(seg, 0, 1);
if (dateStr.length >= 8) {
header.date = dateStr.substring(6, 8) + '.' + dateStr.substring(4, 6) + '.' + dateStr.substring(0, 4);
}
}
} else if (seg.tag === 'NAD') {
const qual = getVal(seg, 0, 0);
const nadObj = {
id: getVal(seg, 1, 0), // party id
name: getVal(seg, 3, 0),
street: getVal(seg, 4, 0),
city: getVal(seg, 5, 0) + ' ' + getVal(seg, 7, 0) // city + zip (approx)
};
if (qual === 'BY') header.buyer = nadObj;
else if (qual === 'SE') header.seller = nadObj;
else if (qual === 'ST') header.shipTo = nadObj;
} else if (seg.tag === 'LOC') {
const qual = getVal(seg, 0, 0);
if (qual === '11') {
header.shipTo.abladestelle = getVal(seg, 1, 0);
}
}
// Articles & Schedules Extraction
else if (seg.tag === 'LIN') {
if (currentArticle) articles.push(currentArticle);
currentArticle = {
itemNumber: getVal(seg, 2, 0),
buyerItem: getVal(seg, 2, 0),
desc: '',
orderNo: '',
eingangsfortschrittszahl: 0,
nullStellung: '',
deliveries: [],
schedules: []
};
} else if (seg.tag === 'PIA' && currentArticle) {
// Secondary IDs if available
} else if (seg.tag === 'IMD' && currentArticle) {
if (getVal(seg, 2, 3)) currentArticle.desc = getVal(seg, 2, 3);
} else if (seg.tag === 'RFF' && currentArticle && currentArticle.schedules.length === 0) {
const qual = getVal(seg, 0, 0);
if (['ON', 'CR'].includes(qual)) currentArticle.orderNo = getVal(seg, 0, 1);
} else if (seg.tag === 'QTY' && currentArticle) {
const qual = getVal(seg, 0, 0);
// Schedule quantities (113, 1) or cumulative (194)
if (['113', '1'].includes(qual)) {
// Find DTMs and QTY+3 (Target EFZ)
let deliveryDate = '';
let pickupDate = '';
let targetEFZ = null;
for (let j = i + 1; j < segments.length && j < i + 10; j++) {
const nextSeg = segments[j];
if (nextSeg.tag === 'DTM') {
const dtmQual = getVal(nextSeg, 0, 0);
const dtmVal = getVal(nextSeg, 0, 1);
if (['2', '160', '132'].includes(dtmQual)) deliveryDate = dtmVal;
else if (dtmQual === '10') pickupDate = dtmVal;
} else if (nextSeg.tag === 'QTY' && getVal(nextSeg, 0, 0) === '3') {
targetEFZ = parseFloat(getVal(nextSeg, 0, 1)) || 0;
} else if (['LIN', 'QTY', 'SCC'].includes(nextSeg.tag)) {
// Stop if we hit a new loop element
if (nextSeg.tag === 'QTY' && ['113', '1'].includes(getVal(nextSeg, 0, 0))) break;
if (nextSeg.tag === 'LIN' || nextSeg.tag === 'SCC') break;
}
}
const formatEdiDate = (d) => {
if (!d || d.length < 8) return d;
return d.substring(6, 8) + '.' + d.substring(4, 6) + '.' + d.substring(0, 4);
};
const primaryDate = pickupDate || deliveryDate;
let monthKey = '';
if (primaryDate.length >= 8) {
monthKey = primaryDate.substring(4, 6) + '.' + primaryDate.substring(0, 4);
}
currentArticle.schedules.push({
date: formatEdiDate(primaryDate),
deliveryDate: formatEdiDate(deliveryDate),
pickupDate: formatEdiDate(pickupDate),
monthKey: monthKey,
qty: parseFloat(getVal(seg, 0, 1)) || 0,
unit: getVal(seg, 0, 2),
targetEFZ: targetEFZ
});
}
// Deliveries / Lieferscheine (12 = Despatch Qty, 48 = Received Qty, 194 = Used by IFM)
else if (['12', '48', '194'].includes(qual)) {
let dtmStr = '';
for (let j = i + 1; j < segments.length && j < i + 4; j++) {
if (segments[j].tag === 'DTM') {
dtmStr = getVal(segments[j], 0, 1);
break;
}
}
let refStr = '';
// Look around for RFF
for (let j = i - 3; j < i + 4; j++) {
if (j >= 0 && j < segments.length && segments[j].tag === 'RFF') {
const rffQual = getVal(segments[j], 0, 0);
if (['DQ', 'AAK', 'SI', 'AAU'].includes(rffQual)) {
refStr = getVal(segments[j], 0, 1);
break;
}
}
}
let fmtDate = dtmStr;
if (dtmStr.length >= 8) {
fmtDate = dtmStr.substring(6, 8) + '.' + dtmStr.substring(4, 6) + '.' + dtmStr.substring(0, 4);
}
currentArticle.deliveries.push({
docNo: refStr || '-',
date: fmtDate,
qty: parseFloat(getVal(seg, 0, 1)) || 0,
unit: getVal(seg, 0, 2)
});
}
// Cumulative received (70 or 71 -> Eingangsfortschrittszahl)
else if (['70', '71'].includes(qual)) {
currentArticle.eingangsfortschrittszahl = parseFloat(getVal(seg, 0, 1)) || 0;
// Look for DTM+51 for Null-Stellung
for (let j = i + 1; j < segments.length && j < i + 4; j++) {
if (segments[j].tag === 'DTM') {
const dtmQual = getVal(segments[j], 0, 0);
if (dtmQual === '51') {
const dStr = getVal(segments[j], 0, 1);
if (dStr.length >= 8) {
currentArticle.nullStellung = dStr.substring(6, 8) + '.' + dStr.substring(4, 6) + '.' + dStr.substring(0, 4);
}
break;
}
}
}
}
}
}
if (currentArticle) articles.push(currentArticle);
return { header, articles };
}
renderDesadv(content) {
const parser = window.EDIBridge.DelforParser;
if (!parser) throw new Error('Parser nicht geladen.');
const parsed = parser.parse(content);
const data = this.extractDesadvData(parsed);
this.renderDesadvTemplate(data);
}
extractDesadvData(parsed) {
const segments = parsed.segments;
let header = {
docNo: '', date: '',
deliveryDate: '',
buyer: { id: '', name: '', street: '', city: '' },
seller: { id: '', name: '', street: '', city: '' },
shipTo: { id: '', name: '', street: '', city: '', abladestelle: '' }
};
let articles = [];
let currentArticle = null;
const getVal = (seg, elIdx, compIdx) => {
if (!seg || !seg.elements) return '';
const el = seg.elements[elIdx];
if (!el) return '';
if (Array.isArray(el)) return el[compIdx] || '';
if (compIdx === 0) return el;
return '';
};
const formatDate = (dateStr) => {
if (!dateStr || dateStr.length < 8) return dateStr;
return dateStr.substring(6, 8) + '.' + dateStr.substring(4, 6) + '.' + dateStr.substring(0, 4);
};
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
if (seg.tag === 'BGM') {
header.docNo = getVal(seg, 1, 0);
} else if (seg.tag === 'DTM') {
const qual = getVal(seg, 0, 0);
const dateStr = getVal(seg, 0, 1);
if (qual === '137') {
header.date = formatDate(dateStr);
} else if (qual === '132' || qual === '11') {
header.deliveryDate = formatDate(dateStr);
}
} else if (seg.tag === 'NAD') {
const qual = getVal(seg, 0, 0);
const nadObj = {
id: getVal(seg, 1, 0),
name: getVal(seg, 3, 0),
street: getVal(seg, 4, 0),
city: getVal(seg, 5, 0) + ' ' + getVal(seg, 7, 0),
abladestelle: ''
};
if (qual === 'BY') header.buyer = nadObj;
else if (qual === 'SE') header.seller = nadObj;
else if (qual === 'DP' || qual === 'ST' || qual === 'CN') header.shipTo = nadObj;
} else if (seg.tag === 'LOC' && getVal(seg, 0, 0) === '11') {
header.shipTo.abladestelle = getVal(seg, 1, 0);
} else if (seg.tag === 'LIN') {
if (currentArticle) articles.push(currentArticle);
currentArticle = {
itemNumber: getVal(seg, 2, 0),
supplierMat: '',
revisionLevel: '',
desc: '', orderNo: '', qty: 0, unit: 'PCE', batchNo: '', packages: []
};
} else if (seg.tag === 'PIA' && currentArticle) {
const qual = getVal(seg, 0, 0);
if (qual === '1' || qual === '5') {
const subQual = getVal(seg, 1, 1);
if (subQual === 'SA') currentArticle.supplierMat = getVal(seg, 1, 0);
else if (subQual === 'EC') currentArticle.revisionLevel = getVal(seg, 1, 0);
else if (subQual === 'NB' || subQual === 'BT') currentArticle.batchNo = getVal(seg, 1, 0);
}
} else if (seg.tag === 'GIR' && currentArticle) {
if (getVal(seg, 0, 0) === 'V1' || getVal(seg, 1, 1) === 'BT' || getVal(seg, 1, 1) === 'NB') {
currentArticle.batchNo = getVal(seg, 1, 0);
}
} else if (seg.tag === 'IMD' && currentArticle) {
if (getVal(seg, 2, 3)) currentArticle.desc = getVal(seg, 2, 3);
else if (getVal(seg, 2, 0)) currentArticle.desc = getVal(seg, 2, 0);
} else if (seg.tag === 'RFF' && currentArticle) {
const qual = getVal(seg, 0, 0);
if (['ON', 'CR'].includes(qual)) currentArticle.orderNo = getVal(seg, 0, 1);
} else if (seg.tag === 'QTY' && currentArticle) {
const qual = getVal(seg, 0, 0);
if (qual === '12' || qual === '1') {
currentArticle.qty = parseFloat(getVal(seg, 0, 1)) || 0;
const unit = getVal(seg, 0, 2);
if (unit) currentArticle.unit = unit;
}
} else if (seg.tag === 'PAC' && currentArticle) {
currentArticle.packages.push({
qty: parseInt(getVal(seg, 0, 0)) || 1,
type: getVal(seg, 2, 0),
custType: getVal(seg, 2, 1),
name: getVal(seg, 2, 3) || getVal(seg, 2, 0)
});
}
}
if (currentArticle) articles.push(currentArticle);
return { header, articles };
}
renderDesadvTemplate(data) {
let html = `
<div class="print-document desadv-document">
<div class="print-header no-print">
<div class="header-left">
<div class="doc-type-badge">DESADV</div>
<h1>Digitaler Lieferschein</h1>
</div>
<div class="header-right">
<button class="btn-print" onclick="app.viewer.printDocument()">
<i data-lucide="printer"></i> Drucken
</button>
</div>
</div>
<div class="print-address-grid">
<div class="print-address-box">
<div class="box-title"><i data-lucide="building-2"></i> KÄUFER: ${data.header.buyer.id}</div>
<div class="address-content">
<strong>${data.header.buyer.name}</strong><br>
${data.header.buyer.street}<br>
${data.header.buyer.city}
</div>
</div>
<div class="print-address-box">
<div class="box-title"><i data-lucide="truck"></i> VERKÄUFER: ${data.header.seller.id}</div>
<div class="address-content">
<strong>${data.header.seller.name}</strong><br>
${data.header.seller.street}<br>
${data.header.seller.city}
</div>
</div>
<div class="print-address-box info-box">
<div class="box-title"><i data-lucide="map-pin"></i> WARENEMPFÄNGER: ${data.header.shipTo.id}</div>
<div class="address-content">
<strong>${data.header.shipTo.name}</strong><br>
${data.header.shipTo.street}<br>
${data.header.shipTo.city}<br>
<div class="sub-info">Abladestelle: <strong>${data.header.shipTo.abladestelle || '-'}</strong></div>
</div>
</div>
</div>
<div class="print-meta-bar">
<div class="meta-item">
<span class="label">Lieferschein-Nr:</span>
<strong class="highlight-green">${data.header.docNo}</strong>
</div>
<div class="meta-item">
<span class="label">Belegdatum:</span>
<strong>${data.header.date}</strong>
</div>
${data.header.deliveryDate ? `
<div class="meta-item">
<span class="label">Liefertermin:</span>
<strong>${data.header.deliveryDate}</strong>
</div>
` : ''}
</div>
<div class="print-articles">
`;
data.articles.forEach((art, index) => {
html += `
<div class="print-article-row">
<div class="article-header-main">
<div class="col-pos">#${index + 1}</div>
<div class="col-mat">
<div class="mat-pair">
<span class="mat-label">Kunde:</span>
<span class="mat-val"><strong>${art.itemNumber}</strong></span>
</div>
${art.supplierMat ? `
<div class="mat-pair">
<span class="mat-label">Lieferant:</span>
<span class="mat-val">${art.supplierMat}</span>
</div>
` : ''}
</div>
<div class="col-desc">
<div class="desc-text">${art.desc || 'Keine Beschreibung'}</div>
<div class="tags-row">
${art.revisionLevel ? `<span class="badge badge-gray">Rev: ${art.revisionLevel}</span>` : ''}
${art.batchNo ? `<span class="badge badge-indigo">Charge: ${art.batchNo}</span>` : ''}
</div>
</div>
<div class="col-qty text-right">
<div class="qty-total">${this.formatNumber(art.qty)}</div>
<div class="qty-unit">${art.unit}</div>
</div>
</div>
<div class="article-sub-details">
<div class="sub-item">
<span class="label">Bestellung:</span>
<strong>${art.orderNo || '-'}</strong>
</div>
${art.packages.length > 0 ? `
<div class="packaging-details">
<div class="package-list">
${art.packages.map(p => `
<div class="package-item">
<i data-lucide="package" style="width:14px; height:14px; margin-right:4px; opacity:0.6;"></i>
<strong>${p.qty}x</strong> ${p.name || p.type}
<span class="type-code">(${p.type}${p.custType ? ` / ${p.custType}` : ''})</span>
</div>
`).join('')}
</div>
</div>
` : ''}
</div>
</div>`;
});
html += `
</div>
<div class="print-footer">
<div class="footer-left">VDA/EDIFACT DESADV GMI022</div>
<div class="footer-right">Generiert am: ${new Date().toLocaleDateString('de-DE')}</div>
</div>
</div>
`;
this.showViewer(html);
}
async renderInvrpt(content) {
let parsed;
if (window.electronAPI && window.electronAPI.parseEdifactLib) {
try {
const libResult = await window.electronAPI.parseEdifactLib(content);
if (libResult.success && libResult.data) {
parsed = { segments: libResult.data };
}
} catch (e) {
console.warn('ts-edifact parsing fall-back to basic parser in viewer:', e);
}
}
if (!parsed) {
const parser = window.EDIBridge.DelforParser;
if (!parser) throw new Error('Parser nicht geladen.');
parsed = parser.parse(content);
}
const data = this.extractInvrptData(parsed);
this.renderInvrptTemplate(data);
}
extractInvrptData(parsed) {
const segments = parsed.segments;
let header = {
docNo: '', date: '',
buyer: { id: '', name: '', street: '', city: '' },
seller: { id: '', name: '', street: '', city: '' }
};
let items = [];
let currentItem = null;
const getVal = (seg, elIdx, compIdx) => {
if (!seg || !seg.elements) return '';
const el = seg.elements[elIdx];
if (!el) return '';
if (Array.isArray(el)) return el[compIdx] || '';
if (compIdx === 0) return el;
return '';
};
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
if (seg.tag === 'BGM') {
header.docNo = getVal(seg, 1, 0);
} else if (seg.tag === 'DTM') {
const qual = getVal(seg, 0, 0);
if (qual === '137') {
const dateStr = getVal(seg, 0, 1);
if (dateStr.length >= 8) {
header.date = dateStr.substring(6, 8) + '.' + dateStr.substring(4, 6) + '.' + dateStr.substring(0, 4);
}
} else if (qual === '179' && currentItem && currentItem.inventories.length > 0) {
const currentInv = currentItem.inventories[currentItem.inventories.length - 1];
const dateStr = getVal(seg, 0, 1);
if (dateStr.length >= 8) {
currentInv.date = dateStr.substring(6, 8) + '.' + dateStr.substring(4, 6) + '.' + dateStr.substring(0, 4);
}
}
} else if (seg.tag === 'NAD') {
const qual = getVal(seg, 0, 0);
const nadObj = {
id: getVal(seg, 1, 0),
name: getVal(seg, 3, 0),
street: getVal(seg, 4, 0),
city: getVal(seg, 5, 0) + ' ' + getVal(seg, 7, 0)
};
if (qual === 'BY' || qual === 'OY' || qual === 'GM') header.buyer = nadObj;
else if (qual === 'SE' || qual === 'SU' || qual === 'WH') header.seller = nadObj;
} else if (seg.tag === 'LIN') {
if (currentItem) items.push(currentItem);
currentItem = {
itemNumber: getVal(seg, 2, 0),
supplierMat: '',
revisionLevel: '',
desc: '', batchNo: '',
inventories: []
};
} else if (seg.tag === 'PIA' && currentItem) {
const qual = getVal(seg, 0, 0);
if (qual === '1' || qual === '5') {
const subQual = getVal(seg, 1, 1);
if (subQual === 'SA') currentItem.supplierMat = getVal(seg, 1, 0);
else if (subQual === 'EC') currentItem.revisionLevel = getVal(seg, 1, 0);
else if (subQual === 'NB' || subQual === 'BT') currentItem.batchNo = getVal(seg, 1, 0);
}
} else if (seg.tag === 'IMD' && currentItem) {
if (getVal(seg, 2, 3)) currentItem.desc = getVal(seg, 2, 3);
} else if (seg.tag === 'INV' && currentItem) {
currentItem.inventories.push({
direction: getVal(seg, 0, 0),
reason: getVal(seg, 2, 0),
movementQty: null,
balanceQty: null,
unit: 'PCE',
location: '',
date: '',
qualifier: ''
});
} else if (seg.tag === 'QTY' && currentItem && currentItem.inventories.length > 0) {
const currentInv = currentItem.inventories[currentItem.inventories.length - 1];
const qual = getVal(seg, 0, 0);
const qtyVal = parseFloat(getVal(seg, 0, 1)) || 0;
const unit = getVal(seg, 0, 2);
if (unit) currentInv.unit = unit;
if (qual === '156') {
currentInv.movementQty = qtyVal;
currentInv.qualifier = '156';
} else if (qual === '145') {
currentInv.balanceQty = qtyVal;
if (!currentInv.qualifier) currentInv.qualifier = '145';
} else if (qual === '1') {
currentInv.balanceQty = qtyVal;
if (!currentInv.qualifier) currentInv.qualifier = '1';
}
} else if (seg.tag === 'LOC' && currentItem && currentItem.inventories.length > 0) {
const currentInv = currentItem.inventories[currentItem.inventories.length - 1];
if (getVal(seg, 0, 0) === '18' || getVal(seg, 0, 0) === '11') {
currentInv.location = getVal(seg, 1, 0);
}
}
}
if (currentItem) items.push(currentItem);
return { header, items };
}
renderInvrptTemplate(data) {
let html = `
<div class="print-document invrpt-document">
<div class="print-header no-print">
<div class="header-left">
<div class="doc-type-badge" style="background: #10b981;">INVRPT</div>
<h1>Lagerbestandsbericht</h1>
</div>
<div class="header-right">
<button class="btn-print" onclick="app.viewer.printDocument()">
<i data-lucide="printer"></i> Drucken
</button>
</div>
</div>
<div class="print-address-grid">
<div class="print-address-box">
<div class="box-title"><i data-lucide="building-2"></i> KÄUFER: ${data.header.buyer.id}</div>
<div class="address-content">
<strong>${data.header.buyer.name}</strong><br>
${data.header.buyer.street}<br>
${data.header.buyer.city}
</div>
</div>
<div class="print-address-box">
<div class="box-title"><i data-lucide="truck"></i> VERKÄUFER: ${data.header.seller.id}</div>
<div class="address-content">
<strong>${data.header.seller.name}</strong><br>
${data.header.seller.street}<br>
${data.header.seller.city}
</div>
</div>
</div>
<div class="print-meta-bar">
<div class="meta-item">
<span class="label">Bericht-Nr:</span>
<strong class="highlight-green">${data.header.docNo}</strong>
</div>
<div class="meta-item">
<span class="label">Belegdatum:</span>
<strong>${data.header.date}</strong>
</div>
</div>
<div class="print-articles">
`;
const getReasonText = (dir, reason) => {
if (reason === '1') return 'Wareneingang';
if (reason === '2') return 'Lieferung';
if (reason === '3') return 'Ausschuss / Verschrottung';
if (reason === '4') return 'Bestandsdifferenz';
if (reason === '5') return 'Umlagerung';
if (reason === '6') return 'Recycling';
if (reason === '7') return 'Storno vorheriger Bewegung';
if (reason === '11') return 'Verbrauch aus dem Lager';
if (dir === '1') return 'Bewegung aus dem Lager (Ausgang)';
if (dir === '2') return 'Bewegung ins Lager (Eingang)';
return 'Bestandsmeldung';
};
data.items.forEach((item, index) => {
html += `
<div class="print-article-row">
<div class="article-header-main">
<div class="col-pos">#${index + 1}</div>
<div class="col-mat">
<div class="mat-pair">
<span class="mat-label">Kunde:</span>
<span class="mat-val"><strong>${item.itemNumber}</strong></span>
</div>
${item.supplierMat ? `
<div class="mat-pair">
<span class="mat-label">Lieferant:</span>
<span class="mat-val">${item.supplierMat}</span>
</div>
` : ''}
</div>
<div class="col-desc" style="flex: 2;">
<div class="desc-text">
${item.desc || 'Keine Beschreibung'}
</div>
<div class="tags-row">
${item.revisionLevel ? `<span class="badge badge-gray">Rev: ${item.revisionLevel}</span>` : ''}
${item.batchNo ? `<span class="badge badge-indigo">Charge: ${item.batchNo}</span>` : ''}
</div>
</div>
</div>
<div class="article-sub-details" style="display: block;">
<strong style="margin-bottom: 8px; display: block; font-size: 0.85rem; color: #4b5563;">Bestandsdetails</strong>
<table class="invrpt-table" style="width: 100%; border-collapse: collapse; font-size: 0.85rem; text-align: left; margin-top: 5px;">
<thead>
<tr style="border-bottom: 2px solid #e5e7eb;">
<th style="padding: 8px; color: #6b7280; font-weight: 600;">Datum</th>
<th style="padding: 8px; color: #6b7280; font-weight: 600;">Bewegungsgrund</th>
<th style="padding: 8px; color: #6b7280; font-weight: 600; text-align: right;">Bewegungsmenge</th>
<th style="padding: 8px; color: #6b7280; font-weight: 600; text-align: right;">Aktueller Bestand</th>
<th style="padding: 8px; color: #6b7280; font-weight: 600;">Status</th>
</tr>
</thead>
<tbody>
${item.inventories && item.inventories.length > 0 ? item.inventories.map((inv, idx) => {
const reasonText = getReasonText(inv.direction, inv.reason);
const movQtyStr = inv.movementQty !== null ? `<strong style="color: #3b82f6;">${this.formatNumber(inv.movementQty)} ${inv.unit}</strong>` : '-';
const balQtyStr = inv.balanceQty !== null ? `<strong style="color: #10b981;">${this.formatNumber(inv.balanceQty)} ${inv.unit}</strong>` : '-';
let statusText = '';
if (inv.movementQty !== null && inv.balanceQty !== null) {
statusText = `Nach der Bewegung verbleiben ${this.formatNumber(inv.balanceQty)} ${inv.unit} im Bestand.`;
} else if (inv.movementQty === null && inv.balanceQty !== null) {
statusText = `Dies ist eine Meldung des aktuellen Lagerbestands ohne eine Warenbewegung.`;
} else if (inv.movementQty !== null) {
statusText = `Bewegung erfasst (kein resultierender Bestand gemeldet).`;
}
return `
<tr style="border-bottom: 1px solid #e5e7eb; background: ${idx % 2 === 0 ? '#fafafa' : '#ffffff'};">
<td style="padding: 8px;">${inv.date || '-'}</td>
<td style="padding: 8px;">${reasonText} ${inv.location ? `<span class="badge badge-gray" style="font-size: 0.7rem; margin-left: 5px;">${inv.location}</span>` : ''}</td>
<td style="padding: 8px; text-align: right;">${movQtyStr}</td>
<td style="padding: 8px; text-align: right;">${balQtyStr}</td>
<td style="padding: 8px; color: #4b5563; font-style: italic;">${statusText}</td>
</tr>
`;
}).join('') : '<tr><td colspan="5" style="padding: 8px; text-align: center; color: #9ca3af;">Keine Bestandsdaten gefunden</td></tr>'}
</tbody>
</table>
</div>
</div>`;
});
html += `
</div>
<div class="print-footer">
<div class="footer-left">EDIFACT INVRPT D.13A</div>
<div class="footer-right">Generiert am: ${new Date().toLocaleDateString('de-DE')}</div>
</div>
</div>
`;
this.showViewer(html);
}
renderVDA(content) {
const parser = window.EDIBridge.VDAParser;
if (!parser) throw new Error('VDAParser nicht geladen.');
const parsed = parser.parse(content);
// Ensure there is at least one interchange
if (!parsed.interchanges || parsed.interchanges.length === 0) {
throw new Error('Kein gültiger VDA-Inhalt gefunden.');
}
const interchange = parsed.interchanges[0];
// Check if it's VDA 4905 (511 header)
if (interchange.header && content.startsWith('511')) {
const data = this.extractVda4905Data(interchange);
this.renderTemplate(data);
}
// Check if it's VDA 4913 (711 header)
else if (interchange.header && content.startsWith('711')) {
const data = this.extractVda4913Data(interchange);
this.renderVda4913Template(data);
}
else {
alert('Aktuell werden nur VDA 4905 und VDA 4913 Formate im Viewer unterstützt.');
}
}
extractVda4905Data(interchange) {
let header = {
docNo: interchange.header.newTransNo,
oldDocNo: interchange.header.oldTransNo,
date: this.formatVdaDate(interchange.header.date),
buyer: { id: interchange.header.customerId, name: '', street: '', city: '' },
seller: { id: interchange.header.supplierId, name: '', street: '', city: '' },
shipTo: { id: '', name: '', street: '', city: '', abladestelle: '', plant: '' }
};
let articles = [];
if (interchange.articles) {
interchange.articles.forEach(art => {
header.shipTo.abladestelle = art.article.unloadingPoint;
header.shipTo.plant = art.article.plant;
let articleData = {
itemNumber: art.article.suppMat,
buyerItem: art.article.custMat,
desc: '', // Not typically in VDA 4905
orderNo: art.article.orderNo,
eingangsfortschrittszahl: art.efz || 0,
nullStellung: art.nullStellung ? this.formatVdaDate(art.nullStellung) : '',
deliveries: art.deliveries || [],
schedules: []
};
// Format deliveries
articleData.deliveries = articleData.deliveries.map(d => ({
docNo: d.docNo,
date: this.formatVdaDate(d.date),
qty: d.qty,
unit: d.unit
}));
// Process schedules
if (art.schedules) {
art.schedules.forEach(sched => {
const fmtDate = this.formatVdaDate(sched.date);
let monthKey = '';
if (sched.date && sched.date.length === 6) {
monthKey = sched.date.substring(2, 4) + '.' + '20' + sched.date.substring(0, 2);
}
articleData.schedules.push({
date: fmtDate,
monthKey: monthKey,
qty: sched.qty,
unit: art.article.unit || 'ST'
});
});
}
articles.push(articleData);
});
}
return { header, articles };
}
extractVda4913Data(interchange) {
let header = {
docNo: interchange.header.transNo2,
oldDocNo: interchange.header.transNo1,
date: this.formatVdaDate(interchange.header.date),
buyer: { id: interchange.header.targetId, name: '', street: '', city: '' },
seller: { id: interchange.header.sourceId, name: '', street: '', city: '' }
};
let transports = [];
if (interchange.transports) {
interchange.transports.forEach(t => {
let transport = {
carrier: t.data.carrierName,
date: this.formatVdaDate(t.data.date),
grossWeight: t.data.grossWeight,
netWeight: t.data.netWeight,
deliveryNotes: []
};
t.deliveryNotes.forEach(dn => {
let dnote = {
dnNo: dn.header.dnNo,
date: this.formatVdaDate(dn.header.date),
unloadingPoint: dn.header.unloadingPoint,
orderNo: dn.header.orderNo,
positions: [],
texts: dn.texts || []
};
dn.positions.forEach(pos => {
let position = {
suppMat: pos.data.suppMat,
custMat: pos.data.custMat,
qty: pos.data.qty,
unit: pos.data.unit,
refNo: pos.data.refNo,
batchNo: pos.data.batchNo || '',
revisionLevel: pos.data.revisionLevel || '',
packaging: pos.packaging || []
};
dnote.positions.push(position);
});
transport.deliveryNotes.push(dnote);
});
transports.push(transport);
});
}
return { header, transports };
}
getWeekDateRange(year, week) {
const simple = new Date(year, 0, 1 + (week - 1) * 7);
const dow = simple.getDay();
const ISOweekStart = simple;
if (dow <= 4)
ISOweekStart.setDate(simple.getDate() - simple.getDay() + 1);
else
ISOweekStart.setDate(simple.getDate() + 8 - simple.getDay());
const ISOweekEnd = new Date(ISOweekStart);
ISOweekEnd.setDate(ISOweekStart.getDate() + 6);
const fmt = (d) => String(d.getDate()).padStart(2, '0') + '.' + String(d.getMonth() + 1).padStart(2, '0') + '.' + String(d.getFullYear()).substring(2);
return `W ${fmt(ISOweekStart)} - ${fmt(ISOweekEnd)} (KW: ${String(week).padStart(2, '0')
})`;
}
getMonthDateRange(year, month) {
const start = new Date(year, month - 1, 1);
const end = new Date(year, month, 0);
const fmt = (d) => String(d.getDate()).padStart(2, '0') + '.' + String(d.getMonth() + 1).padStart(2, '0') + '.' + String(d.getFullYear()).substring(2);
return `M ${fmt(start)} - ${fmt(end)} (MO: ${String(month).padStart(2, '0')})`;
}
formatVdaDate(rawDate) {
if (!rawDate) return '';
let val = typeof rawDate === 'string' ? rawDate : '';
if (val === '000000' || val.trim() === '') return '';
// Handle week/month (length 6)
if (val.length === 6) {
// Check for format YY00WW or YY00MM
if (val.substring(2, 4) === '00') {
const yyStr = val.substring(0, 2);
const partStr = val.substring(4, 6);
const year = 2000 + parseInt(yyStr, 10);
const part = parseInt(partStr, 10);
if (part > 12) {
return this.getWeekDateRange(year, part);
} else {
return this.getMonthDateRange(year, part);
}
} else if (val.endsWith(' ') || val.endsWith('00')) {
// If it's YYMM00 or YYMM
let trimVal = val.trim();
if (trimVal.endsWith('00')) trimVal = trimVal.substring(0, 4);
if (trimVal.length === 4) {
let yyStr = trimVal.substring(0, 2);
let partStr = trimVal.substring(2, 4);
let year = 2000 + parseInt(yyStr, 10);
let part = parseInt(partStr, 10);
if (part > 12) {
return this.getWeekDateRange(year, part);
} else {
return this.getMonthDateRange(year, part);
}
}
}
// Standard YYMMDD format
return val.substring(4, 6) + '.' + val.substring(2, 4) + '.' + '20' + val.substring(0, 2);
}
return val;
}
formatNumber(val) {
if (val === undefined || val === null || isNaN(val)) return '0';
// Round to nearest integer if the user says there are no decimals (pieces)
// or round to 3 decimals to avoid floating point errors.
const rounded = Math.round(val * 1000) / 1000;
return rounded.toLocaleString('de-DE', {
minimumFractionDigits: 0,
maximumFractionDigits: 3
});
}
renderTemplate(data) {
let html = `
<div class="print-document delfor-document">
<div class="print-header no-print">
<div class="header-left">
<div class="doc-type-badge" style="background: #2563eb;">DELFOR</div>
<h1>Lieferplan / Abruf</h1>
</div>
<div class="header-right">
<button class="btn-print" onclick="app.viewer.printDocument()">
<i data-lucide="printer"></i> Drucken
</button>
</div>
</div>
<div class="print-address-grid">
<div class="print-address-box">
<div class="box-title"><i data-lucide="building-2"></i> KÄUFER: ${data.header.buyer.id}</div>
<div class="address-content">
<strong>${data.header.buyer.name}</strong><br>
${data.header.buyer.street}<br>
${data.header.buyer.city}
</div>
</div>
<div class="print-address-box">
<div class="box-title"><i data-lucide="truck"></i> VERKÄUFER: ${data.header.seller.id}</div>
<div class="address-content">
<strong>${data.header.seller.name}</strong><br>
${data.header.seller.street}<br>
${data.header.seller.city}
</div>
</div>
<div class="print-address-box info-box">
<div class="box-title"><i data-lucide="map-pin"></i> WARENEMPFÄNGER: ${data.header.shipTo.id}</div>
<div class="address-content">
<strong>${data.header.shipTo.name}</strong><br>
${data.header.shipTo.street}<br>
${data.header.shipTo.city}<br>
<div class="sub-info">Abladestelle: <strong>${data.header.shipTo.abladestelle || '-'}</strong></div>
${data.header.shipTo.plant ? `<div class="sub-info">Werk: <strong>${data.header.shipTo.plant}</strong></div>` : ''}
</div>
</div>
</div>
<div class="print-meta-bar">
<div class="meta-item">
<span class="label">Abrufnummer:</span>
<strong class="highlight-green">${data.header.docNo}</strong>
</div>
<div class="meta-item">
<span class="label">Belegdatum:</span>
<strong>${data.header.date}</strong>
</div>
${data.header.oldDocNo ? `
<div class="meta-item">
<span class="label">Vorheriger Abruf:</span>
<strong>${data.header.oldDocNo}</strong>
</div>
` : ''}
</div>
<div class="print-articles">
`;
data.articles.forEach((art, index) => {
html += `
<div class="print-article-row">
<div class="article-header-main">
<div class="col-pos">#${index + 1}</div>
<div class="col-mat">
<div class="mat-pair">
<span class="mat-label">Kunde:</span>
<span class="mat-val"><strong>${art.buyerItem}</strong></span>
</div>
<div class="mat-pair" style="font-size: 0.8rem; opacity: 0.7;">
<span class="mat-label">Pos:</span>
<span class="mat-val">${art.orderPos || '-'}</span>
</div>
</div>
<div class="col-desc">
<div class="desc-text">${art.desc || 'Keine Beschreibung'}</div>
<div class="tags-row">
<span class="badge badge-indigo">FZ: ${this.formatNumber(art.eingangsfortschrittszahl)}</span>
${art.nullStellung ? `<span class="badge badge-gray">Nullstellung: ${art.nullStellung}</span>` : ''}
</div>
</div>
<div class="col-qty text-right">
<div class="qty-total" style="font-size: 0.9rem;">Bestellung:</div>
<div class="qty-unit">${art.orderNo || '-'}</div>
</div>
</div>
<div class="article-sub-details" style="display: block;">
${art.deliveries.length > 0 ? `
<div style="margin-bottom: 15px;">
<h4 style="font-size: 0.75rem; text-transform: uppercase; color: #64748b; margin-bottom: 5px; display: flex; align-items: center; gap: 5px;">
<i data-lucide="history" style="width:12px;"></i> Letzte Lieferungen
</h4>
<table class="delivery-table">
<thead>
<tr><th>Lieferschein</th><th>WE-Datum</th><th class="text-right">Menge</th></tr>
</thead>
<tbody>
${art.deliveries.map(d => `
<tr>
<td>${d.docNo}</td>
<td>${d.date}</td>
<td class="text-right"><strong>${this.formatNumber(d.qty)}</strong> ${d.unit}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
` : ''}
<h4 style="font-size: 0.75rem; text-transform: uppercase; color: #64748b; margin-bottom: 5px; display: flex; align-items: center; gap: 5px;">
<i data-lucide="calendar" style="width:12px;"></i> Einteilungen
</h4>
<table class="schedule-table">
<thead>
<tr>
<th>Typ</th>
<th>Termin (Abholung / Lieferung)</th>
<th class="text-right">Menge</th>
<th class="text-right">Kum.Menge</th>
<th class="text-right">Soll-FZ</th>
</tr>
</thead>
<tbody>
`;
let currentMonth = null;
let monthSum = 0;
let kumSum = 0;
art.schedules.forEach((sched, sIndex) => {
if (currentMonth !== null && sched.monthKey !== currentMonth) {
html += `
<tr class="month-sum-row">
<td colspan="2" class="text-right"><strong>Monatssumme:</strong></td>
<td class="text-right"><strong>${this.formatNumber(monthSum)}</strong></td>
<td></td>
</tr>
`;
monthSum = 0;
}
currentMonth = sched.monthKey;
monthSum = Math.round((monthSum + sched.qty) * 1000) / 1000;
kumSum = Math.round((kumSum + sched.qty) * 1000) / 1000;
html += `
<tr>
<td>${sched.type || 'Abruf'}</td>
<td>
${sched.pickupDate && sched.deliveryDate && sched.pickupDate !== sched.deliveryDate ? `
<div style="display:flex; flex-direction:column; gap:2px;">
<span style="font-size: 0.85rem;">Abholung: <strong>${sched.pickupDate}</strong></span>
<span style="font-size: 0.75rem; color: #64748b;">Lieferung: ${sched.deliveryDate}</span>
</div>
` : `
<strong>${sched.date}</strong>
`}
</td>
<td class="text-right">${this.formatNumber(sched.qty)} ${sched.unit}</td>
<td class="text-right" style="color: #64748b;">${this.formatNumber(kumSum)}</td>
<td class="text-right">
${sched.targetEFZ !== null ? `<strong>${this.formatNumber(sched.targetEFZ)}</strong>` : '-'}
</td>
</tr>
`;
if (sIndex === art.schedules.length - 1) {
html += `
<tr class="month-sum-row">
<td colspan="2" class="text-right"><strong>Monatssumme:</strong></td>
<td class="text-right"><strong>${this.formatNumber(monthSum)}</strong></td>
<td></td>
</tr>
`;
}
});
html += `
</tbody>
</table>
</div>
</div>`;
});
html += `
</div>
<div class="print-footer">
<div class="footer-left">EDIFACT DELFOR / VDA 4905</div>
<div class="footer-right">Generiert am: ${new Date().toLocaleDateString('de-DE')}</div>
</div>
</div>
`;
this.showViewer(html);
}
reset() {
this.renderArea.innerHTML = '';
this.renderArea.style.display = 'none';
this.dropZone.style.display = 'flex';
if (this.btnPrint) this.btnPrint.style.display = 'none';
if (this.btnBack) this.btnBack.style.display = 'none';
if (this.fileInput) this.fileInput.value = '';
}
renderVda4913Template(data) {
let html = `
<div class="print-document vda-4913-document">
<div class="print-header no-print">
<div class="header-left">
<div class="doc-type-badge" style="background: #3b82f6;">VDA 4913</div>
<h1>Digitaler Lieferschein (VDA)</h1>
</div>
<div class="header-right">
<button class="btn-print" onclick="app.viewer.printDocument()">
<i data-lucide="printer"></i> Drucken
</button>
</div>
</div>
<div class="print-address-grid">
<div class="print-address-box">
<div class="box-title"><i data-lucide="building-2"></i> KÄUFER: ${data.header.customerId}</div>
<div class="address-content">
<strong>${data.header.buyer.name}</strong><br>
${data.header.buyer.street}<br>
${data.header.buyer.city}
</div>
</div>
<div class="print-address-box">
<div class="box-title"><i data-lucide="truck"></i> VERKÄUFER: ${data.header.supplierId}</div>
<div class="address-content">
<strong>${data.header.seller.name}</strong><br>
${data.header.seller.street}<br>
${data.header.seller.city}
</div>
</div>
<div class="print-address-box info-box">
<div class="box-title"><i data-lucide="info"></i> ÜBERTRAGUNG</div>
<div class="address-content">
<div class="sub-info">Übertragungs-Nr: <strong>${data.header.docNo}</strong></div>
<div class="sub-info">Datum: <strong>${data.header.date}</strong></div>
<div class="sub-info">Ref-Nr: <strong>${data.header.oldDocNo || '-'}</strong></div>
</div>
</div>
</div>
<div class="vda-main-content">
`;
if (!data.transports || data.transports.length === 0) {
html += `<div class="no-data">Keine Transportdaten in der Nachricht gefunden.</div>`;
} else {
data.transports.forEach((transport, tIdx) => {
html += `
<div class="vda-transport-block" style="margin-bottom: 30px; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden;">
<div class="vda-transport-header" style="background: #f8fafc; padding: 10px 15px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 10px;">
<i data-lucide="truck" style="color: #64748b;"></i>
<strong style="text-transform: uppercase; font-size: 0.8rem; color: #64748b;">Transport ${tIdx + 1}:</strong>
<span style="font-weight: 700;">${transport.carrier || 'Unbekannter Frachtführer'}</span>
</div>
<div style="display: flex; gap: 20px; font-size: 0.85rem;">
<span>Brutto: <strong>${transport.grossWeight} kg</strong></span>
<span>Netto: <strong>${transport.netWeight} kg</strong></span>
<span>Versand: <strong>${transport.date}</strong></span>
</div>
</div>
<div style="padding: 15px;">
`;
transport.deliveryNotes.forEach((dn, dnIdx) => {
html += `
<div class="vda-dn-card" style="border: 1px solid #cbd5e1; border-radius: 6px; margin-bottom: 20px; background: white;">
<div class="vda-dn-header" style="background: #f1f5f9; padding: 10px 15px; border-bottom: 1px solid #cbd5e1; display: flex; justify-content: space-between;">
<div style="font-weight: 800; color: #1e293b;">LIEFERSCHEIN: ${dn.dnNo}</div>
<div style="display: flex; gap: 15px; font-size: 0.8rem; color: #475569;">
<span>Datum: <strong>${dn.date}</strong></span>
<span>Bestell-Nr: <strong>${dn.orderNo}</strong></span>
<span>Abladestelle: <strong>${dn.unloadingPoint}</strong></span>
</div>
</div>
`;
if (dn.texts && dn.texts.length > 0) {
html += `
<div class="vda-dn-notes" style="padding: 8px 15px; font-size: 0.8rem; background: #fffcf0; border-bottom: 1px solid #cbd5e1; color: #854d0e;">
<i data-lucide="message-square" style="width:12px; height:12px; margin-right:5px; vertical-align: middle;"></i>
<strong>Bemerkungen:</strong> ${dn.texts.join(' | ')}
</div>
`;
}
html += `
<div style="padding: 10px;">
<div class="print-articles">
`;
dn.positions.forEach((pos, pIdx) => {
html += `
<div class="print-article-row" style="border: none; background: transparent; padding-bottom: 15px; border-bottom: 1px solid #f1f5f9; margin-bottom: 15px;">
<div class="article-header-main">
<div class="col-pos">#${pIdx + 1}</div>
<div class="col-mat">
<div class="mat-pair">
<span class="mat-label">Kunde:</span>
<span class="mat-val"><strong>${pos.custMat}</strong></span>
</div>
<div class="mat-pair">
<span class="mat-label">Lieferant:</span>
<span class="mat-val">${pos.suppMat}</span>
</div>
</div>
<div class="col-desc">
<div class="desc-text">Keine Beschreibung</div>
<div class="tags-row">
${pos.revisionLevel ? `<span class="badge badge-gray">Rev: ${pos.revisionLevel}</span>` : ''}
${pos.batchNo ? `<span class="badge badge-indigo">Charge: ${pos.batchNo}</span>` : ''}
<span class="badge badge-gray">Ref: ${pos.refNo}</span>
</div>
</div>
<div class="col-qty text-right">
<div class="qty-total">${this.formatNumber(pos.qty)}</div>
<div class="qty-unit">${pos.unit}</div>
</div>
</div>
${pos.packaging && pos.packaging.length > 0 ? `
<div class="article-sub-details">
<div class="packaging-details">
<div class="package-list">
${pos.packaging.map(pk => `
<div class="package-item">
<i data-lucide="package" style="width:14px; height:14px; margin-right:4px; opacity:0.6;"></i>
<strong>${pk.qty}x</strong> ${pk.packMatCust}
<span class="type-code">(${pk.packMatSupp || '-'})</span>
<span class="type-code" style="margin-left: 10px; opacity: 0.7;">(S: ${pk.labelFrom} - ${pk.labelTo || pk.labelFrom})</span>
</div>
`).join('')}
</div>
</div>
</div>
` : ''}
</div>
`;
});
html += `
</div>
</div>
</div>
`;
});
html += `
</div>
</div>`;
});
}
html += `
</div>
<div class="print-footer">
<div class="footer-left">VDA 4913 Dokumentenansicht</div>
<div class="footer-right">Generiert am: ${new Date().toLocaleDateString('de-DE')}</div>
</div>
</div>
`;
this.showViewer(html);
}
printDocument() {
window.print();
}
}
window.EDIBridge.Viewer = Viewer;