1389 lines
64 KiB
JavaScript
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;
|