Files
RollCalcPython/templates/roll_calculator.html
T

1934 lines
74 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="theme-color" content="#0066cc">
<meta name="description" content="Roll Diameter Calculator - Internal Naue Tool">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Roll Diameter Calculator</title>
<style>
/* ---- NAUE BASE STYLES ---- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: #f4f6f9;
color: #333;
min-height: 100vh;
}
/* ---- DISCLAIMER MODAL ---- */
#disclaimerOverlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.7);
z-index: 9999;
display: flex; align-items: center; justify-content: center;
padding: 20px;
}
#disclaimerBox {
background: #fff;
border-radius: 10px;
max-width: 680px;
width: 100%;
padding: 32px;
box-shadow: 0 8px 40px rgba(0,0,0,0.3);
}
.disclaimer-badge {
display: inline-block;
background: #ff6600;
color: #fff;
font-size: 11px;
font-weight: 700;
padding: 3px 10px;
border-radius: 4px;
letter-spacing: 1px;
margin-bottom: 12px;
}
#disclaimerBox h2 {
font-size: 18px;
color: #003366;
margin-bottom: 20px;
}
.disclaimer-section {
background: #f8f9fa;
border-left: 4px solid #0066cc;
border-radius: 4px;
padding: 14px 16px;
margin-bottom: 14px;
font-size: 13px;
line-height: 1.6;
}
.disclaimer-section h4 {
font-size: 13px;
margin-bottom: 6px;
color: #333;
}
.disclaimer-confirm {
margin: 18px 0 20px;
font-size: 13px;
display: flex;
align-items: flex-start;
gap: 10px;
}
.disclaimer-confirm input[type=checkbox] {
margin-top: 2px;
width: 16px; height: 16px;
flex-shrink: 0;
}
.disclaimer-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
}
#btnCancel {
padding: 10px 22px;
border: 2px solid #ccc;
background: #fff;
color: #666;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
}
#btnAccept {
padding: 10px 22px;
border: none;
background: #0066cc;
color: #fff;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
}
#btnAccept:disabled {
background: #aaa;
cursor: not-allowed;
}
#btnAccept:not(:disabled):hover { background: #0052a3; }
/* ---- APP HEADER ---- */
.app-header {
background: linear-gradient(135deg, #003366 0%, #0066cc 100%);
color: #fff;
padding: 18px 32px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.app-header .logo-text {
font-size: 20px;
font-weight: 700;
letter-spacing: 1px;
}
.app-header .subtitle {
font-size: 13px;
opacity: 0.85;
}
.beta-pill {
background: #ff6600;
color: #fff;
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 20px;
letter-spacing: 1px;
margin-left: 8px;
}
.internal-pill {
background: rgba(255,255,255,0.2);
color: #fff;
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 20px;
letter-spacing: 0.5px;
margin-left: 4px;
}
/* ---- MAIN LAYOUT ---- */
.app-main {
max-width: 920px;
margin: 30px auto;
padding: 0 16px 60px;
}
/* ---- CARD ---- */
.card {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,0.07);
overflow: hidden;
margin-bottom: 24px;
}
.card-header {
background: #f0f4fa;
padding: 14px 22px;
border-bottom: 1px solid #e0e8f0;
display: flex;
align-items: center;
gap: 10px;
}
.card-header h2 {
font-size: 15px;
color: #003366;
font-weight: 700;
}
.card-body {
padding: 22px;
}
/* ---- TABS ---- */
.tab-bar {
display: flex;
border-bottom: 2px solid #e0e8f0;
margin-bottom: 24px;
}
.tab-btn {
padding: 10px 20px;
border: none;
background: none;
cursor: pointer;
font-size: 14px;
color: #666;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
transition: all 0.2s;
font-weight: 500;
}
.tab-btn.active {
color: #0066cc;
border-bottom-color: #0066cc;
font-weight: 700;
}
.tab-btn:hover:not(.active) { color: #333; }
.tab-pane { display: none; }
.tab-pane.active { display: block; }
/* ---- FORM ELEMENTS ---- */
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 16px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
font-size: 12px;
font-weight: 600;
color: #555;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 0.4px;
}
.form-group input,
.form-group select {
padding: 9px 11px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 14px;
transition: border 0.2s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0,102,204,0.1);
}
.input-with-unit {
display: flex;
align-items: center;
gap: 6px;
}
.input-with-unit input { flex: 1; }
.unit-label {
font-size: 12px;
color: #888;
white-space: nowrap;
}
.tol-row {
display: flex;
align-items: center;
gap: 8px;
}
.tol-row input { width: 80px; }
/* ---- MODE SELECTOR ---- */
.mode-selector {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.mode-btn {
padding: 7px 16px;
border: 2px solid #0066cc;
background: #fff;
color: #0066cc;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: all 0.2s;
}
.mode-btn.active {
background: #0066cc;
color: #fff;
}
.mode-btn:hover:not(.active) {
background: #e8f0fb;
}
/* ---- FORMULA BOX ---- */
.formula-box {
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 10px 14px;
font-size: 12px;
color: #666;
margin-top: 18px;
font-family: 'Courier New', monospace;
}
/* ---- RESULT DISPLAY ---- */
.result-box {
background: linear-gradient(135deg, #e8f4fd 0%, #d0e8fa 100%);
border: 2px solid #0066cc;
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 18px;
}
.result-box .result-label {
font-size: 12px;
color: #555;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
}
.result-box .result-value {
font-size: 28px;
font-weight: 700;
color: #003366;
line-height: 1.2;
}
.result-box .result-range {
font-size: 12px;
color: #666;
margin-top: 4px;
}
/* ---- DIAGRAM ---- */
.diagram-row {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
margin: 18px 0;
}
.diagram-svg-wrap {
flex-shrink: 0;
}
.diagram-legend {
font-size: 12px;
color: #555;
line-height: 1.8;
}
/* ---- BUTTONS ---- */
.btn-calc {
background: #0066cc;
color: #fff;
border: none;
border-radius: 6px;
padding: 11px 28px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: background 0.2s, box-shadow 0.2s;
}
.btn-calc:hover { background: #0052a3; box-shadow: 0 2px 8px rgba(0,102,204,0.3); }
.btn-reset {
background: #e0e0e0;
color: #444;
border: none;
border-radius: 6px;
padding: 11px 20px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
margin-left: 10px;
}
.btn-reset:hover { background: #d0d0d0; }
/* ---- INFO NOTE ---- */
.info-note {
background: #fffbea;
border-left: 4px solid #f0b429;
padding: 12px 16px;
border-radius: 4px;
font-size: 12px;
color: #666;
line-height: 1.6;
margin-top: 18px;
}
/* ---- SECTION SEPARATOR ---- */
.section-sep {
border: none;
border-top: 1px solid #e8ecf0;
margin: 20px 0;
}
/* ---- WEIGHT SECTION ---- */
.weight-section {
background: #f8fafa;
border: 1px dashed #aac;
border-radius: 6px;
padding: 14px 18px;
margin-top: 18px;
}
.weight-section h4 {
font-size: 13px;
color: #555;
margin-bottom: 12px;
}
/* ---- APP FOOTER ---- */
.app-footer {
text-align: center;
font-size: 11px;
color: #aaa;
padding: 18px;
border-top: 1px solid #e8ecf0;
margin-top: 10px;
}
/* ============================================
LOAD OPTIMIZER STYLES
============================================ */
.load-optimizer-section {
margin-top: 30px;
border: 2px solid #0066cc;
border-radius: 8px;
background-color: #f0f7ff;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,102,204,0.1);
}
.lo-accordion-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: linear-gradient(135deg, #0066cc 0%, #004499 100%);
color: white;
cursor: pointer;
user-select: none;
transition: background 0.3s ease;
}
.lo-accordion-header:hover {
background: linear-gradient(135deg, #0052a3 0%, #003d7a 100%);
}
.lo-accordion-header h3 {
margin: 0;
flex: 1;
font-size: 16px;
font-weight: 600;
}
.lo-accordion-icon {
font-size: 14px;
transition: transform 0.3s ease;
display: inline-block;
}
.lo-accordion-header.active .lo-accordion-icon {
transform: rotate(180deg);
}
.badge-beta-lo {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
background-color: rgba(255,255,255,0.3);
color: white;
}
.lo-accordion-content {
padding: 20px;
}
@keyframes loSlideDown {
from { opacity: 0; }
to { opacity: 1; }
}
.lo-accordion-content { animation: loSlideDown 0.3s ease; }
.lo-inputs {
display: grid;
gap: 20px;
margin-bottom: 30px;
}
.lo-presets-section, .lo-custom-section {
background: white;
padding: 16px;
border-radius: 6px;
border: 1px solid #ddd;
}
.lo-presets-section h4, .lo-custom-section h4 {
margin-top: 0;
color: #333;
font-size: 14px;
margin-bottom: 12px;
}
.lo-preset-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.lo-preset-btn {
padding: 8px 16px;
border: 2px solid #0066cc;
background: white;
color: #0066cc;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}
.lo-preset-btn:hover { background: #0066cc; color: white; }
.lo-preset-btn.active {
background: #0066cc;
color: white;
box-shadow: 0 0 0 3px rgba(0,102,204,0.2);
}
.lo-input-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.lo-input-field { display: flex; flex-direction: column; }
.lo-input-field label {
font-size: 12px;
font-weight: 600;
color: #666;
margin-bottom: 4px;
}
.lo-input-field input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 13px;
}
.lo-input-field input:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 2px rgba(0,102,204,0.1);
}
.lo-button-group { display: flex; gap: 12px; }
.lo-btn-primary, .lo-btn-secondary {
flex: 1;
padding: 12px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.lo-btn-primary { background: #0066cc; color: white; }
.lo-btn-primary:hover { background: #0052a3; box-shadow: 0 2px 6px rgba(0,102,204,0.3); }
.lo-btn-secondary { background: #e0e0e0; color: #333; }
.lo-btn-secondary:hover { background: #d0d0d0; }
.lo-results { display: grid; gap: 20px; }
.lo-results-summary {
background: white;
padding: 16px;
border-radius: 6px;
border-left: 4px solid #28a745;
}
.lo-results-summary h4 { margin-top: 0; color: #333; font-size: 14px; margin-bottom: 12px; }
.lo-summary-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
font-size: 13px;
}
.lo-summary-item:last-child { margin-bottom: 0; }
.lo-value-highlight { color: #0066cc; font-weight: bold; font-size: 16px; }
.lo-value-number { color: #28a745; font-weight: bold; }
.lo-util-group { display: flex; flex-direction: column; gap: 4px; text-align: right; }
.lo-results-table {
background: white;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
}
.lo-results-table h4 { margin-top: 0; margin-bottom: 12px; }
.lo-results-table table { width: 100%; border-collapse: collapse; font-size: 12px; }
.lo-results-table thead { background: #f5f5f5; }
.lo-results-table th {
padding: 8px;
text-align: left;
font-weight: 600;
color: #333;
border-bottom: 2px solid #0066cc;
}
.lo-results-table td { padding: 8px; border-bottom: 1px solid #e0e0e0; }
.lo-results-table tbody tr:hover { background: #f9f9f9; }
.lo-results-table tbody tr.lo-recommended { background: #fffacd; font-weight: bold; }
.lo-warning {
background: #fff8e1;
border: 1px solid #f0b429;
border-radius: 5px;
padding: 10px 14px;
font-size: 12px;
color: #7a5c00;
margin-bottom: 12px;
}
/* ---- SETTINGS SECTION ---- */
.lo-settings-section {
background: white;
padding: 16px;
border-radius: 6px;
border: 1px solid #ddd;
position: relative;
}
.lo-settings-section h4 {
margin-top: 0;
color: #333;
font-size: 14px;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.lo-settings-section .lo-settings-subtitle {
font-size: 11px;
color: #888;
margin-bottom: 12px;
}
.lo-settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.lo-settings-field {
display: flex;
flex-direction: column;
}
.lo-settings-field label {
font-size: 12px;
font-weight: 600;
color: #555;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 5px;
}
.lo-settings-field label .lo-settings-hint {
font-weight: 400;
color: #999;
font-size: 11px;
}
.lo-settings-input-wrap {
display: flex;
align-items: center;
gap: 6px;
}
.lo-settings-input-wrap input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 13px;
width: 90px;
}
.lo-settings-input-wrap input:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 2px rgba(0,102,204,0.1);
}
.lo-settings-input-wrap .lo-unit {
font-size: 12px;
color: #888;
}
.lo-settings-effective {
font-size: 11px;
color: #0066cc;
margin-top: 5px;
min-height: 16px;
}
@media (max-width: 768px) {
.lo-accordion-header h3 { font-size: 14px; }
.lo-preset-buttons { flex-direction: column; }
.lo-preset-btn { width: 100%; }
.lo-input-grid { grid-template-columns: 1fr; }
.lo-button-group { flex-direction: column; }
.form-row { grid-template-columns: 1fr; }
.lo-settings-grid { grid-template-columns: 1fr; }
}
/* ===== SIDE VIEW ===== */
.lo-sideview-section {
background: white; padding: 16px; border-radius: 6px; border: 1px solid #ddd;
}
.lo-sideview-section h4 { margin: 0 0 12px 0; font-size: 14px; color: #333; }
.lo-sideview-wrap {
background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 4px; overflow: hidden;
}
.lo-sideview-legend { display:flex; gap:20px; margin-top:10px; font-size:12px; color:#555; flex-wrap:wrap; }
.lo-legend-item { display:flex; align-items:center; gap:6px; }
.lo-legend-box { width:14px; height:14px; border-radius:2px; display:inline-block; border:1px solid #aaa; }
.roll-color { background:#a8d4f5; border-color:#0066cc; }
.container-color { background:#e8e8e8; border-color:#999; }
.margin-color { background: repeating-linear-gradient(45deg,#fff7e0,#fff7e0 3px,#ffe8a0 3px,#ffe8a0 6px); border-color:#f0b429; }
.core-presets { display:flex; gap:5px; margin-top:6px; flex-wrap:wrap; }
.core-preset-btn { padding:3px 9px; font-size:11px; font-weight:600; border:1px solid #0066cc; background:#fff; color:#0066cc; border-radius:4px; cursor:pointer; transition:all 0.15s; }
.core-preset-btn:hover, .core-preset-btn.active { background:#0066cc; color:#fff; }
/* ---- FORKLIFT CHECK BOX ---- */
.forklift-check{border-radius:7px;padding:14px 16px;margin-top:14px;font-size:13px;line-height:1.6;display:none;}
.forklift-check.fc-ok{background:#eafaf1;border:1.5px solid #28a745;}
.forklift-check.fc-warn{background:#fff8e1;border:1.5px solid #f0b429;}
.forklift-check.fc-block{background:#fff0f0;border:2px solid #dc3545;}
.forklift-check .fc-title{font-weight:700;font-size:13px;margin-bottom:8px;display:flex;align-items:center;gap:7px;}
.forklift-check ul{margin:6px 0 0 18px;padding:0;}
.forklift-check ul li{margin-bottom:3px;}
.fc-req-ok{color:#28a745;}
.fc-req-nok{color:#dc3545;font-weight:600;}
</style>
</head>
<body>
<!-- ============================================================
DISCLAIMER MODAL
============================================================ -->
<div id="disclaimerOverlay">
<div id="disclaimerBox">
<div class="disclaimer-badge">&#9888; BETA VERSION &middot; INTERNAL USE ONLY</div>
<h2>Roll Diameter Calculator &ndash; Naue GmbH &amp; Co. KG</h2>
<div class="disclaimer-section">
<h4>&#128274; Authorized Departments Only / Nur autorisierte Abteilungen</h4>
This tool is exclusively intended for internal use by:<br>
<strong>Production Planning &middot; Logistics &middot; Product Management</strong><br><br>
Dieses Tool ist ausschlie&szlig;lich f&uuml;r den internen Gebrauch in:<br>
<strong>Produktionsplanung &middot; Logistik &middot; Produktmanagement</strong>
</div>
<div class="disclaimer-section">
<h4>&#129514; Beta-Status</h4>
This application is in Beta. Results should be verified before operational use.<br>
Diese Anwendung befindet sich im Beta-Stadium. Ergebnisse sind vor dem produktiven Einsatz zu &uuml;berpr&uuml;fen.
</div>
<div class="disclaimer-section">
<h4>&#128202; Product Thickness Values</h4>
Pre-filled product thickness values are derived from an automated data evaluation and serve as a starting point only.
They must be independently verified before use in any calculation.<br>
Die vorgeschlagenen Produktdicken stammen aus einer automatischen Datenauswertung und m&uuml;ssen vor Verwendung in Berechnungen eigenst&auml;ndig &uuml;berpr&uuml;ft werden.
</div>
<div class="disclaimer-confirm">
<input type="checkbox" id="confirmCheck">
<label for="confirmCheck">
Ich best&auml;tige, dass ich zur Nutzung dieses Tools berechtigt bin und die obigen Hinweise gelesen und verstanden habe.<br>
<em>I confirm that I am authorised to use this tool and have read and understood the notices above.</em>
</label>
</div>
<div class="disclaimer-buttons">
<button id="btnCancel">&#10005; Abbrechen / Cancel</button>
<button id="btnAccept" disabled>&#10003; Akzeptieren &amp; Weiter / Accept &amp; Continue</button>
</div>
</div>
</div>
<!-- ============================================================
APP HEADER
============================================================ -->
<div class="app-header">
<div>
<div class="logo-text">
Naue
<span class="beta-pill">BETA</span>
<span class="internal-pill">Internal</span>
</div>
<div class="subtitle">Roll Diameter Calculator &middot; Roll Diameter &middot; Product Length &middot; Roll Weight</div>
</div>
</div>
<!-- ============================================================
MAIN
============================================================ -->
<div class="app-main">
<div class="card">
<div class="card-header">
<h2>&#9998; Roll Diameter Calculator</h2>
</div>
<div class="card-body">
<!-- TABS -->
<div class="tab-bar">
<button class="tab-btn active" data-tab="direct">&#9998; Direct Calculation</button>
<button class="tab-btn" data-tab="extrap">&#128269; Extrapolation</button>
</div>
<!-- ===================== TAB: DIRECT CALCULATION ===================== -->
<div class="tab-pane active" id="tab-direct">
<!-- Product Category + Site -->
<div class="form-row">
<div class="form-group">
<label>Product Category</label>
<select id="d-cat">
<option value="">&ndash; Select &ndash;</option>
<option value="bentofix">Bentofix</option>
<option value="secutex">Secutex</option>
<option value="carbofol">Carbofol</option>
<option value="secumat">Secumat</option>
<option value="secudrain">Secudrain</option>
</select>
</div>
<div class="form-group">
<label>Production Site</label>
<select id="d-site">
<option value="">&ndash; Select &ndash;</option>
<option value="espelkamp">Espelkamp</option>
<option value="malaysia">Malaysia</option>
</select>
</div>
</div>
<!-- Calculation Mode -->
<div class="form-group" style="margin-bottom:14px;">
<label>Calculation Mode</label>
<div class="mode-selector">
<button class="mode-btn active" data-mode="diameter">&rarr; Roll Diameter</button>
<button class="mode-btn" data-mode="length">&rarr; Product Length</button>
<button class="mode-btn" data-mode="target">&#127919; Target Diameter</button>
<button class="mode-btn" data-mode="thickness">&rarr; Product Thickness</button>
</div>
</div>
<!-- Diagram -->
<div class="diagram-row">
<div class="diagram-svg-wrap">
<svg width="120" height="120" viewBox="0 0 120 120">
<circle cx="60" cy="60" r="50" fill="#d0e8fa" stroke="#0066cc" stroke-width="2"/>
<circle cx="60" cy="60" r="20" fill="#fff" stroke="#666" stroke-width="2" stroke-dasharray="4,3"/>
<line x1="60" y1="60" x2="110" y2="60" stroke="#0066cc" stroke-width="1.5"/>
<line x1="60" y1="60" x2="80" y2="60" stroke="#666" stroke-width="1.5"/>
<text x="82" y="56" font-size="9" fill="#0066cc">D</text>
<text x="68" y="56" font-size="9" fill="#666">d</text>
</svg>
</div>
<div class="diagram-legend">
<div><strong>D</strong> = Roll outer diameter</div>
<div><strong>d</strong> = Core outer diameter</div>
<div><strong>t</strong> = Product thickness</div>
<div><strong>L</strong> = Product length</div>
</div>
</div>
<hr class="section-sep">
<!-- Input Parameters -->
<h4 style="font-size:13px;color:#555;margin-bottom:14px;">Input Parameters</h4>
<div class="form-row">
<div class="form-group">
<label>Core Outer Diameter (d)</label>
<div class="input-with-unit">
<input type="number" id="d-co" value="150" min="0" step="1">
<span class="unit-label">mm</span>
</div>
<div class="core-presets">
<button class="core-preset-btn" data-core="133">133mm Stahl</button>
<button class="core-preset-btn" data-core="150">150mm PVC</button>
<button class="core-preset-btn" data-core="168">168mm PVC</button>
<button class="core-preset-btn" data-core="194">194mm Stahl</button>
<button class="core-preset-btn" data-core="300">300</button>
</div>
</div>
<div class="form-group">
<label>Article Number (optional)</label>
<input type="text" id="d-art" list="d-art-list" placeholder="Search article no. or name..." autocomplete="off" style="width:100%;">
<datalist id="d-art-list"></datalist>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Product Thickness (t)</label>
<div class="tol-row">
<input type="number" id="d-th" placeholder="mm" min="0" step="0.01">
<span class="unit-label">&plusmn;</span>
<input type="number" id="d-tol" placeholder="mm" min="0" step="0.01" style="width:80px;">
<span class="unit-label">mm</span>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Product Length (L)</label>
<div class="input-with-unit">
<input type="number" id="d-le" placeholder="m" min="0" step="0.1">
<span class="unit-label">m</span>
</div>
</div>
<div class="form-group">
<label>Roll Diameter (D)</label>
<div class="input-with-unit">
<input type="number" id="d-di" placeholder="mm" min="0" step="1">
<span class="unit-label">mm</span>
</div>
</div>
</div>
<!-- Weight (optional) -->
<div class="weight-section">
<h4>Roll Weight (optional)</h4>
<div class="form-row">
<div class="form-group">
<label>Roll Width</label>
<div class="input-with-unit">
<input type="number" id="de-wi" placeholder="m" min="0" step="0.01">
<span class="unit-label">m</span>
</div>
</div>
<div class="form-group">
<label>Area Weight</label>
<div class="input-with-unit">
<input type="number" id="d-aw" placeholder="g/m&sup2;" min="0" step="1">
<span class="unit-label">g/m&sup2;</span>
</div>
</div>
<div class="form-group">
<label>Calculated Weight</label>
<div class="input-with-unit">
<input type="text" id="d-wo" readonly placeholder="&ndash;" style="background:#f0f0f0;">
<span class="unit-label">kg</span>
</div>
</div>
</div>
</div>
<!-- Forklift / Weight Check -->
<div class="forklift-check" id="forkliftCheck">
<div class="fc-title" id="forkliftCheckTitle"></div>
<div id="forkliftCheckBody"></div>
</div>
<div style="margin-top:18px;">
<button class="btn-calc" id="d-calc">&#9654; Calculate</button>
<button class="btn-reset" id="d-reset">Reset</button>
</div>
<!-- Result -->
<div id="d-result" style="display:none; margin-top:20px;">
<div class="result-box">
<div class="result-label" id="d-res-label">Result</div>
<div class="result-value" id="d-res-val">&ndash;</div>
<div class="result-range" id="d-res-range"></div>
</div>
</div>
<div class="formula-box">
Geometry: &pi;/4 &times; (D&sup2; &minus; d&sup2;) = L &times; t &nbsp;|&nbsp; Weight: G [kg] = Area Weight [g/m&sup2;] &times; L [m] &times; Width [m] / 1000
</div>
</div>
<!-- END TAB DIRECT -->
<!-- ===================== TAB: EXTRAPOLATION ===================== -->
<div class="tab-pane" id="tab-extrap">
<h4 style="font-size:13px;color:#555;margin-bottom:14px;">Known Reference Roll</h4>
<div class="form-row">
<div class="form-group">
<label>Article Number (optional)</label>
<input type="text" id="e-art" list="e-art-list" placeholder="Search article no. or name..." autocomplete="off" style="width:100%;">
<datalist id="e-art-list"></datalist>
</div>
<div class="form-group">
<label>Core Outer Diameter (d)</label>
<div class="input-with-unit">
<input type="number" id="e-co" value="150" min="0" step="1">
<span class="unit-label">mm</span>
</div>
<div class="core-presets">
<button class="core-preset-btn" data-core="133">133mm Stahl</button>
<button class="core-preset-btn" data-core="150">150mm PVC</button>
<button class="core-preset-btn" data-core="168">168mm PVC</button>
<button class="core-preset-btn" data-core="194">194mm Stahl</button>
<button class="core-preset-btn" data-core="300">300</button>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Known Roll Diameter (D&#8320;)</label>
<div class="input-with-unit">
<input type="number" id="e-d0" placeholder="mm" min="0" step="1">
<span class="unit-label">mm</span>
</div>
</div>
<div class="form-group">
<label>Known Roll Length (L&#8320;)</label>
<div class="input-with-unit">
<input type="number" id="e-l0" placeholder="m" min="0" step="0.1">
<span class="unit-label">m</span>
</div>
</div>
</div>
<hr class="section-sep">
<h4 style="font-size:13px;color:#555;margin-bottom:14px;">New Roll Length</h4>
<div class="form-row">
<div class="form-group">
<label>New Roll Length (L&#8321;)</label>
<div class="input-with-unit">
<input type="number" id="e-l1" placeholder="m" min="0" step="0.1">
<span class="unit-label">m</span>
</div>
</div>
<div class="form-group">
<label>Tolerance (optional)</label>
<div class="tol-row">
<span class="unit-label">&plusmn;</span>
<input type="number" id="e-tol" placeholder="mm" min="0" step="0.1" style="width:100px;">
<span class="unit-label">mm</span>
</div>
</div>
</div>
<!-- Weight (optional) -->
<div class="weight-section">
<h4>Roll Weight (optional)</h4>
<div class="form-row">
<div class="form-group">
<label>Roll Width</label>
<div class="input-with-unit">
<input type="number" id="e-wi" placeholder="m" min="0" step="0.01">
<span class="unit-label">m</span>
</div>
</div>
<div class="form-group">
<label>Area Weight</label>
<div class="input-with-unit">
<input type="number" id="e-aw" placeholder="g/m&sup2;" min="0" step="1">
<span class="unit-label">g/m&sup2;</span>
</div>
</div>
</div>
</div>
<div style="margin-top:18px;">
<button class="btn-calc" id="e-calc">&#9654; Calculate Roll Diameter</button>
<button class="btn-reset" id="e-reset">Reset</button>
</div>
<!-- Result -->
<div id="e-result" style="display:none; margin-top:20px;">
<div class="result-box">
<div class="result-label">Extrapolated Roll Diameter (D&#8321;)</div>
<div class="result-value" id="e-res-val">&ndash;</div>
<div class="result-range" id="e-res-range"></div>
</div>
</div>
<div class="formula-box">
Geometry: D&#8321; = &radic;(d&sup2; + L&#8321;/L&#8320; &times; (D&#8320;&sup2; &minus; d&sup2;)) &nbsp;|&nbsp; Weight: G [kg] = Area Weight &times; L&#8321; &times; Width / 1000
</div>
</div>
<!-- END TAB EXTRAP -->
</div><!-- card-body -->
</div><!-- card -->
<!-- INFO NOTE -->
<div class="info-note">
<strong>&#9432; Calculation Notes</strong><br>
The calculated roll diameters assume a perfectly cylindrical roll with uniform, gap-free winding.<br><br>
The product thickness can vary during production within the permitted manufacturing tolerance.
The actual roll diameter may therefore differ from the calculated values.<br><br>
Use the <em>&plusmn; Tolerance</em> field next to the product thickness to estimate this effect &ndash; the result will then show the minimum, nominal and maximum roll diameter.
</div>
<!-- ============================================================
LOAD OPTIMIZER SECTION
============================================================ -->
<div id="loadOptimizerContainer" class="load-optimizer-section">
<div class="lo-accordion-header" id="loAccordionToggle">
<span class="lo-accordion-icon">&#9660;</span>
<h3>&#127919; Load Optimizer &ndash; Transport Capacity Calculator</h3>
<span class="badge-beta-lo">BETA</span>
</div>
<div class="lo-accordion-content" id="loAccordionContent" style="display:none;">
<div class="lo-warning">
&#9888; The Load Optimizer uses roll data from the <strong>Direct Calculation</strong> tab above.
Please complete a roll calculation first (Roll Diameter + Roll Width + Roll Weight must be filled).
</div>
<div class="lo-inputs">
<!-- ===== SETTINGS SECTION ===== -->
<div class="lo-settings-section">
<h4>&#9881;&#65039; Loading Clearances</h4>
<div class="lo-settings-subtitle">Safety margins applied to the effective loading space before calculation.</div>
<div class="lo-settings-grid">
<div class="lo-settings-field">
<label>
Side Wall Clearance
<span class="lo-settings-hint">(one side)</span>
</label>
<div class="lo-settings-input-wrap">
<input type="number" id="loMarginSide" value="0" min="0" max="100" step="1">
<span class="lo-unit">cm</span>
</div>
<div class="lo-settings-effective" id="loMarginSideEffective"></div>
</div>
<div class="lo-settings-field">
<label>
Ceiling Clearance
<span class="lo-settings-hint">(top only)</span>
</label>
<div class="lo-settings-input-wrap">
<input type="number" id="loMarginCeiling" value="20" min="0" max="100" step="1">
<span class="lo-unit">cm</span>
</div>
<div class="lo-settings-effective" id="loMarginCeilingEffective"></div>
</div>
</div>
</div>
<!-- ===== END SETTINGS ===== -->
<div class="lo-presets-section">
<h4>&#128280; Preset Transport Type:</h4>
<div class="lo-preset-buttons">
<button class="lo-preset-btn" data-preset="container_20ft">20ft Container</button>
<button class="lo-preset-btn" data-preset="container_40ft">40ft Container</button>
<button class="lo-preset-btn" data-preset="container_40ft_hc">40ft High Cube</button>
<button class="lo-preset-btn" data-preset="lkw_sattelzug">LKW Sattelzug</button>
<button class="lo-preset-btn" data-preset="lkw_tandem">LKW Tandem</button>
<button class="lo-preset-btn" data-preset="custom">Custom</button>
</div>
</div>
<div class="lo-custom-section">
<h4>&#128207; Dimensions &amp; Weight Limit:</h4>
<div class="lo-input-grid">
<div class="lo-input-field">
<label>Length (m)</label>
<input type="number" id="customLength" placeholder="e.g. 12.19" step="0.01" min="0">
</div>
<div class="lo-input-field">
<label>Width (m)</label>
<input type="number" id="customWidth" placeholder="e.g. 2.35" step="0.01" min="0">
</div>
<div class="lo-input-field">
<label>Height (m)</label>
<input type="number" id="customHeight" placeholder="e.g. 2.39" step="0.01" min="0">
</div>
<div class="lo-input-field">
<label>Max. Load (kg)</label>
<input type="number" id="customMaxWeight" placeholder="e.g. 30480" step="100" min="0">
</div>
</div>
</div>
<div class="lo-button-group">
<button class="lo-btn-primary" id="calculateLoad">&#9654; Calculate</button>
<button class="lo-btn-secondary" id="resetLoad">&#8634; Reset</button>
</div>
</div>
<div class="lo-results" id="resultsContainer" style="display:none;">
<div class="lo-results-summary">
<h4>Results:</h4>
<div class="lo-summary-item">
<span><strong>Limiting Factor:</strong></span>
<span id="limitingFactorValue" class="lo-value-highlight">&ndash;</span>
</div>
<div class="lo-summary-item">
<span><strong>Total Rolls:</strong></span>
<div style="text-align:right;">
<span id="totalRollsValue" class="lo-value-highlight">&ndash;</span>
<small id="rollsDetailValue" style="display:block;color:#888;"></small>
</div>
</div>
<div class="lo-summary-item">
<span><strong>Utilization:</strong></span>
<div class="lo-util-group">
<div>Weight: <span id="weightUtilValue" class="lo-value-number">&ndash;</span> %</div>
<div>Volume: <span id="volumeUtilValue" class="lo-value-number">&ndash;</span> %</div>
<div>Product: <span id="productM2Value" class="lo-value-number">&ndash;</span> m&sup2;</div>
</div>
</div>
</div>
<div class="lo-results-table">
<h4>Comparison Table:</h4>
<table id="comparisonTable">
<thead>
<tr>
<th>Scenario</th>
<th>Rolls</th>
<th>kg</th>
<th>m&sup2;</th>
<th>Util. %</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="lo-sideview-section" id="sideViewSection" style="display:none;">
<h4>&#128269; Front View &ndash; Cross-Section</h4>
<div class="lo-sideview-wrap">
<svg id="sideViewSVG" width="100%" viewBox="0 0 800 300" xmlns="http://www.w3.org/2000/svg">
<text x="400" y="130" text-anchor="middle" fill="#aaa" font-size="14">Run a calculation to see the side view.</text>
</svg>
</div>
<div class="lo-sideview-legend">
<span class="lo-legend-item"><span class="lo-legend-box roll-color"></span> Roll (cross-section, &Oslash; = diameter)</span>
<span class="lo-legend-item"><span class="lo-legend-box container-color"></span> Container / Vehicle</span>
<span class="lo-legend-item"><span class="lo-legend-box margin-color"></span> Clearance zone</span>
</div>
</div>
</div>
</div>
</div>
<!-- END LOAD OPTIMIZER -->
</div><!-- app-main -->
<div class="app-footer">
&#9632; Naue GmbH &amp; Co. KG &nbsp;&middot;&nbsp; Roll Diameter Calculator &nbsp;&middot;&nbsp; Internal Application
</div>
<!-- ============================================================
ARTICLE DATA (JSON)
============================================================ -->
<script>
window.ARTICLE_DATA = [];
// Inline config fallback – always available, even without server
window.APP_CONFIG = {"_comment":"Naue Roll Calculator - General Configuration. Article data stays in article-data.json.","forklift_rules":{"_comment":"Weight-based forklift and core diameter rules. Applies per product category.","applicable_categories":["bentofix"],"applicable_sites":["espelkamp","malaysia"],"rules":[{"id":"heavy_roll","condition":"weight_kg >= 1700","weight_threshold_kg":1700,"max_weight_kg":2750,"requirements":{"min_core_outer_diameter_mm":170,"forklift_mandrel_diameter_mm":140,"forklift_capacity_t":7.5},"messages":{"en":{"warning_title":"Heavy Roll – Special Equipment Required","requirements_label":"The following requirements apply for rolls ≥ 1700 kg (Bentofix):","req_core":"Core outer diameter ≥ 170 mm required (current: {core} mm)","req_mandrel":"Forklift mandrel diameter: 140 mm required","req_forklift":"Forklift capacity: 7.5 t required","core_ok":"Core diameter OK (≥ 170 mm)","core_nok":"Core diameter too small – minimum 170 mm required (current: {core} mm)"},"de":{"warning_title":"Schwere Rolle – Sonderausrüstung erforderlich","requirements_label":"Für Rollen ≥ 1700 kg (Bentofix) gelten folgende Anforderungen:","req_core":"Kernaußendurchmesser ≥ 170 mm erforderlich (aktuell: {core} mm)","req_mandrel":"Gabelstapler-Dorn: 140 mm Durchmesser erforderlich","req_forklift":"Gabelstapler: 7,5 t Tragkraft erforderlich","core_ok":"Kerndurchmesser OK (≥ 170 mm)","core_nok":"Kerndurchmesser zu klein – Minimum 170 mm erforderlich (aktuell: {core} mm)"}}}],"hard_limit":{"max_weight_kg":2750,"messages":{"en":{"block_title":"Weight Limit Exceeded – Calculation Blocked","block_text":"The calculated roll weight of {weight} kg exceeds the absolute maximum of 2750 kg for Bentofix. Please adjust the product length or roll dimensions."},"de":{"block_title":"Gewichtslimit überschritten – Berechnung gesperrt","block_text":"Das berechnete Rollengewicht von {weight} kg überschreitet das absolute Maximum von 2750 kg für Bentofix. Bitte Produktlänge oder Rollenmaße anpassen."}}}}};
(async function() {
try {
const [artResp, cfgResp] = await Promise.all([
fetch('/static/article-data.json'), fetch('/static/config.json')
]);
if (artResp.ok) {
window.ARTICLE_DATA = await artResp.json();
console.log('Article data loaded:', window.ARTICLE_DATA.length, 'items');
populateArticleDropdowns();
} else { console.error('Failed to load article data:', artResp.status); }
if (cfgResp.ok) { window.APP_CONFIG = await cfgResp.json(); console.log('Config updated from file'); }
else { console.info('config.json not found – using inline config.'); }
} catch (e) { console.error('Error loading data:', e); }
})();
</script>
<!-- ============================================================
LOAD OPTIMIZER CORE
============================================================ -->
<script>
class LoadOptimizer {
constructor() {
this.state = {
rollDiameter: 0, coreDiameter: 150, rollWidth: 0,
rollWeight: 0, m2PerRoll: 0, productLength: 0
};
this.presets = {
container_20ft: { name:"20ft Container", length:5.90, width:2.35, height:2.39, maxWeight:21600 },
container_40ft: { name:"40ft Container", length:12.19, width:2.35, height:2.39, maxWeight:30480 },
container_40ft_hc:{ name:"40ft High Cube", length:12.19, width:2.35, height:2.69, maxWeight:30480 },
lkw_sattelzug: { name:"LKW Sattelzug", length:13.50, width:2.50, height:2.70, maxWeight:24000 },
lkw_tandem: { name:"LKW Tandem", length:13.50, width:2.50, height:2.70, maxWeight:25000 }
};
}
setRollData(data) {
this.state = {
rollDiameter: data.rollDiameter || 0,
coreDiameter: data.coreDiameter || 150,
rollWidth: data.rollWidth || 0,
rollWeight: data.rollWeight || 0,
m2PerRoll: data.m2PerRoll || 0,
productLength: data.productLength || 0
};
}
// marginSide (m) = one side wall clearance
// marginCeiling (m) = top clearance
calculateLoadUtilization(dim, maxLoadWeight, marginSide, marginCeiling) {
marginSide = marginSide || 0;
marginCeiling = marginCeiling || 0;
const { rollDiameter, coreDiameter, rollWidth, rollWeight, m2PerRoll } = this.state;
const D = rollDiameter / 1000, d = coreDiameter / 1000;
// Apply clearances to effective loading space
const effWidth = dim.width - marginSide;
const effHeight = dim.height - marginCeiling;
// Rolls liegen auf der Seite: Laenge der Rolle = rollWidth, Durchmesser = D
const rollsAlongLength = Math.floor(dim.length / rollWidth); // entlang Containerlaenge (kein Margin)
const rollsAlongWidth = Math.floor(effWidth / D); // quer (Durchmesser, mit Seitenabstand)
const rollsPerLayer = rollsAlongLength * rollsAlongWidth;
const maxStackHeight = Math.floor(effHeight / D); // Stapellagen (mit Deckenabstand)
const maxByGeometry = rollsPerLayer * maxStackHeight;
const maxByWeight = rollWeight > 0 ? Math.floor(maxLoadWeight / rollWeight) : Infinity;
// Volume: use original dim for container volume, effective for rolls
const volPerRoll = (Math.PI / 4) * (D*D - d*d) * rollWidth;
const containerVol = dim.length * dim.width * dim.height;
const effVol = dim.length * effWidth * effHeight;
const maxByVolume = volPerRoll > 0 ? Math.floor(effVol / volPerRoll) : Infinity;
const scenarios = [
{ name:'Weight', rolls: maxByWeight, weight: maxByWeight * rollWeight, type:'weight' },
{ name:'Volume', rolls: maxByVolume, weight: maxByVolume * rollWeight, type:'volume' },
{ name:'Geometry', rolls: maxByGeometry, weight: maxByGeometry * rollWeight, type:'geometry' }
];
const limiting = scenarios.reduce((mn, c) => c.rolls < mn.rolls ? c : mn);
const finalRolls = limiting.rolls === Infinity ? 0 : limiting.rolls;
const finalWeight = finalRolls * rollWeight;
const finalM2 = finalRolls * m2PerRoll;
return {
scenarios, limiting, finalRolls, finalWeight, finalM2,
utilizationWeight: maxLoadWeight > 0 ? (finalWeight / maxLoadWeight) * 100 : 0,
utilizationVolume: containerVol > 0 ? (finalRolls * volPerRoll / containerVol) * 100 : 0,
rollsPerLayer, rollsAlongLength, rollsAlongWidth, maxStackHeight,
containerVol, volPerRoll, effWidth, effHeight,
marginSide, marginCeiling
};
}
fmt(n, dec=1) { return isFinite(n) ? n.toFixed(dec) : '\u2013'; }
}
window.LoadOptimizer = LoadOptimizer;
</script>
<!-- ============================================================
LOAD OPTIMIZER UI
============================================================ -->
<script>
class LoadOptimizerUI {
constructor() {
this.optimizer = new window.LoadOptimizer();
this.currentPreset = null;
this.init();
}
init() {
document.getElementById('loAccordionToggle').addEventListener('click', () => this.toggleAccordion());
document.querySelectorAll('.lo-preset-btn').forEach(btn =>
btn.addEventListener('click', e => this.loadPreset(e.target.dataset.preset))
);
document.getElementById('calculateLoad').addEventListener('click', () => this.calculate());
document.getElementById('resetLoad').addEventListener('click', () => this.resetForm());
// Live-preview of effective dimensions when margins or dimensions change
const updateEffective = () => this.updateEffectiveHints();
['loMarginSide','loMarginCeiling','customWidth','customHeight'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('input', updateEffective);
});
}
// Show effective dimension hints below margin inputs
updateEffectiveHints() {
const sideM = parseFloat(document.getElementById('loMarginSide').value) || 0;
const ceilM = parseFloat(document.getElementById('loMarginCeiling').value) || 0;
const W = parseFloat(document.getElementById('customWidth').value) || 0;
const H = parseFloat(document.getElementById('customHeight').value) || 0;
const sideEl = document.getElementById('loMarginSideEffective');
const ceilEl = document.getElementById('loMarginCeilingEffective');
if (W > 0 && sideM > 0) {
const eff = (W - sideM / 100).toFixed(2);
sideEl.textContent = '\u2192 effective width: ' + eff + ' m';
} else {
sideEl.textContent = W > 0 ? '\u2192 effective width: ' + W.toFixed(2) + ' m' : '';
}
if (H > 0 && ceilM > 0) {
const eff = (H - ceilM / 100).toFixed(2);
ceilEl.textContent = '\u2192 effective height: ' + eff + ' m';
} else {
ceilEl.textContent = H > 0 ? '\u2192 effective height: ' + H.toFixed(2) + ' m' : '';
}
}
toggleAccordion() {
const header = document.getElementById('loAccordionToggle');
const content = document.getElementById('loAccordionContent');
const isOpen = content.style.display !== 'none';
content.style.display = isOpen ? 'none' : 'block';
header.classList.toggle('active');
}
loadPreset(id) {
if (id === 'custom') { this.clearPresets(); return; }
const p = this.optimizer.presets[id];
if (!p) return;
document.getElementById('customLength').value = p.length;
document.getElementById('customWidth').value = p.width;
document.getElementById('customHeight').value = p.height;
document.getElementById('customMaxWeight').value = p.maxWeight;
this.updatePresetBtns(id);
this.currentPreset = id;
this.updateEffectiveHints();
}
updatePresetBtns(activeId) {
document.querySelectorAll('.lo-preset-btn').forEach(b => b.classList.remove('active'));
if (activeId) document.querySelector('[data-preset="'+activeId+'"]')?.classList.add('active');
}
clearPresets() { this.updatePresetBtns(null); this.currentPreset = null; }
// Read margin values (cm -> m)
getMargins() {
const sideM = (parseFloat(document.getElementById('loMarginSide').value) || 0) / 100;
const ceilM = (parseFloat(document.getElementById('loMarginCeiling').value) || 0) / 100;
return { sideM, ceilM };
}
calculate() {
const L = parseFloat(document.getElementById('customLength').value);
const W = parseFloat(document.getElementById('customWidth').value);
const H = parseFloat(document.getElementById('customHeight').value);
const MW = parseFloat(document.getElementById('customMaxWeight').value);
if ([L,W,H,MW].some(v => isNaN(v) || v <= 0)) {
alert('Please fill in all transport dimensions!');
return;
}
const rollData = this.getRollData();
if (!rollData) {
alert('Please complete a roll calculation first (Direct Calculation tab).\nRoll Diameter, Roll Width and Roll Weight are required.');
return;
}
const { sideM, ceilM } = this.getMargins();
this.optimizer.setRollData(rollData);
const res = this.optimizer.calculateLoadUtilization({ length:L, width:W, height:H }, MW, sideM, ceilM);
this.showResults(res, MW);
this.drawSideView(res, { length:L, width:W, height:H });
}
getRollData() {
const D = parseFloat(document.getElementById('d-di')?.value || 0);
const W = parseFloat(document.getElementById('de-wi')?.value || 0);
const GW = parseFloat(document.getElementById('d-wo')?.value || 0);
const L = parseFloat(document.getElementById('d-le')?.value || 0);
const d = parseFloat(document.getElementById('d-co')?.value || 150);
if (!D || !W || !GW) return null;
return { rollDiameter:D, coreDiameter:d, rollWidth:W, rollWeight:GW, m2PerRoll:L*W, productLength:L };
}
showResults(res, maxW) {
const m2PerRoll = this.optimizer.state.m2PerRoll || 0;
document.getElementById('limitingFactorValue').textContent = res.limiting.name;
document.getElementById('totalRollsValue').textContent = res.finalRolls;
document.getElementById('rollsDetailValue').textContent =
'(' + res.rollsAlongLength + ' along length \u00d7 ' + res.rollsAlongWidth + ' across, ' + res.maxStackHeight + ' layers)';
document.getElementById('weightUtilValue').textContent = this.optimizer.fmt(res.utilizationWeight);
document.getElementById('volumeUtilValue').textContent = this.optimizer.fmt(res.utilizationVolume);
document.getElementById('productM2Value').textContent = this.optimizer.fmt(res.finalM2, 0);
const tbody = document.querySelector('#comparisonTable tbody');
tbody.innerHTML = '';
res.scenarios.forEach(sc => {
const tr = document.createElement('tr');
const lim = (sc.type === res.limiting.type);
if (lim) tr.classList.add('lo-recommended');
const util = maxW > 0 ? (sc.weight / maxW * 100).toFixed(1) : '\u2013';
const scM2 = isFinite(sc.rolls) && m2PerRoll > 0 ? Math.round(sc.rolls * m2PerRoll) : '\u2013';
tr.innerHTML =
'<td>'+ sc.name +(lim?' \u2605':'')+'</td>'+
'<td>'+ (sc.rolls === Infinity ? '\u2013' : sc.rolls) +'</td>'+
'<td>'+ (isFinite(sc.weight) ? Math.round(sc.weight) : '\u2013') +'</td>'+
'<td>'+ scM2 +'</td>'+
'<td>'+ util +'%</td>';
tbody.appendChild(tr);
});
document.getElementById('resultsContainer').style.display = 'block';
}
drawSideView(res, dim) {
const svg = document.getElementById('sideViewSVG');
const section = document.getElementById('sideViewSection');
if (!svg || !res || res.finalRolls === 0) return;
const D_m = this.optimizer.state.rollDiameter / 1000;
const aW = res.rollsAlongWidth;
const aL = res.rollsAlongLength;
const layers = res.maxStackHeight;
const total = res.finalRolls;
const marginSide = res.marginSide || 0; // m
const marginCeil = res.marginCeiling || 0; // m
const PAD = 52, W_SVG = 800, H_SVG = 300;
const scaleX = (W_SVG - 2*PAD) / dim.width;
const scaleY = (H_SVG - 2*PAD) / dim.height;
const scale = Math.min(scaleX, scaleY);
const cW = dim.width * scale;
const cH = dim.height * scale;
const cX = PAD + ((W_SVG - 2*PAD) - cW) / 2;
const cY = PAD + ((H_SVG - 2*PAD) - cH) / 2;
const rD = D_m * scale;
// Margin in SVG pixels
const mSidePx = marginSide * scale;
const mCeilPx = marginCeil * scale;
let s = '';
s += '<defs>';
s += '<marker id="arr" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="#666"/></marker>';
s += '<marker id="arr2" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto-start-reverse"><path d="M0,0 L0,6 L6,3 z" fill="#666"/></marker>';
// Hatch pattern for clearance zones
s += '<pattern id="hatchMargin" patternUnits="userSpaceOnUse" width="8" height="8" patternTransform="rotate(45)">';
s += '<rect width="8" height="8" fill="#fff7e0"/>';
s += '<line x1="0" y1="0" x2="0" y2="8" stroke="#f0b429" stroke-width="2.5"/>';
s += '</pattern>';
s += '</defs>';
s += '<rect x="0" y="0" width="800" height="300" fill="#f8f9fa"/>';
// Container outline
s += '<rect x="'+cX.toFixed(1)+'" y="'+cY.toFixed(1)+'" width="'+cW.toFixed(1)+'" height="'+cH.toFixed(1)+'" fill="#e8e8e8" stroke="#888" stroke-width="2.5" rx="3"/>';
s += '<rect x="'+cX.toFixed(1)+'" y="'+(cY+cH-5).toFixed(1)+'" width="'+cW.toFixed(1)+'" height="5" fill="#bbb"/>';
// Clearance zones (drawn before rolls)
if (mSidePx > 0.5) {
// Right side wall clearance zone
s += '<rect x="'+(cX+cW-mSidePx).toFixed(1)+'" y="'+cY.toFixed(1)+'"'
+ ' width="'+mSidePx.toFixed(1)+'" height="'+cH.toFixed(1)+'"'
+ ' fill="url(#hatchMargin)" opacity="0.9"/>';
// Label if wide enough
if (mSidePx > 18) {
s += '<text x="'+(cX+cW-mSidePx/2).toFixed(1)+'" y="'+(cY+cH/2).toFixed(1)+'"'
+ ' text-anchor="middle" font-size="9" fill="#b07800" transform="rotate(-90,'
+ (cX+cW-mSidePx/2).toFixed(1)+','+(cY+cH/2).toFixed(1)+')">'
+ (marginSide*100).toFixed(0)+' cm</text>';
}
}
if (mCeilPx > 0.5) {
// Ceiling clearance zone
s += '<rect x="'+cX.toFixed(1)+'" y="'+cY.toFixed(1)+'"'
+ ' width="'+cW.toFixed(1)+'" height="'+mCeilPx.toFixed(1)+'"'
+ ' fill="url(#hatchMargin)" opacity="0.9"/>';
if (mCeilPx > 12) {
s += '<text x="'+(cX+cW/2).toFixed(1)+'" y="'+(cY+mCeilPx/2+4).toFixed(1)+'"'
+ ' text-anchor="middle" font-size="9" fill="#b07800">'
+ (marginCeil*100).toFixed(0)+' cm</text>';
}
}
// Draw rolls — position within effective area
// Rolls start from bottom of container (above floor bar), stack upward within effHeight
// Side: rolls start from left wall, fill effWidth
let drawn = 0;
for (let layer = 0; layer < layers && drawn < total; layer++) {
for (let col = 0; col < aW && drawn < total; col++) {
// cx: from left wall (no side margin on left side)
const cx = cX + col * rD + rD / 2;
// cy: from bottom, stacking up, ceiling margin at top
const cy = cY + cH - layer * rD - rD / 2 - 5;
const r = Math.max(1, rD / 2 - 1.0);
// Shadow
s += '<circle cx="'+(cx+1.5).toFixed(1)+'" cy="'+(cy+1.5).toFixed(1)+'" r="'+r.toFixed(1)+'" fill="rgba(0,0,0,0.08)"/>';
// Roll body
s += '<circle cx="'+cx.toFixed(1)+'" cy="'+cy.toFixed(1)+'" r="'+r.toFixed(1)+'" fill="#a8d4f5" stroke="#0066cc" stroke-width="1.2"/>';
// Core
if (rD > 16) {
const kr = Math.max(1.5, rD * 0.13);
s += '<circle cx="'+cx.toFixed(1)+'" cy="'+cy.toFixed(1)+'" r="'+kr.toFixed(1)+'" fill="white" stroke="#0052a3" stroke-width="1" opacity="0.8"/>';
}
drawn++;
}
}
// Dimension arrows
const dimY = cY + cH + 20;
s += '<line x1="'+cX.toFixed(1)+'" y1="'+dimY.toFixed(1)+'" x2="'+(cX+cW).toFixed(1)+'" y2="'+dimY.toFixed(1)+'" stroke="#666" stroke-width="1" marker-start="url(#arr2)" marker-end="url(#arr)"/>';
s += '<text x="'+(cX+cW/2).toFixed(1)+'" y="'+(dimY+13).toFixed(1)+'" text-anchor="middle" font-size="11" fill="#555">'+dim.width+' m</text>';
const dimX = cX - 22;
const midY = (cY + cH/2).toFixed(1);
s += '<line x1="'+dimX.toFixed(1)+'" y1="'+cY.toFixed(1)+'" x2="'+dimX.toFixed(1)+'" y2="'+(cY+cH).toFixed(1)+'" stroke="#666" stroke-width="1" marker-start="url(#arr2)" marker-end="url(#arr)"/>';
s += '<text x="'+(dimX-5).toFixed(1)+'" y="'+midY+'" text-anchor="middle" font-size="11" fill="#555" transform="rotate(-90,'+(dimX-5).toFixed(1)+','+midY+')">'+dim.height+' m</text>';
s += '<text x="'+(cX+cW/2).toFixed(1)+'" y="'+(cY-8).toFixed(1)+'" text-anchor="middle" font-size="11" fill="#666" font-style="italic">'+aL+' rows deep (into page)</text>';
s += '<text x="'+(cX+cW-4).toFixed(1)+'" y="'+(cY+15).toFixed(1)+'" text-anchor="end" font-size="10" fill="#555" font-style="italic">'+aW+' wide \u00d7 '+layers+' high \u00d7 '+aL+' deep \u00b7 '+total+' rolls</text>';
// Roll diameter annotation
if (rD > 30 && aW > 0) {
const rx1 = cX, rx2 = cX + rD, ry = cY + cH - rD/2 - 5;
s += '<line x1="'+rx1.toFixed(1)+'" y1="'+(ry+rD/2+10).toFixed(1)+'" x2="'+rx2.toFixed(1)+'" y2="'+(ry+rD/2+10).toFixed(1)+'" stroke="#0066cc" stroke-width="0.8" stroke-dasharray="3,2" marker-start="url(#arr2)" marker-end="url(#arr)"/>';
s += '<text x="'+(cX+rD/2).toFixed(1)+'" y="'+(ry+rD/2+22).toFixed(1)+'" text-anchor="middle" font-size="10" fill="#0066cc">\u00d8 '+(D_m*1000).toFixed(0)+' mm</text>';
}
svg.setAttribute('viewBox', '0 0 800 300');
svg.innerHTML = s;
section.style.display = 'block';
}
resetForm() {
['customLength','customWidth','customHeight','customMaxWeight'].forEach(id =>
document.getElementById(id).value = ''
);
// Reset margins to defaults
document.getElementById('loMarginSide').value = '0';
document.getElementById('loMarginCeiling').value = '20';
document.getElementById('loMarginSideEffective').textContent = '';
document.getElementById('loMarginCeilingEffective').textContent = '';
document.getElementById('resultsContainer').style.display = 'none';
document.getElementById('sideViewSection').style.display = 'none';
this.clearPresets();
}
}
</script>
<!-- ============================================================
FORKLIFT / WEIGHT CHECK LOGIC
============================================================ -->
<script>
const ForkliftChecker = {
getRules() { return (window.APP_CONFIG && window.APP_CONFIG.forklift_rules) || null; },
isApplicable(cat) {
const r = this.getRules();
return r ? (r.applicable_categories||[]).includes((cat||'').toLowerCase()) : false;
},
evaluate(weightKg, coreMm, category) {
const box = document.getElementById('forkliftCheck');
const titleEl = document.getElementById('forkliftCheckTitle');
const bodyEl = document.getElementById('forkliftCheckBody');
if (!this.isApplicable(category) || !weightKg || weightKg <= 0) {
box.style.display = 'none'; box.className = 'forklift-check'; return true;
}
const rules = this.getRules(), rule = rules.rules[0], limit = rules.hard_limit;
const msgs = rule.messages.en, limMsg = limit.messages.en, req = rule.requirements;
box.style.display = 'block';
if (weightKg > limit.max_weight_kg) {
box.className = 'forklift-check fc-block';
titleEl.innerHTML = '&#128683; ' + limMsg.block_title;
bodyEl.innerHTML = '<p>' + limMsg.block_text
.replace('{weight}', weightKg.toFixed(1))
.replace('2750', limit.max_weight_kg) + '</p>';
return false;
}
if (weightKg >= rule.weight_threshold_kg) {
const coreOk = coreMm >= req.min_core_outer_diameter_mm;
box.className = 'forklift-check fc-warn';
titleEl.innerHTML = '&#9888;&#65039; ' + msgs.warning_title;
const ci = coreOk
? '<li class="fc-req-ok">&#9989; ' + msgs.core_ok + '</li>'
: '<li class="fc-req-nok">&#10060; ' + msgs.core_nok.replace('{core}', coreMm) + '</li>';
bodyEl.innerHTML = '<div>' + msgs.requirements_label + '</div><ul>' + ci +
'<li>&#128295; ' + msgs.req_mandrel + '</li>' +
'<li>&#128668; ' + msgs.req_forklift + '</li></ul>';
return true;
}
box.className = 'forklift-check fc-ok';
titleEl.innerHTML = '&#9989; Forklift Check &ndash; OK';
bodyEl.innerHTML = 'Roll weight <strong>' + weightKg.toFixed(1) +
' kg</strong> is within the standard limit (&lt;' + rule.weight_threshold_kg +
' kg). No special equipment required.';
return true;
},
hide() {
const b = document.getElementById('forkliftCheck');
if (b) { b.style.display = 'none'; b.className = 'forklift-check'; }
}
};
window.ForkliftChecker = ForkliftChecker;
</script>
<!-- ============================================================
ROLL CALCULATOR LOGIC
============================================================ -->
<script>
// --- Article dropdown population ---
function populateArticleDropdowns() {
const data = window.ARTICLE_DATA || [];
// Datalists befuellen
['d-art','e-art'].forEach(function(inputId) {
var dl = document.getElementById(inputId + '-list');
if (!dl) return;
data.forEach(function(a) {
var opt = document.createElement('option');
opt.value = a.nr + ' \u2013 ' + a.name;
dl.appendChild(opt);
});
});
// Auto-fill Dicke bei Artikelauswahl (d-art)
var dArt = document.getElementById('d-art');
if (dArt) {
dArt.addEventListener('input', function() {
var val = dArt.value;
var match = data.find(function(a) { return val.startsWith(a.nr + ' \u2013') || val === a.nr; });
if (match) {
document.getElementById('d-th').value = match.thickness;
}
});
}
// Kern-Preset Buttons
document.querySelectorAll('.core-preset-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var val = btn.dataset.core;
var pane = btn.closest('.tab-pane');
if (pane) {
var inp = pane.querySelector('input[id$="-co"]');
if (inp) {
inp.value = val;
pane.querySelectorAll('.core-preset-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
}
}
});
});
}
// --- Tab switching ---
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
});
});
// --- Mode switching (Direct Calc) ---
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// --- DIRECT CALCULATION ---
document.getElementById('d-calc').addEventListener('click', () => {
const mode = document.querySelector('.mode-btn.active')?.dataset.mode || 'diameter';
const d_mm = parseFloat(document.getElementById('d-co').value) || 0;
const t_mm = parseFloat(document.getElementById('d-th').value) || 0;
const tol = parseFloat(document.getElementById('d-tol').value) || 0;
const L_m = parseFloat(document.getElementById('d-le').value) || 0;
const D_mm = parseFloat(document.getElementById('d-di').value) || 0;
const resBox = document.getElementById('d-result');
const resLabel = document.getElementById('d-res-label');
const resVal = document.getElementById('d-res-val');
const resRange = document.getElementById('d-res-range');
const category = (document.getElementById('d-cat')?.value || '').toLowerCase();
function calcWeight(length) {
const wi = parseFloat(document.getElementById('de-wi').value) || 0;
const aw = parseFloat(document.getElementById('d-aw').value) || 0;
if (wi > 0 && aw > 0 && length > 0) {
const w = (aw * length * wi / 1000).toFixed(1);
document.getElementById('d-wo').value = w;
return parseFloat(w);
}
document.getElementById('d-wo').value = '';
return null;
}
function runForkliftCheck(weight) {
const core = parseFloat(document.getElementById('d-co').value) || 0;
if (window.ForkliftChecker) {
console.log('[Forklift] Evaluating:', {weight, core, category, configReady: window.CONFIG_READY, configExists: !!window.APP_CONFIG});
return window.ForkliftChecker.evaluate(weight || 0, core, category);
}
console.warn('[Forklift] ForkliftChecker not available');
return true;
}
let calculatedLength = null;
if (mode === 'diameter') {
if (!d_mm || !t_mm || !L_m) { alert('Please enter d, t and L.'); return; }
const D_nom = Math.sqrt(d_mm*d_mm + (4*L_m*1000*t_mm)/Math.PI);
resLabel.textContent = 'Roll Diameter (D)';
resVal.textContent = D_nom.toFixed(1) + ' mm';
if (tol) {
const D_min = Math.sqrt(d_mm*d_mm + (4*L_m*1000*(t_mm-tol))/Math.PI);
const D_max = Math.sqrt(d_mm*d_mm + (4*L_m*1000*(t_mm+tol))/Math.PI);
resRange.textContent = 'Min: ' + D_min.toFixed(1) + ' mm | Max: ' + D_max.toFixed(1) + ' mm';
} else { resRange.textContent = ''; }
document.getElementById('d-di').value = D_nom.toFixed(1);
calculatedLength = L_m;
} else if (mode === 'length') {
if (!d_mm || !t_mm || !D_mm) { alert('Please enter d, t and D.'); return; }
const L = (Math.PI / (4 * t_mm)) * (D_mm*D_mm - d_mm*d_mm) / 1000;
resLabel.textContent = 'Product Length (L)';
resVal.textContent = L.toFixed(2) + ' m';
if (tol) {
const L_min = (Math.PI / (4*(t_mm+tol))) * (D_mm*D_mm - d_mm*d_mm) / 1000;
const L_max = (Math.PI / (4*(t_mm-tol))) * (D_mm*D_mm - d_mm*d_mm) / 1000;
resRange.textContent = 'Min: ' + L_min.toFixed(2) + ' m | Max: ' + L_max.toFixed(2) + ' m';
} else { resRange.textContent = ''; }
document.getElementById('d-le').value = L.toFixed(2);
calculatedLength = L;
} else if (mode === 'target') {
if (!d_mm || !t_mm) { alert('Please enter d and t.'); return; }
const D_target = D_mm || 800;
const L = (Math.PI / (4 * t_mm)) * (D_target*D_target - d_mm*d_mm) / 1000;
resLabel.textContent = 'Length for Target Diameter ' + D_target + ' mm';
resVal.textContent = L.toFixed(2) + ' m';
resRange.textContent = '';
document.getElementById('d-le').value = L.toFixed(2);
calculatedLength = L;
} else if (mode === 'thickness') {
if (!d_mm || !D_mm || !L_m) { alert('Please enter d, D and L.'); return; }
const t = (Math.PI / (4 * L_m * 1000)) * (D_mm*D_mm - d_mm*d_mm);
resLabel.textContent = 'Product Thickness (t)';
resVal.textContent = t.toFixed(3) + ' mm';
resRange.textContent = '';
document.getElementById('d-th').value = t.toFixed(3);
calculatedLength = L_m;
}
const weight = calcWeight(calculatedLength);
const allowed = runForkliftCheck(weight);
resBox.style.display = allowed ? 'block' : 'none';
});
// --- DIRECT RESET ---
document.getElementById('d-reset').addEventListener('click', () => {
['d-co','d-th','d-tol','d-le','d-di','de-wi','d-aw','d-wo'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = id === 'd-co' ? '150' : '';
});
document.getElementById('d-art').value = '';
document.getElementById('d-result').style.display = 'none';
if (window.ForkliftChecker) window.ForkliftChecker.hide();
});
// --- EXTRAPOLATION CALCULATION ---
document.getElementById('e-calc').addEventListener('click', () => {
const d = parseFloat(document.getElementById('e-co').value) || 0;
const D0 = parseFloat(document.getElementById('e-d0').value) || 0;
const L0 = parseFloat(document.getElementById('e-l0').value) || 0;
const L1 = parseFloat(document.getElementById('e-l1').value) || 0;
const tol= parseFloat(document.getElementById('e-tol').value)|| 0;
if (!d || !D0 || !L0 || !L1) { alert('Please enter d, D\u2080, L\u2080 and L\u2081.'); return; }
const D1 = Math.sqrt(d*d + (L1/L0) * (D0*D0 - d*d));
document.getElementById('e-res-val').textContent = D1.toFixed(1) + ' mm';
if (tol) {
const D1_min = D1 - tol, D1_max = D1 + tol;
document.getElementById('e-res-range').textContent =
'Min: ' + D1_min.toFixed(1) + ' mm | Max: ' + D1_max.toFixed(1) + ' mm';
} else {
document.getElementById('e-res-range').textContent = '';
}
document.getElementById('e-result').style.display = 'block';
});
// --- EXTRAPOLATION RESET ---
document.getElementById('e-reset').addEventListener('click', () => {
['e-co','e-d0','e-l0','e-l1','e-tol','e-wi','e-aw'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = id === 'e-co' ? '150' : '';
});
document.getElementById('e-art').selectedIndex = 0;
document.getElementById('e-result').style.display = 'none';
});
// --- DISCLAIMER ---
document.getElementById('confirmCheck').addEventListener('change', function() {
document.getElementById('btnAccept').disabled = !this.checked;
});
document.getElementById('btnAccept').addEventListener('click', () => {
document.getElementById('disclaimerOverlay').style.display = 'none';
});
document.getElementById('btnCancel').addEventListener('click', () => {
window.location.href = 'about:blank';
});
// --- INIT ---
document.addEventListener('DOMContentLoaded', () => {
if (window.ARTICLE_DATA && window.ARTICLE_DATA.length > 0) {
populateArticleDropdowns();
}
window.loadOptimizerUI = new LoadOptimizerUI();
});
</script>
<!-- ============================================================
SERVICE WORKER REGISTRATION
============================================================ -->
<!-- Offline Indicator -->
<div id="offlineIndicator" style="display:none; position:fixed; top:0; left:0; right:0; background:#ff6600; color:#fff; padding:10px; text-align:center; font-weight:bold; z-index:10000;">
&#128225; You are offline - using cached data
</div>
<script>
window.addEventListener('offline', () => {
document.getElementById('offlineIndicator').style.display = 'block';
});
window.addEventListener('online', () => {
document.getElementById('offlineIndicator').style.display = 'none';
});
if (!navigator.onLine) {
document.getElementById('offlineIndicator').style.display = 'block';
}
</script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/static/service-worker.js')
.then(reg => { console.log('[App] ServiceWorker registered:', reg.scope); })
.catch(err => { console.warn('[App] ServiceWorker registration failed:', err); });
});
}
// Auto-calculate weight when article is selected with area_weight
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
const origD_art = document.getElementById('d-art');
if (origD_art) {
origD_art.addEventListener('input', function() {
// Berechne Gewicht automatisch wenn alle Felder gefüllt sind
setTimeout(function() {
var wi = parseFloat(document.getElementById('de-wi').value) || 0;
var aw = parseFloat(document.getElementById('d-aw').value) || 0;
var L = parseFloat(document.getElementById('d-le').value) || 0;
if (wi > 0 && aw > 0 && L > 0) {
var w = (aw * L * wi / 1000).toFixed(1);
document.getElementById('d-wo').value = w;
console.log('[AutoCalc] Gewicht: ' + w + ' kg');
}
}, 100);
});
}
}, 500);
});
</script>
</body>
</html>