Files
RollCalcPython/templates/roll_calculator.html
T

1693 lines
69 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="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Roll Diameter Calculator - Internal Naue Tool">
<title>Roll Diameter Calculator</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f4f6f9; color: #333; min-height: 100vh; }
.header { background: linear-gradient(135deg, #1F5438 0%, #2BAC70 100%); color: #fff; padding: 20px 32px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); }
.header h1 { font-size: 24px; margin-bottom: 5px; }
.header p { font-size: 13px; opacity: 0.9; }
.container { max-width: 900px; margin: 30px auto; padding: 0 16px 60px; }
.card { background: #fff; border-radius: 10px; box-shadow: 0 2px 12px rgba(0,0,0,0.07); margin-bottom: 24px; overflow: hidden; }
.card-header { background: #f0f4fa; padding: 16px 22px; border-bottom: 1px solid #e0e8f0; }
.card-header h2 { font-size: 16px; color: #1F5438; font-weight: 700; }
.card-body { padding: 22px; }
.tabs { display: flex; border-bottom: 2px solid #e0e8f0; margin-bottom: 20px; }
.tab-btn { padding: 10px 20px; border: none; background: none; cursor: pointer; font-size: 14px; color: #666; font-weight: 500; border-bottom: 3px solid transparent; transition: all 0.2s; }
.tab-btn.active { color: #2BAC70; border-bottom-color: #2BAC70; font-weight: 700; }
.tab-pane { display: none; }
.tab-pane.active { display: block; }
.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: 6px; text-transform: uppercase; }
.form-group input, .form-group select { padding: 9px 11px; border: 1px solid #ccc; border-radius: 5px; font-size: 14px; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: #2BAC70; box-shadow: 0 0 0 3px rgba(43,172,112,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; }
.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; }
.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 #2BAC70; background: #fff; color: #2BAC70; border-radius: 4px; cursor: pointer; transition: all 0.15s; }
.core-preset-btn:hover, .core-preset-btn.active { background: #2BAC70; color: #fff; }
.mode-selector { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
.mode-btn { padding: 7px 16px; border: 2px solid #2BAC70; background: #fff; color: #2BAC70; border-radius: 5px; cursor: pointer; font-size: 13px; font-weight: 600; transition: all 0.2s; }
.mode-btn.active { background: #2BAC70; color: #fff; }
.btn-calc { background: #2BAC70; color: #fff; border: none; border-radius: 6px; padding: 11px 28px; font-size: 14px; font-weight: 700; cursor: pointer; transition: background 0.2s; }
.btn-calc:hover { background: #1F5438; }
.btn-reset { background: #e0e0e0; color: #444; border: none; border-radius: 6px; padding: 11px 20px; font-size: 14px; cursor: pointer; margin-left: 10px; }
.btn-reset:hover { background: #d0d0d0; }
.btn-pdf { background: #1F5438; color: #fff; border: none; border-radius: 5px; padding: 9px 16px; font-size: 12px; font-weight: 700; cursor: pointer; margin-top: 13px; }
.btn-pdf:hover:not(:disabled) { background: #153c28; }
.btn-pdf:disabled { cursor: not-allowed; opacity: 0.48; }
.result-box { background: linear-gradient(135deg, #D6EBE2 0%, #8CC9AB 100%); border: 2px solid #2BAC70; border-radius: 8px; padding: 16px 20px; margin-bottom: 18px; display: none; }
.result-box.show { display: block; }
.result-label { font-size: 12px; color: #1F5438; font-weight: 600; text-transform: uppercase; }
.result-value { font-size: 28px; font-weight: 700; color: #1F5438; line-height: 1.2; }
.result-range { font-size: 12px; color: #1F5438; margin-top: 4px; font-weight: 500; }
.result-range div { margin-top: 2px; }
.result-range .range-warning { color: #9a6700; font-weight: 700; }
.result-range .range-ok { color: #1F5438; font-weight: 700; }
.forklift-check { border-radius: 7px; padding: 14px 16px; margin-top: 14px; font-size: 13px; line-height: 1.6; display: none; }
.forklift-check.show { display: block; }
.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; margin-bottom: 8px; }
.forklift-check ul { margin: 6px 0 0 18px; }
.forklift-check li { margin-bottom: 3px; }
.fc-req-ok { color: #28a745; }
.fc-req-nok { color: #dc3545; font-weight: 600; }
.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; }
.footer { text-align: center; font-size: 11px; color: #aaa; padding: 18px; margin-top: 30px; }
.footer-build-info { margin-top: 6px; color: #bbb; }
.disclaimer-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(20, 28, 34, 0.72);
}
.disclaimer-overlay.is-hidden { display: none; }
.disclaimer-dialog {
width: min(760px, 100%);
max-height: min(86vh, 820px);
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 18px 45px rgba(0, 0, 0, 0.28);
overflow: hidden;
}
.disclaimer-header {
padding: 18px 22px;
background: #1F5438;
color: #fff;
}
.disclaimer-header h2 { font-size: 18px; margin: 0; }
.disclaimer-content {
padding: 20px 22px;
overflow-y: auto;
line-height: 1.5;
font-size: 14px;
}
.disclaimer-content h3 {
margin: 0 0 8px;
color: #1F5438;
font-size: 15px;
}
.disclaimer-content p { margin: 0 0 12px; }
.disclaimer-language + .disclaimer-language {
margin-top: 18px;
padding-top: 18px;
border-top: 1px solid #e0e8f0;
}
.disclaimer-actions {
padding: 16px 22px;
border-top: 1px solid #e0e8f0;
background: #f8fafa;
}
.disclaimer-confirm {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 14px;
font-size: 13px;
color: #333;
}
.disclaimer-confirm input {
width: 18px;
height: 18px;
margin-top: 1px;
flex: 0 0 auto;
}
.disclaimer-btn {
width: 100%;
border: none;
border-radius: 6px;
padding: 11px 18px;
background: #2BAC70;
color: #fff;
font-size: 14px;
font-weight: 700;
cursor: pointer;
}
.disclaimer-btn:hover { background: #1F5438; }
.disclaimer-btn:disabled {
background: #b8c7bf;
cursor: not-allowed;
}
/* ============================================
LOAD OPTIMIZER STYLES
============================================ */
.load-optimizer-section {
margin-top: 30px;
border: 2px solid #2BAC70;
border-radius: 8px;
background-color: #f0f7ff;
overflow: hidden;
box-shadow: 0 2px 8px rgba(43,172,112,0.1);
}
.lo-accordion-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: linear-gradient(135deg, #2BAC70 0%, #1F5438 100%);
color: white;
cursor: pointer;
user-select: none;
transition: background 0.3s ease;
}
.lo-accordion-header:hover {
background: linear-gradient(135deg, #1F5438 0%, #1F5438 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;
display: none;
}
.lo-accordion-content.active {
display: block;
animation: loSlideDown 0.3s ease;
}
@keyframes loSlideDown {
from { opacity: 0; }
to { opacity: 1; }
}
.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 #2BAC70;
background: white;
color: #2BAC70;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}
.lo-preset-btn:hover { background: #2BAC70; color: white; }
.lo-preset-btn.active {
background: #2BAC70;
color: white;
box-shadow: 0 0 0 3px rgba(43,172,112,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: #2BAC70;
box-shadow: 0 0 0 2px rgba(43,172,112,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: #2BAC70; color: white; }
.lo-btn-primary:hover { background: #1F5438; box-shadow: 0 2px 6px rgba(43,172,112,0.3); }
.lo-btn-secondary { background: #e0e0e0; color: #333; }
.lo-btn-secondary:hover { background: #d0d0d0; }
.lo-results { display: grid; gap: 20px; display: none; }
.lo-results.show { display: grid; }
.lo-results-summary {
background: white;
padding: 16px;
border-radius: 6px;
border-left: 4px solid #2BAC70;
}
.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: #2BAC70; font-weight: bold; font-size: 16px; }
.lo-value-number { color: #1F5438; 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 #2BAC70;
}
.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;
}
.lo-settings-section h4 {
margin-top: 0;
color: #333;
font-size: 14px;
margin-bottom: 4px;
}
.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;
}
.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: #2BAC70;
box-shadow: 0 0 0 2px rgba(43,172,112,0.1);
}
.lo-settings-input-wrap .lo-unit {
font-size: 12px;
color: #888;
}
.lo-settings-effective {
font-size: 11px;
color: #2BAC70;
margin-top: 5px;
min-height: 16px;
}
/* ===== SIDE VIEW ===== */
.lo-sideview-section {
background: white; padding: 16px; border-radius: 6px; border: 1px solid #ddd; display: none;
}
.lo-sideview-section.show { display: block; }
.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:#2BAC70; }
.container-color { background:#e8e8e8; border-color:#999; }
.margin-color { background: repeating-linear-gradient(45deg,#fff7e0,#fff7e0 3px,#ffe8a0 3px,#ffe8a0 6px); border-color:#f0b429; }
@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; }
}
</style>
</head>
<body>
<div class="disclaimer-overlay" id="disclaimerOverlay" role="dialog" aria-modal="true" aria-labelledby="disclaimerTitle">
<div class="disclaimer-dialog">
<div class="disclaimer-header">
<h2 id="disclaimerTitle">Disclaimer</h2>
</div>
<div class="disclaimer-content">
<section class="disclaimer-language" aria-label="Deutscher Disclaimer">
<h3>Deutsch</h3>
<p><strong>⚠️ Disclaimer</strong></p>
<p>Dieses Tool ist ausschließlich zur persönlichen, internen Verwendung bestimmt. Die Weitergabe von Logins ist nicht gestattet.</p>
<p>Berechtigt sind nur: Produktmanagement, Produktionsleitung, R&amp;D, Controlling und Logistik.</p>
<p>Wichtig: Alle Eingaben und Ausgaben müssen vor der Verwendung auf Plausibilität geprüft werden. Die Datenbasis für Materialdicke und Flächengewicht wird automatisch erstellt und ist weder geprüft noch erhebt sie Anspruch auf Vollständigkeit.</p>
</section>
<section class="disclaimer-language" aria-label="English disclaimer">
<h3>English</h3>
<p><strong>⚠️ Disclaimer</strong></p>
<p>This tool is exclusively for personal, internal use. Sharing login credentials is prohibited.</p>
<p>Authorized departments only: Product Management, Production Management, R&amp;D, Controlling, and Logistics.</p>
<p>Important: All inputs and outputs must be verified for plausibility before use. The material thickness and area weight data is automatically generated and has not been reviewed. It does not claim to be complete or comprehensive.</p>
</section>
</div>
<div class="disclaimer-actions">
<label class="disclaimer-confirm" for="disclaimerAccepted">
<input type="checkbox" id="disclaimerAccepted">
<span>I have read and understood the disclaimer / Ich habe den Disclaimer gelesen und verstanden.</span>
</label>
<button class="disclaimer-btn" id="disclaimerConfirmBtn" type="button" disabled>Confirm / Bestätigen</button>
</div>
</div>
</div>
<div class="header">
<h1>Naue Roll Diameter Calculator</h1>
<p>Roll Diameter • Product Length • Roll Weight • Load Optimizer</p>
</div>
<div class="container">
<div class="card">
<div class="card-header">
<h2>📝 Roll Diameter Calculator</h2>
</div>
<div class="card-body">
<div class="tabs">
<button class="tab-btn active" data-tab="direct">Direct Calculation</button>
<button class="tab-btn" data-tab="extrap">Extrapolation</button>
</div>
<!-- DIRECT CALCULATION TAB -->
<div class="tab-pane active" id="tab-direct">
<div class="form-row">
<div class="form-group">
<label>Product Category</label>
<select id="d-cat">
<option value="">– Select –</option>
<option value="bentofix">Bentofix</option>
<option value="secutex">Secutex</option>
<option value="carbofol">Carbofol</option>
</select>
</div>
<div class="form-group">
<label>Production Site</label>
<select id="d-site">
<option value="">– Select –</option>
<option value="espelkamp">Espelkamp</option>
<option value="malaysia">Malaysia</option>
</select>
</div>
</div>
<div class="form-group" style="margin-bottom: 14px;">
<label>Calculation Mode</label>
<div class="mode-selector">
<button class="mode-btn active" data-mode="diameter">→ Roll Diameter</button>
<button class="mode-btn" data-mode="length">→ Product Length</button>
<button class="mode-btn" data-mode="target">🎯 Target Diameter</button>
</div>
</div>
<h3 style="font-size: 13px; color: #555; margin: 18px 0 14px;">Input Parameters</h3>
<div class="form-row">
<div class="form-group">
<label>Core 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">
{% for preset in core_presets %}
<button class="core-preset-btn" data-core="{{ preset.ui_diameter }}" data-core-material="{{ preset.material }}">{{ preset.label }}</button>
{% endfor %}
</div>
</div>
<div class="form-group">
<label>Article Number (optional)</label>
<input type="text" id="d-art" list="d-art-list" placeholder="Search article..." autocomplete="off">
<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">±</span>
<input type="number" id="d-tol" placeholder="mm" min="0" step="0.01">
<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>
<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="d-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²" min="0" step="1">
<span class="unit-label">g/m²</span>
</div>
</div>
<div class="form-group">
<label>Calculated Weight</label>
<div class="input-with-unit">
<input type="text" id="d-wo" readonly placeholder="–" style="background: #f0f0f0;">
<span class="unit-label">kg</span>
</div>
</div>
<div class="form-group">
<label>Max. Roll Diameter</label>
<div class="input-with-unit">
<input type="number" id="d-target-di" placeholder="mm" min="0" step="1">
<span class="unit-label">mm</span>
</div>
</div>
<div class="form-group">
<label>Max. Roll Weight</label>
<div class="input-with-unit">
<input type="number" id="d-max-weight" placeholder="kg" min="0" step="1">
<span class="unit-label">kg</span>
</div>
</div>
<div class="form-group">
<label>Minimum Product Length</label>
<div class="input-with-unit">
<input type="number" id="d-min-le" placeholder="m" min="0" step="0.1">
<span class="unit-label">m</span>
</div>
</div>
</div>
</div>
<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">▶ Calculate</button>
<button class="btn-reset" id="d-reset">Reset</button>
</div>
<div class="result-box" id="d-result">
<div class="result-label" id="d-res-label">Result</div>
<div class="result-value" id="d-res-val">–</div>
<div class="result-range" id="d-res-range"></div>
<button class="btn-pdf" id="d-pdf" type="button" disabled>Download PDF</button>
</div>
<div class="formula-box">
Geometry: π/4 × (D² − d²) = L × t | Weight: G [kg] = Area Weight [g/m²] × L [m] × Width [m] / 1000
</div>
</div>
<!-- EXTRAPOLATION TAB -->
<div class="tab-pane" id="tab-extrap">
<h3 style="font-size: 13px; color: #555; margin-bottom: 14px;">Known Reference Roll</h3>
<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..." autocomplete="off">
<datalist id="e-art-list"></datalist>
</div>
<div class="form-group">
<label>Core 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>
</div>
<div class="form-row">
<div class="form-group">
<label>Known Roll Diameter (D₀)</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₀)</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>
<h3 style="font-size: 13px; color: #555; margin: 18px 0 14px;">New Roll Length</h3>
<div class="form-row">
<div class="form-group">
<label>New Roll Length (L₁)</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">±</span>
<input type="number" id="e-tol" placeholder="mm" min="0" step="0.1">
<span class="unit-label">mm</span>
</div>
</div>
</div>
<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²" min="0" step="1">
<span class="unit-label">g/m²</span>
</div>
</div>
</div>
</div>
<div style="margin-top: 18px;">
<button class="btn-calc" id="e-calc">▶ Calculate Roll Diameter</button>
<button class="btn-reset" id="e-reset">Reset</button>
</div>
<div class="result-box" id="e-result">
<div class="result-label">Extrapolated Roll Diameter (D₁)</div>
<div class="result-value" id="e-res-val">–</div>
<div class="result-range" id="e-res-range"></div>
</div>
<div class="formula-box">
Geometry: D₁ = √(d² + L₁/L₀ × (D₀² − d²)) | Weight: G [kg] = Area Weight × L₁ × Width / 1000
</div>
</div>
</div>
</div>
<!-- ============================================================
LOAD OPTIMIZER SECTION
============================================================ -->
<div id="loadOptimizerContainer" class="load-optimizer-section">
<div class="lo-accordion-header" id="loAccordionToggle">
<span class="lo-accordion-icon">▼</span>
<h3>🎯 Load Optimizer – Transport Capacity Calculator</h3>
<span class="badge-beta-lo">BETA</span>
</div>
<div class="lo-accordion-content" id="loAccordionContent">
<div class="lo-warning">
⚠ 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>⚙️ 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 (one side)</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 (top only)</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>🚚 Preset Transport Type:</h4>
<div class="lo-preset-buttons">
{% for preset in transport_presets %}
<button class="lo-preset-btn" data-preset="{{ preset.key }}" data-length="{{ preset.length_m }}" data-width="{{ preset.width_m }}" data-height="{{ preset.height_m }}" data-max-weight="{{ preset.max_weight_kg }}">{{ preset.name }}</button>
{% endfor %}
<button class="lo-preset-btn" data-preset="custom">Custom</button>
</div>
</div>
<div class="lo-custom-section">
<h4>📦 Dimensions & 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">▶ Calculate</button>
<button class="lo-btn-secondary" id="resetLoad">↻ Reset</button>
</div>
</div>
<div class="lo-results" id="resultsContainer">
<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">–</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">–</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">–</span> %</div>
<div>Volume: <span id="volumeUtilValue" class="lo-value-number">–</span> %</div>
<div>Product: <span id="productM2Value" class="lo-value-number">–</span> m²</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²</th>
<th>Util. %</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="lo-sideview-section" id="sideViewSection">
<h4>🔍 Front View – 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, ⌀ = 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>
<div class="footer">
Naue GmbH & Co. KG · Roll Diameter Calculator · Internal Application
<div class="footer-build-info">
Version {{ build_info.version }} · Branch {{ build_info.branch }} · Commit {{ build_info.commit }} · Built {{ build_info.timestamp }}
</div>
</div>
<script>
// =====================================================
// GLOBAL STATE
// =====================================================
window.ARTICLE_DATA = [];
window.APP_CONFIG = {"forklift_rules":{"applicable_categories":["bentofix"],"rules":[{"weight_threshold_kg":1700,"max_weight_kg":2750,"requirements":{"min_core_outer_diameter_mm":170},"messages":{"en":{"warning_title":"Heavy Roll – Special Equipment Required","core_ok":"Core diameter OK (≥ 170 mm)","core_nok":"Core diameter too small"}}}],"hard_limit":{"max_weight_kg":2750,"messages":{"en":{"block_title":"Weight Limit Exceeded","block_text":"Roll weight exceeds 2750 kg maximum"}}}}};
window.CURRENT_ROLL_REQUEST = null;
window.CURRENT_ROLL_RESULT = null;
// =====================================================
// LOAD ARTICLE DATA
// =====================================================
function populateArticleDropdowns() {
const data = window.ARTICLE_DATA || [];
['d-art', 'e-art'].forEach(function(inputId) {
const dl = document.getElementById(inputId + '-list');
if (!dl) return;
data.forEach(function(a) {
const opt = document.createElement('option');
opt.value = a.nr + ' – ' + a.name;
dl.appendChild(opt);
});
});
const dArt = document.getElementById('d-art');
if (dArt) {
dArt.addEventListener('input', function() {
const val = dArt.value;
const match = data.find(a => val.startsWith(a.nr + ' –') || val === a.nr);
if (match) {
if (match.thickness) document.getElementById('d-th').value = match.thickness;
if (match.thickness_stddev && match.thickness_stddev > 0) document.getElementById('d-tol').value = match.thickness_stddev;
if (match.area_weight && match.area_weight > 0) document.getElementById('d-aw').value = match.area_weight;
if (match.core_type && match.core_type > 0) document.getElementById('d-co').value = match.core_type;
}
});
}
const eArt = document.getElementById('e-art');
if (eArt) {
eArt.addEventListener('input', function() {
const val = eArt.value;
const match = data.find(a => val.startsWith(a.nr + ' –') || val === a.nr);
if (match) {
if (match.thickness) document.getElementById('e-th').value = match.thickness;
if (match.thickness_stddev && match.thickness_stddev > 0) document.getElementById('e-tol').value = match.thickness_stddev;
if (match.area_weight && match.area_weight > 0) document.getElementById('e-aw').value = match.area_weight;
if (match.core_type && match.core_type > 0) document.getElementById('e-co').value = match.core_type;
}
});
}
document.querySelectorAll('.core-preset-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const val = btn.dataset.core;
const pane = btn.closest('.tab-pane');
if (pane) {
const inp = pane.querySelector('input[id$="-co"]');
if (inp) {
inp.value = val;
pane.querySelectorAll('.core-preset-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
if (pane.id === 'tab-direct') invalidatePdfReport();
}
}
});
});
}
(async function() {
try {
const resp = await fetch('/static/article-data.json');
if (resp.ok) {
window.ARTICLE_DATA = await resp.json();
console.log('✅ Article data loaded:', window.ARTICLE_DATA.length, 'items');
populateArticleDropdowns();
} else {
console.error('❌ Failed to load article data:', resp.status);
}
} catch (e) {
console.error('❌ Error loading data:', e);
}
})();
// =====================================================
// 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
// =====================================================
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 (WITH 2σ)
// =====================================================
function currentArticle() {
const rawValue = document.getElementById('d-art').value.trim();
if (!rawValue) return null;
const match = (window.ARTICLE_DATA || []).find(article =>
rawValue === article.nr || rawValue.startsWith(article.nr + ' –')
);
if (match) return { number: String(match.nr), name: match.name || '' };
const separator = rawValue.indexOf(' – ');
if (separator >= 0) {
return { number: rawValue.slice(0, separator), name: rawValue.slice(separator + 3) };
}
return { number: rawValue, name: '' };
}
function currentCoreType(coreDiameter) {
const activePreset = document.querySelector('#tab-direct .core-preset-btn.active');
if (activePreset && Number(activePreset.dataset.core) === coreDiameter) return activePreset.textContent.trim();
return 'Custom / not specified';
}
function invalidatePdfReport() {
window.CURRENT_ROLL_REQUEST = null;
window.CURRENT_ROLL_RESULT = null;
const button = document.getElementById('d-pdf');
button.disabled = true;
button.textContent = 'Download PDF';
}
function numericInput(id) {
const raw = document.getElementById(id).value.trim();
return raw === '' ? null : Number(raw);
}
function buildCalculationRequest() {
const article = currentArticle();
const width = numericInput('d-wi');
const areaWeight = numericInput('d-aw');
return {
intent: 'calculate_roll_diameter',
article_number: article?.number || null,
article_name_hint: article?.name || null,
roll_length_m: numericInput('d-le'),
width_m: width,
thickness_mm: numericInput('d-th'),
thickness_stddev_mm: numericInput('d-tol'),
area_weight_g_m2: areaWeight,
core_diameter_mm: numericInput('d-co'),
core_type: currentCoreType(numericInput('d-co')),
category: document.getElementById('d-cat').value || null,
production_site: document.getElementById('d-site').value || null,
include_roll_weight: width !== null && areaWeight !== null
};
}
function calculationIssueMessage(result) {
if (result.status === 'article_not_found') return 'Article not found.';
if (result.status === 'article_conflict') return 'Article number and name conflict.';
if (result.status === 'needs_clarification') {
const fields = [...(result.missing || []), ...(result.ambiguous || [])];
return 'Please clarify: ' + fields.join(', ');
}
const issue = result.invalid?.[0]?.message;
return issue ? 'Invalid calculation request: ' + issue : 'Calculation failed.';
}
function renderAuthoritativeForkliftResult(result) {
const box = document.getElementById('forkliftCheck');
const titleEl = document.getElementById('forkliftCheckTitle');
const bodyEl = document.getElementById('forkliftCheckBody');
const warning = result.warnings?.[0];
const note = result.notes?.[0];
if (warning) {
box.className = 'forklift-check ' + (warning.severity === 'critical' ? 'fc-block' : 'fc-warn') + ' show';
titleEl.textContent = '⚠️ ' + warning.title;
bodyEl.textContent = warning.message;
} else if (note) {
box.className = 'forklift-check fc-ok show';
titleEl.textContent = '✓ Forklift Check OK';
bodyEl.textContent = note;
} else {
box.classList.remove('show');
}
}
async function requestRollCalculation(calculationRequest) {
const response = await fetch('/api/calculations/roll', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(calculationRequest)
});
if (!response.ok) throw new Error('Calculation service failed');
return response.json();
}
async function requestProductLengthCalculation(calculationRequest) {
const response = await fetch('/api/calculations/product-length', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(calculationRequest)
});
if (!response.ok) throw new Error('Product-length calculation service failed');
return response.json();
}
document.getElementById('d-calc').addEventListener('click', async () => {
invalidatePdfReport();
const mode = document.querySelector('.mode-btn.active')?.dataset.mode || 'diameter';
const d = parseFloat(document.getElementById('d-co').value) || 0;
const t = parseFloat(document.getElementById('d-th').value) || 0;
const tol = parseFloat(document.getElementById('d-tol').value) || 0;
const L = parseFloat(document.getElementById('d-le').value) || 0;
const D = parseFloat(document.getElementById('d-di').value) || 0;
const category = document.getElementById('d-cat')?.value || '';
const resultBox = 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');
let diameterResult = null;
let authoritativeResult = null;
let calculationRequest = null;
if (mode === 'diameter') {
calculationRequest = buildCalculationRequest();
try {
authoritativeResult = await requestRollCalculation(calculationRequest);
} catch (error) {
alert(error.message);
return;
}
if (authoritativeResult.status !== 'success') {
alert(calculationIssueMessage(authoritativeResult));
return;
}
const calculation = authoritativeResult.calculation;
diameterResult = {
minimum: calculation.minimum_diameter_mm,
average: calculation.average_diameter_mm,
maximum: calculation.maximum_diameter_mm,
minus2Sigma: calculation.minimum_diameter_mm,
plus2Sigma: calculation.maximum_diameter_mm,
rollLength: calculation.effective_roll_length_m
};
resLabel.textContent = 'Roll Diameter (D)';
resVal.textContent = diameterResult.average.toFixed(1) + ' mm';
if (tol) {
resRange.textContent = '−2σ: ' + diameterResult.minus2Sigma.toFixed(1) + ' mm | +2σ: ' + diameterResult.plus2Sigma.toFixed(1) + ' mm';
} else {
resRange.textContent = '';
}
document.getElementById('d-di').value = diameterResult.average.toFixed(1);
if (authoritativeResult.effective_inputs.width_m?.source === 'article_master_data') {
document.getElementById('d-wi').value = authoritativeResult.effective_inputs.width_m.value;
}
const authoritativeWeight = calculation.roll_weight_kg;
document.getElementById('d-wo').value = authoritativeWeight === null ? '' : authoritativeWeight.toFixed(1);
renderAuthoritativeForkliftResult(authoritativeResult);
} else if (mode === 'length') {
if (!d || !t || !D) { alert('Please enter d, t, and D.'); return; }
try {
authoritativeResult = await requestProductLengthCalculation({
target_roll_diameter_mm: D,
core_diameter_mm: d,
thickness_mm: t,
thickness_stddev_mm: tol,
width_m: numericInput('d-wi'),
area_weight_g_m2: numericInput('d-aw')
});
} catch (error) {
alert(error.message);
return;
}
if (authoritativeResult.status !== 'success') {
const issue = authoritativeResult.invalid?.[0]?.message;
alert(issue || 'Product-length calculation failed.');
return;
}
const calculation = authoritativeResult.calculation;
const L_calc = calculation.average_product_length_m;
resLabel.textContent = 'Product Length (L)';
resVal.textContent = L_calc.toFixed(2) + ' m';
if (tol) {
resRange.textContent = '−2σ: ' + calculation.minimum_product_length_m.toFixed(2) + ' m | +2σ: ' + calculation.maximum_product_length_m.toFixed(2) + ' m';
} else {
resRange.textContent = '';
}
document.getElementById('d-le').value = L_calc.toFixed(2);
const materialWeight = calculation.material_weights_kg?.average_kg;
document.getElementById('d-wo').value = materialWeight === undefined ? '' : materialWeight.toFixed(1);
} else if (mode === 'target') {
const wi = parseFloat(document.getElementById('d-wi').value) || 0;
const aw = parseFloat(document.getElementById('d-aw').value) || 0;
const maxWeight = parseFloat(document.getElementById('d-max-weight').value) || 0;
const minLength = parseFloat(document.getElementById('d-min-le').value) || 0;
const D_target = parseFloat(document.getElementById('d-target-di').value) || 0;
if (!d || !t || !D_target || !wi || !aw || !maxWeight) {
alert('Please enter d, t, max. roll diameter, roll width, area weight, and max. roll weight.');
return;
}
if (D_target <= d) {
alert('Max. roll diameter must be larger than core diameter.');
return;
}
const L_diameter = (Math.PI / (4 * t)) * (D_target*D_target - d*d) / 1000;
const L_weight = maxWeight * 1000 / (aw * wi);
const L_allowed = Math.min(L_diameter, L_weight);
const D_result = Math.sqrt(d*d + (4*L_allowed*1000*t)/Math.PI);
const W_result = aw * L_allowed * wi / 1000;
const diameterUtil = D_result / D_target * 100;
const weightUtil = W_result / maxWeight * 100;
const lengthDiff = minLength > 0 ? L_allowed - minLength : null;
const epsilon = 0.0001;
let limitingFactor = 'Diameter + Weight';
if (L_diameter < L_weight - epsilon) {
limitingFactor = 'Diameter';
} else if (L_weight < L_diameter - epsilon) {
limitingFactor = 'Weight';
}
resLabel.textContent = 'Maximum Allowed Product Length';
resVal.textContent = L_allowed.toFixed(2) + ' m';
const minLengthText = minLength > 0 ? minLength.toFixed(2) + ' m' : 'not set';
const diffText = lengthDiff !== null ? lengthDiff.toFixed(2) + ' m' : 'n/a';
let minStatus = '<div>Minimum length not set</div>';
if (minLength > 0 && L_allowed < minLength) {
minStatus = '<div class="range-warning">Warning: maximum allowed length is below the minimum product length.</div>';
} else if (minLength > 0) {
minStatus = '<div class="range-ok">Minimum length OK</div>';
}
resRange.innerHTML =
'<div>Resulting Roll Diameter: ' + D_result.toFixed(1) + ' mm</div>' +
'<div>Resulting weight: ' + W_result.toFixed(1) + ' kg</div>' +
'<div>Minimum length: ' + minLengthText + '</div>' +
'<div>Difference: ' + diffText + '</div>' +
'<div>Limiting factor: ' + limitingFactor + '</div>' +
'<div>Utilization: Diameter ' + diameterUtil.toFixed(1) + '% | Weight ' + weightUtil.toFixed(1) + '%</div>' +
minStatus;
document.getElementById('d-le').value = L_allowed.toFixed(2);
document.getElementById('d-wo').value = W_result.toFixed(1);
}
if (mode !== 'diameter') {
const wi = parseFloat(document.getElementById('d-wi').value) || 0;
const aw = parseFloat(document.getElementById('d-aw').value) || 0;
const L_final = parseFloat(document.getElementById('d-le').value) || 0;
if (wi > 0 && aw > 0 && L_final > 0) {
const rollWeight = aw * L_final * wi / 1000;
document.getElementById('d-wo').value = rollWeight.toFixed(1);
checkForklift(rollWeight, d, category);
} else {
document.getElementById('forkliftCheck').classList.remove('show');
}
}
if (mode === 'diameter' && authoritativeResult) {
window.CURRENT_ROLL_REQUEST = calculationRequest;
window.CURRENT_ROLL_RESULT = authoritativeResult;
document.getElementById('d-pdf').disabled = false;
}
resultBox.style.display = 'block';
});
document.getElementById('d-reset').addEventListener('click', () => {
['d-co', 'd-th', 'd-tol', 'd-le', 'd-di', 'd-wi', 'd-aw', 'd-wo', 'd-target-di', 'd-max-weight', 'd-min-le'].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';
document.getElementById('forkliftCheck').classList.remove('show');
invalidatePdfReport();
});
// =====================================================
// EXTRAPOLATION
// =====================================================
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 all values.'); 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) {
document.getElementById('e-res-range').textContent = 'Min: ' + (D1 - tol).toFixed(1) + ' mm | Max: ' + (D1 + tol).toFixed(1) + ' mm';
} else {
document.getElementById('e-res-range').textContent = '';
}
document.getElementById('e-result').style.display = 'block';
});
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').value = '';
document.getElementById('e-result').style.display = 'none';
});
// =====================================================
// FORKLIFT CHECK
// =====================================================
function checkForklift(weightKg, coreMm, category) {
const box = document.getElementById('forkliftCheck');
const titleEl = document.getElementById('forkliftCheckTitle');
const bodyEl = document.getElementById('forkliftCheckBody');
const cfg = window.APP_CONFIG?.forklift_rules;
if (!cfg || !cfg.applicable_categories.includes(category.toLowerCase())) {
box.classList.remove('show');
return null;
}
const rule = cfg.rules[0];
const limit = cfg.hard_limit;
if (weightKg > limit.max_weight_kg) {
const message = limit.messages.en.block_text.replace('{weight}', weightKg);
box.className = 'forklift-check fc-block show';
titleEl.textContent = '⚠️ ' + limit.messages.en.block_title;
bodyEl.innerHTML = '<p>' + message + '</p>';
return {
kind: 'warning',
severity: 'critical',
title: limit.messages.en.block_title,
message
};
}
if (weightKg >= rule.weight_threshold_kg) {
const coreOk = coreMm >= rule.requirements.min_core_outer_diameter_mm;
const coreMessage = coreOk
? 'Core diameter OK'
: 'Core diameter ' + coreMm + ' mm (min 170 mm required)';
const message = 'Special equipment required for rolls ≥ ' + rule.weight_threshold_kg + ' kg. ' + coreMessage + '.';
box.className = 'forklift-check fc-warn show';
titleEl.textContent = '⚠️ ' + rule.messages.en.warning_title;
bodyEl.innerHTML = '<p>' + message + '</p>';
return {
kind: 'warning',
severity: 'warning',
title: rule.messages.en.warning_title,
message
};
}
const message = 'Roll weight ' + weightKg.toFixed(1) + ' kg is within standard limits. No special equipment required.';
box.className = 'forklift-check fc-ok show';
titleEl.textContent = '✓ Forklift Check OK';
bodyEl.innerHTML = '<p>' + message + '</p>';
return { kind: 'note', message };
}
document.getElementById('tab-direct').addEventListener('input', event => {
if (event.target.id !== 'd-wo') invalidatePdfReport();
});
document.getElementById('tab-direct').addEventListener('change', invalidatePdfReport);
document.querySelectorAll('.mode-btn').forEach(button => button.addEventListener('click', invalidatePdfReport));
document.getElementById('d-pdf').addEventListener('click', async () => {
const button = document.getElementById('d-pdf');
if (!window.CURRENT_ROLL_REQUEST || button.disabled) return;
button.disabled = true;
button.textContent = 'Creating PDF…';
try {
const response = await fetch('/api/reports/roll-calculation.pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(window.CURRENT_ROLL_REQUEST)
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error || 'PDF generation failed');
}
const disposition = response.headers.get('Content-Disposition') || '';
const filenameMatch = disposition.match(/filename\*?=(?:UTF-8''|"?)([^";]+)/i);
const filename = filenameMatch ? decodeURIComponent(filenameMatch[1].replace(/"$/, '')) : 'roll-calculation.pdf';
const url = URL.createObjectURL(await response.blob());
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
} catch (error) {
alert(error.message);
} finally {
button.disabled = !window.CURRENT_ROLL_REQUEST;
button.textContent = 'Download PDF';
}
});
// =====================================================
// LOAD OPTIMIZER UI
// =====================================================
class LoadOptimizerUI {
constructor() {
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());
const updateEffective = () => this.updateEffectiveHints();
['loMarginSide','loMarginCeiling','customWidth','customHeight'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('input', updateEffective);
});
}
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 = '→ effective width: ' + eff + ' m';
} else {
sideEl.textContent = W > 0 ? '→ effective width: ' + W.toFixed(2) + ' m' : '';
}
if (H > 0 && ceilM > 0) {
const eff = (H - ceilM / 100).toFixed(2);
ceilEl.textContent = '→ effective height: ' + eff + ' m';
} else {
ceilEl.textContent = H > 0 ? '→ effective height: ' + H.toFixed(2) + ' m' : '';
}
}
toggleAccordion() {
const header = document.getElementById('loAccordionToggle');
const content = document.getElementById('loAccordionContent');
const isOpen = content.classList.contains('active');
content.classList.toggle('active');
header.classList.toggle('active');
}
loadPreset(id) {
if (id === 'custom') { this.clearPresets(); return; }
const button = document.querySelector('[data-preset="'+id+'"]');
if (!button) return;
document.getElementById('customLength').value = button.dataset.length;
document.getElementById('customWidth').value = button.dataset.width;
document.getElementById('customHeight').value = button.dataset.height;
document.getElementById('customMaxWeight').value = button.dataset.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; }
getMargins() {
const sideM = (parseFloat(document.getElementById('loMarginSide').value) || 0) / 100;
const ceilM = (parseFloat(document.getElementById('loMarginCeiling').value) || 0) / 100;
return { sideM, ceilM };
}
async 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();
let response;
try {
response = await fetch('/api/calculations/transport', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
transport_preset: this.currentPreset,
length_m: L,
width_m: W,
height_m: H,
max_weight_kg: MW,
margin_side_m: sideM,
margin_ceiling_m: ceilM,
roll_diameter_mm: rollData.rollDiameter,
core_diameter_mm: rollData.coreDiameter,
roll_width_m: rollData.rollWidth,
roll_weight_kg: rollData.rollWeight,
product_length_m: rollData.productLength
})
});
} catch (_error) {
alert('Transport calculation service failed');
return;
}
const payload = await response.json().catch(() => null);
if (!response.ok || !payload || payload.status !== 'success') {
alert(payload?.invalid?.[0]?.message || 'Transport calculation failed');
return;
}
this.showResults(payload.analysis);
this.drawSideView(payload.analysis);
}
getRollData() {
const D = parseFloat(document.getElementById('d-di')?.value || 0);
const W = parseFloat(document.getElementById('d-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, productLength:L };
}
showResults(analysis) {
document.getElementById('limitingFactorValue').textContent = analysis.limiting.name;
document.getElementById('totalRollsValue').textContent = analysis.final_rolls;
document.getElementById('rollsDetailValue').textContent =
'(' + analysis.layout.rolls_along_length + ' along length × ' + analysis.layout.rolls_along_width + ' across, ' + analysis.layout.max_stack_height + ' layers)';
document.getElementById('weightUtilValue').textContent = this.fmt(analysis.utilization_weight);
document.getElementById('volumeUtilValue').textContent = this.fmt(analysis.utilization_volume);
document.getElementById('productM2Value').textContent = this.fmt(analysis.final_m2, 0);
const tbody = document.querySelector('#comparisonTable tbody');
tbody.innerHTML = '';
analysis.scenarios.forEach(sc => {
const tr = document.createElement('tr');
const lim = (sc.type === analysis.limiting.type);
if (lim) tr.classList.add('lo-recommended');
const util = this.fmt(sc.utilization_weight);
const scM2 = sc.m2 === null ? '–' : Math.round(sc.m2);
tr.innerHTML =
'<td>'+ sc.name +(lim?' ★':'')+'</td>'+
'<td>'+ (sc.rolls === null ? '–' : sc.rolls) +'</td>'+
'<td>'+ (sc.weight === null ? '–' : Math.round(sc.weight)) +'</td>'+
'<td>'+ scM2 +'</td>'+
'<td>'+ util +'%</td>';
tbody.appendChild(tr);
});
document.getElementById('resultsContainer').classList.add('show');
}
fmt(n, dec=1) { return Number.isFinite(n) ? n.toFixed(dec) : '–'; }
drawSideView(analysis) {
const svg = document.getElementById('sideViewSVG');
const section = document.getElementById('sideViewSection');
if (!svg || !analysis || analysis.final_rolls === 0) return;
const D_m = analysis.roll.diameter_mm / 1000;
const aW = analysis.layout.rolls_along_width;
const aL = analysis.layout.rolls_along_length;
const layers = analysis.layout.max_stack_height;
const total = analysis.final_rolls;
const marginSide = analysis.layout.margin_side_m || 0;
const marginCeil = analysis.layout.margin_ceiling_m || 0;
const dim = analysis.transport;
const PAD = 52, W_SVG = 800, H_SVG = 300;
const scaleX = (W_SVG - 2*PAD) / dim.width_m;
const scaleY = (H_SVG - 2*PAD) / dim.height_m;
const scale = Math.min(scaleX, scaleY);
const cW = dim.width_m * scale;
const cH = dim.height_m * scale;
const cX = PAD + ((W_SVG - 2*PAD) - cW) / 2;
const cY = PAD + ((H_SVG - 2*PAD) - cH) / 2;
const rD = D_m * scale;
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>';
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"/>';
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"/>';
if (mSidePx > 0.5) {
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"/>';
}
if (mCeilPx > 0.5) {
s += '<rect x="'+cX.toFixed(1)+'" y="'+cY.toFixed(1)+'"'
+ ' width="'+cW.toFixed(1)+'" height="'+mCeilPx.toFixed(1)+'"'
+ ' fill="url(#hatchMargin)" opacity="0.9"/>';
}
let drawn = 0;
for (let layer = 0; layer < layers && drawn < total; layer++) {
for (let col = 0; col < aW && drawn < total; col++) {
const cx = cX + col * rD + rD / 2;
const cy = cY + cH - layer * rD - rD / 2 - 5;
const r = Math.max(1, rD / 2 - 1.0);
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)"/>';
s += '<circle cx="'+cx.toFixed(1)+'" cy="'+cy.toFixed(1)+'" r="'+r.toFixed(1)+'" fill="#a8d4f5" stroke="#2BAC70" stroke-width="1.2"/>';
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="#1F5438" stroke-width="1" opacity="0.8"/>';
}
drawn++;
}
}
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+' 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+' 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 × '+layers+' high × '+aL+' deep · '+total+' rolls</text>';
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="#2BAC70" 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="#2BAC70">⌀ '+(D_m*1000).toFixed(0)+' mm</text>';
}
svg.setAttribute('viewBox', '0 0 800 300');
svg.innerHTML = s;
section.classList.add('show');
}
resetForm() {
['customLength','customWidth','customHeight','customMaxWeight'].forEach(id =>
document.getElementById(id).value = ''
);
document.getElementById('loMarginSide').value = '0';
document.getElementById('loMarginCeiling').value = '20';
document.getElementById('loMarginSideEffective').textContent = '';
document.getElementById('loMarginCeilingEffective').textContent = '';
document.getElementById('resultsContainer').classList.remove('show');
document.getElementById('sideViewSection').classList.remove('show');
this.clearPresets();
}
}
// =====================================================
// INIT
// =====================================================
document.addEventListener('DOMContentLoaded', () => {
window.loadOptimizerUI = new LoadOptimizerUI();
});
document.addEventListener('DOMContentLoaded', () => {
const overlay = document.getElementById('disclaimerOverlay');
const checkbox = document.getElementById('disclaimerAccepted');
const confirmBtn = document.getElementById('disclaimerConfirmBtn');
if (!overlay || !checkbox || !confirmBtn) return;
checkbox.addEventListener('change', () => {
confirmBtn.disabled = !checkbox.checked;
});
confirmBtn.addEventListener('click', () => {
if (!checkbox.checked) return;
overlay.classList.add('is-hidden');
});
});
</script>
</body>
</html>