2608 lines
157 KiB
HTML
2608 lines
157 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>AZ INTEC — Precision Strip Basispreis Index</title>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.2/babel.min.js"></script>
|
||
<style>
|
||
*,
|
||
*::before,
|
||
*::after {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
html,
|
||
body {
|
||
background: #f0f1f2;
|
||
font-family: "Courier New", "Lucida Console", monospace;
|
||
color: #2e3640;
|
||
}
|
||
input,
|
||
select,
|
||
button {
|
||
font-family: "Courier New", "Lucida Console", monospace;
|
||
}
|
||
input:focus,
|
||
select:focus {
|
||
outline: 2px solid #e7362944;
|
||
}
|
||
::-webkit-scrollbar {
|
||
height: 5px;
|
||
width: 5px;
|
||
}
|
||
::-webkit-scrollbar-track {
|
||
background: #e8eaec;
|
||
}
|
||
::-webkit-scrollbar-thumb {
|
||
background: #c8cdd3;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="text/babel">
|
||
const { useState, useEffect } = React;
|
||
|
||
// ── Brand tokens ───────────────────────────────────────────────────────────
|
||
const R = "#e73629";
|
||
const BG = "#f0f1f2";
|
||
const S1 = "#ffffff";
|
||
const S2 = "#e8eaec";
|
||
const BD = "#c8cdd3";
|
||
const TH = "#6b7580";
|
||
const TM = "#2e3640";
|
||
const TF = "#0f1419";
|
||
const CAT_COLORS = { A: "#e73629", B: "#c45a20", C: "#8b6914" };
|
||
|
||
// ── Static reference data ──────────────────────────────────────────────────
|
||
const GRADES = [
|
||
{
|
||
id: "304",
|
||
label: "AISI 304",
|
||
en: "1.4301",
|
||
outokumpu: "Core 304/4301",
|
||
},
|
||
{
|
||
id: "316",
|
||
label: "AISI 316",
|
||
en: "1.4404",
|
||
outokumpu: "Supra 316L/4404",
|
||
},
|
||
{
|
||
id: "444",
|
||
label: "AISI 444",
|
||
en: "1.4521",
|
||
outokumpu: "Supra 444/4521",
|
||
},
|
||
];
|
||
const THICKNESSES = [
|
||
{ wt: "0,15 mm", cat: "A" },
|
||
{ wt: "0,18 mm", cat: "A" },
|
||
{ wt: "0,20 mm", cat: "B" },
|
||
{ wt: "0,22 mm", cat: "B" },
|
||
{ wt: "0,25 mm", cat: "B" },
|
||
{ wt: "0,30 mm", cat: "C" },
|
||
{ wt: "0,35 mm", cat: "C" },
|
||
{ wt: "0,40 mm", cat: "C" },
|
||
];
|
||
const PROD = {
|
||
A: { label: "0,15–0,19 mm", val: 1293 },
|
||
B: { label: "0,20–0,29 mm", val: 1063 },
|
||
C: { label: "0,30–0,40 mm", val: 763 },
|
||
};
|
||
const DATA_URL = "data.json";
|
||
const EMPTY_SURCHARGES = {
|
||
304: { month: "", value: 0 },
|
||
316: { month: "", value: 0 },
|
||
444: { month: "", value: 0 },
|
||
};
|
||
|
||
function monthToYear(month) {
|
||
const match = String(month || "").match(/(20\d{2})/);
|
||
return match ? Number(match[1]) : new Date().getFullYear();
|
||
}
|
||
|
||
function buildHistoryEntry(surcharges, baseSS) {
|
||
const month =
|
||
surcharges["316"]?.month ||
|
||
surcharges["304"]?.month ||
|
||
surcharges["444"]?.month ||
|
||
"";
|
||
return {
|
||
m: month,
|
||
y: monthToYear(month),
|
||
ss: Number.isFinite(Number(baseSS)) ? Number(baseSS) : null,
|
||
lz304: Number(surcharges["304"]?.value) || 0,
|
||
lz316: Number(surcharges["316"]?.value) || 0,
|
||
lz444: Number(surcharges["444"]?.value) || 0,
|
||
};
|
||
}
|
||
|
||
function upsertHistory(history, entry) {
|
||
if (!entry.m) return history;
|
||
const idx = history.findIndex((d) => d.m === entry.m);
|
||
if (idx >= 0) {
|
||
const next = [...history];
|
||
next[idx] = { ...next[idx], ...entry };
|
||
return next;
|
||
}
|
||
return [...history, entry];
|
||
}
|
||
|
||
|
||
function fmt(n, dec = 0) {
|
||
if (n === null || n === undefined) return "—";
|
||
return Number(n).toLocaleString("de-DE", {
|
||
minimumFractionDigits: dec,
|
||
maximumFractionDigits: dec,
|
||
});
|
||
}
|
||
|
||
// ── Logo ───────────────────────────────────────────────────────────────────
|
||
function Logo({ h = 20 }) {
|
||
return (
|
||
<svg
|
||
height={h}
|
||
viewBox="0 0 232.94 34.46"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
style={{ display: "block" }}
|
||
>
|
||
<path
|
||
fill="#e73629"
|
||
fillRule="evenodd"
|
||
d="M56.32,0v8.32h16.65l-20.54,17.85V0h-13.47L0,33.86h14.53l7.78-6.77h19.13v6.77h53.14v-8.32h-26.91L97.07,0h-40.75ZM41.45,10.47l-9.92,8.61h9.92v-8.61"
|
||
/>
|
||
<rect
|
||
fill="#090a0b"
|
||
x="106.71"
|
||
width="7.8"
|
||
height="33.86"
|
||
/>
|
||
<polygon
|
||
fill="#090a0b"
|
||
points="155.51 6.16 163.48 6.16 163.48 33.86 171.28 33.86 171.28 6.16 179.25 6.16 179.25 0 155.51 0 155.51 6.16 155.51 6.16"
|
||
/>
|
||
<polygon
|
||
fill="#090a0b"
|
||
points="144.18 22.24 128.86 .11 121.06 .11 121.06 33.86 128.86 33.86 128.86 11.73 144.18 33.86 151.97 33.86 151.97 .11 144.18 .11 144.18 22.24 144.18 22.24"
|
||
/>
|
||
<polygon
|
||
fill="#090a0b"
|
||
points="190.58 19.4 200.86 19.4 200.86 13.23 190.58 13.23 190.58 6.16 204.69 6.16 204.69 0 182.78 0 182.78 33.86 205.05 33.86 205.05 27.7 190.58 27.7 190.58 19.4 190.58 19.4"
|
||
/>
|
||
<path
|
||
fill="#090a0b"
|
||
d="M232.94,26.91c-1.88.96-4.05,1.53-6.38,1.53-7.01,0-12.67-5-12.67-11.15s5.66-11.15,12.67-11.15c2.33,0,4.5.57,6.38,1.53V1.07c-2.09-.63-4.33-.96-6.65-.96-11.15,0-20.19,7.69-20.19,17.17s9.05,17.17,20.19,17.17c2.33,0,4.57-.33,6.65-.96v-6.59"
|
||
/>
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
// ── Formula bar ────────────────────────────────────────────────────────────
|
||
function FormulaBar() {
|
||
const steps = [
|
||
"Basis Edelstahlpreis",
|
||
"Produktionsaufschlag",
|
||
"Deviance-Aufschlag",
|
||
"Legierungszuschlag",
|
||
];
|
||
return (
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
flexWrap: "wrap",
|
||
gap: 0,
|
||
marginBottom: 24,
|
||
borderLeft: `3px solid ${R}`,
|
||
paddingLeft: 14,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
color: TH,
|
||
marginRight: 10,
|
||
fontSize: "0.58rem",
|
||
letterSpacing: "0.15em",
|
||
textTransform: "uppercase",
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
Preisformel
|
||
</span>
|
||
{steps.map((label, i) => (
|
||
<span
|
||
key={i}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
color: i === 3 ? R : TM,
|
||
padding: "4px 10px",
|
||
background: i === 3 ? `${R}18` : S2,
|
||
border: `1px solid ${i === 3 ? R + "50" : BD}`,
|
||
fontSize: "0.68rem",
|
||
fontWeight: i === 3 ? 700 : 400,
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
{label}
|
||
</span>
|
||
{i < 3 && (
|
||
<span
|
||
style={{
|
||
color: R,
|
||
fontSize: "1rem",
|
||
padding: "0 6px",
|
||
fontWeight: 300,
|
||
}}
|
||
>
|
||
+
|
||
</span>
|
||
)}
|
||
</span>
|
||
))}
|
||
<span
|
||
style={{
|
||
color: R,
|
||
fontSize: "1rem",
|
||
padding: "0 8px",
|
||
fontWeight: 300,
|
||
}}
|
||
>
|
||
=
|
||
</span>
|
||
<span
|
||
style={{
|
||
color: "#fff",
|
||
fontWeight: 700,
|
||
padding: "4px 14px",
|
||
background: R,
|
||
fontSize: "0.72rem",
|
||
letterSpacing: "0.05em",
|
||
textTransform: "uppercase",
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
Index-Preis (€/t)
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Slope chart (calculator tab) ───────────────────────────────────────────
|
||
function SlopeChart({ data, deviance }) {
|
||
const [tooltip, setTooltip] = useState(null);
|
||
const W = 860,
|
||
H = 210,
|
||
PAD = { top: 16, right: 20, bottom: 36, left: 72 };
|
||
const iW = W - PAD.left - PAD.right,
|
||
iH = H - PAD.top - PAD.bottom;
|
||
const vals = data.map((d) => d.val);
|
||
const minV = Math.min(...vals) * 0.97,
|
||
maxV = Math.max(...vals) * 1.02;
|
||
const xOf = (i) => PAD.left + (i / (data.length - 1)) * iW;
|
||
const yOf = (v) =>
|
||
PAD.top + iH - ((v - minV) / (maxV - minV)) * iH;
|
||
const gridLines = Array.from({ length: 5 }, (_, i) => {
|
||
const v = minV + (i * (maxV - minV)) / 4;
|
||
return { v, y: yOf(v) };
|
||
});
|
||
const pts = data
|
||
.map((d, i) => `${xOf(i)},${yOf(d.val)}`)
|
||
.join(" ");
|
||
const areaPath =
|
||
`M ${xOf(0)},${yOf(data[0].val)} ` +
|
||
data
|
||
.slice(1)
|
||
.map((d, i) => `L ${xOf(i + 1)},${yOf(d.val)}`)
|
||
.join(" ") +
|
||
` L ${xOf(data.length - 1)},${PAD.top + iH} L ${xOf(0)},${PAD.top + iH} Z`;
|
||
return (
|
||
<div
|
||
style={{
|
||
background: S1,
|
||
border: `1px solid ${BD}`,
|
||
borderTop: `3px solid ${R}`,
|
||
padding: "16px 20px 12px",
|
||
marginBottom: 20,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "flex-start",
|
||
marginBottom: 12,
|
||
}}
|
||
>
|
||
<div>
|
||
<div
|
||
style={{
|
||
fontSize: "0.58rem",
|
||
color: TH,
|
||
letterSpacing: "0.15em",
|
||
textTransform: "uppercase",
|
||
marginBottom: 2,
|
||
}}
|
||
>
|
||
Index-Preis vor Legierungszuschlag
|
||
</div>
|
||
<div style={{ fontSize: "0.66rem", color: TH }}>
|
||
Basis SS + Produktionsaufschlag + Deviance (
|
||
{deviance}%) — nach Wandstärke
|
||
</div>
|
||
</div>
|
||
<div style={{ display: "flex", gap: 14 }}>
|
||
{Object.entries(CAT_COLORS).map(
|
||
([cat, col]) => (
|
||
<span
|
||
key={cat}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 5,
|
||
fontSize: "0.62rem",
|
||
color: TH,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
width: 8,
|
||
height: 8,
|
||
background: col,
|
||
display: "inline-block",
|
||
}}
|
||
/>
|
||
Kat. {cat}
|
||
</span>
|
||
),
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div style={{ overflowX: "auto" }}>
|
||
<svg
|
||
viewBox={`0 0 ${W} ${H}`}
|
||
style={{
|
||
width: "100%",
|
||
minWidth: 420,
|
||
display: "block",
|
||
}}
|
||
>
|
||
<defs>
|
||
<linearGradient
|
||
id="aFill"
|
||
x1="0"
|
||
y1="0"
|
||
x2="0"
|
||
y2="1"
|
||
>
|
||
<stop
|
||
offset="0%"
|
||
stopColor={R}
|
||
stopOpacity="0.15"
|
||
/>
|
||
<stop
|
||
offset="100%"
|
||
stopColor={R}
|
||
stopOpacity="0.01"
|
||
/>
|
||
</linearGradient>
|
||
</defs>
|
||
{gridLines.map((gl, i) => (
|
||
<g key={i}>
|
||
<line
|
||
x1={PAD.left}
|
||
y1={gl.y}
|
||
x2={PAD.left + iW}
|
||
y2={gl.y}
|
||
stroke={BD}
|
||
strokeWidth="1"
|
||
strokeDasharray="3 3"
|
||
/>
|
||
<text
|
||
x={PAD.left - 6}
|
||
y={gl.y + 4}
|
||
textAnchor="end"
|
||
fontSize="10"
|
||
fill={TH}
|
||
fontFamily="Courier New"
|
||
>
|
||
{fmt(gl.v)} €
|
||
</text>
|
||
</g>
|
||
))}
|
||
{[2, 5].map((idx) => (
|
||
<line
|
||
key={idx}
|
||
x1={xOf(idx)}
|
||
y1={PAD.top}
|
||
x2={xOf(idx)}
|
||
y2={PAD.top + iH}
|
||
stroke={BD}
|
||
strokeWidth="1"
|
||
strokeDasharray="4 3"
|
||
/>
|
||
))}
|
||
<path d={areaPath} fill="url(#aFill)" />
|
||
<polyline
|
||
points={pts}
|
||
fill="none"
|
||
stroke={R}
|
||
strokeWidth="2.5"
|
||
strokeLinejoin="round"
|
||
strokeLinecap="round"
|
||
/>
|
||
<line
|
||
x1={PAD.left}
|
||
y1={PAD.top + iH}
|
||
x2={PAD.left + iW}
|
||
y2={PAD.top + iH}
|
||
stroke={BD}
|
||
strokeWidth="1"
|
||
/>
|
||
{data.map((d, i) => {
|
||
const cx = xOf(i),
|
||
cy = yOf(d.val);
|
||
return (
|
||
<g
|
||
key={i}
|
||
onMouseEnter={() =>
|
||
setTooltip({
|
||
i,
|
||
x: cx,
|
||
y: cy,
|
||
d,
|
||
})
|
||
}
|
||
onMouseLeave={() =>
|
||
setTooltip(null)
|
||
}
|
||
style={{ cursor: "crosshair" }}
|
||
>
|
||
<rect
|
||
x={cx - 5}
|
||
y={cy - 5}
|
||
width={10}
|
||
height={10}
|
||
fill={CAT_COLORS[d.cat]}
|
||
stroke="#fff"
|
||
strokeWidth="2"
|
||
transform={`rotate(45,${cx},${cy})`}
|
||
/>
|
||
<text
|
||
x={cx}
|
||
y={PAD.top + iH + 20}
|
||
textAnchor="middle"
|
||
fontSize="10"
|
||
fill={TH}
|
||
fontFamily="Courier New"
|
||
>
|
||
{d.wt}
|
||
</text>
|
||
</g>
|
||
);
|
||
})}
|
||
{tooltip &&
|
||
(() => {
|
||
const tx =
|
||
tooltip.x > W * 0.7
|
||
? tooltip.x - 160
|
||
: tooltip.x + 12,
|
||
ty = tooltip.y - 30;
|
||
return (
|
||
<g>
|
||
<rect
|
||
x={tx}
|
||
y={ty}
|
||
width={150}
|
||
height={44}
|
||
fill="#fff"
|
||
stroke={BD}
|
||
strokeWidth="1"
|
||
/>
|
||
<rect
|
||
x={tx}
|
||
y={ty}
|
||
width={3}
|
||
height={44}
|
||
fill={R}
|
||
/>
|
||
<text
|
||
x={tx + 10}
|
||
y={ty + 16}
|
||
fontSize="11"
|
||
fontWeight="700"
|
||
fill={TF}
|
||
fontFamily="Courier New"
|
||
>
|
||
{tooltip.d.wt}
|
||
</text>
|
||
<text
|
||
x={tx + 10}
|
||
y={ty + 32}
|
||
fontSize="11"
|
||
fill={R}
|
||
fontFamily="Courier New"
|
||
>
|
||
{fmt(tooltip.d.val)} €/t
|
||
</text>
|
||
</g>
|
||
);
|
||
})()}
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── History line chart ─────────────────────────────────────────────────────
|
||
function HistoryChart({ histData, series }) {
|
||
const [tooltip, setTooltip] = useState(null);
|
||
const W = 920,
|
||
H = 280,
|
||
PAD = { top: 20, right: 20, bottom: 40, left: 80 };
|
||
const iW = W - PAD.left - PAD.right,
|
||
iH = H - PAD.top - PAD.bottom;
|
||
const n = histData.length;
|
||
|
||
const allVals = series.flatMap((s) =>
|
||
histData.map((d) => d[s.key]).filter((v) => v != null),
|
||
);
|
||
if (allVals.length === 0) return null;
|
||
const minV = Math.min(...allVals) * 0.95,
|
||
maxV = Math.max(...allVals) * 1.03;
|
||
|
||
const xOf = (i) => PAD.left + (i / (n - 1)) * iW;
|
||
const yOf = (v) =>
|
||
PAD.top + iH - ((v - minV) / (maxV - minV)) * iH;
|
||
|
||
// Year boundary indices
|
||
const yearBounds = [];
|
||
let lastY = null;
|
||
histData.forEach((d, i) => {
|
||
if (d.y !== lastY) {
|
||
yearBounds.push({ i, y: d.y });
|
||
lastY = d.y;
|
||
}
|
||
});
|
||
|
||
// Grid
|
||
const gridLines = Array.from({ length: 5 }, (_, i) => {
|
||
const v = minV + (i * (maxV - minV)) / 4;
|
||
return { v, y: yOf(v) };
|
||
});
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
background: S1,
|
||
border: `1px solid ${BD}`,
|
||
borderTop: `3px solid ${R}`,
|
||
padding: "16px 20px 8px",
|
||
marginBottom: 20,
|
||
position: "relative",
|
||
}}
|
||
>
|
||
<div style={{ overflowX: "auto" }}>
|
||
<svg
|
||
viewBox={`0 0 ${W} ${H}`}
|
||
style={{
|
||
width: "100%",
|
||
minWidth: 600,
|
||
display: "block",
|
||
}}
|
||
>
|
||
<defs>
|
||
{series.map((s) => (
|
||
<linearGradient
|
||
key={s.key}
|
||
id={`g_${s.key}`}
|
||
x1="0"
|
||
y1="0"
|
||
x2="0"
|
||
y2="1"
|
||
>
|
||
<stop
|
||
offset="0%"
|
||
stopColor={s.color}
|
||
stopOpacity="0.12"
|
||
/>
|
||
<stop
|
||
offset="100%"
|
||
stopColor={s.color}
|
||
stopOpacity="0"
|
||
/>
|
||
</linearGradient>
|
||
))}
|
||
</defs>
|
||
|
||
{/* Grid */}
|
||
{gridLines.map((gl, i) => (
|
||
<g key={i}>
|
||
<line
|
||
x1={PAD.left}
|
||
y1={gl.y}
|
||
x2={PAD.left + iW}
|
||
y2={gl.y}
|
||
stroke={BD}
|
||
strokeWidth="1"
|
||
strokeDasharray="3 3"
|
||
/>
|
||
<text
|
||
x={PAD.left - 6}
|
||
y={gl.y + 4}
|
||
textAnchor="end"
|
||
fontSize="10"
|
||
fill={TH}
|
||
fontFamily="Courier New"
|
||
>
|
||
{fmt(gl.v)}
|
||
</text>
|
||
</g>
|
||
))}
|
||
|
||
{/* Year boundaries + labels */}
|
||
{yearBounds.map(({ i, y }) => (
|
||
<g key={y}>
|
||
{i > 0 && (
|
||
<line
|
||
x1={xOf(i)}
|
||
y1={PAD.top}
|
||
x2={xOf(i)}
|
||
y2={PAD.top + iH}
|
||
stroke={BD}
|
||
strokeWidth="1"
|
||
strokeDasharray="4 3"
|
||
/>
|
||
)}
|
||
<text
|
||
x={
|
||
i === 0
|
||
? xOf(i) + 4
|
||
: xOf(i) + 4
|
||
}
|
||
y={PAD.top + iH + 28}
|
||
fontSize="11"
|
||
fontWeight="700"
|
||
fill={TM}
|
||
fontFamily="Courier New"
|
||
>
|
||
{y}
|
||
</text>
|
||
</g>
|
||
))}
|
||
|
||
{/* X axis */}
|
||
<line
|
||
x1={PAD.left}
|
||
y1={PAD.top + iH}
|
||
x2={PAD.left + iW}
|
||
y2={PAD.top + iH}
|
||
stroke={BD}
|
||
strokeWidth="1.5"
|
||
/>
|
||
|
||
{/* Area + Lines */}
|
||
{series.map((s) => {
|
||
const validPts = histData
|
||
.map((d, i) => ({ i, v: d[s.key] }))
|
||
.filter((p) => p.v != null);
|
||
if (validPts.length < 2) return null;
|
||
|
||
// Build path segments (handle nulls)
|
||
let polyParts = [];
|
||
let current = [];
|
||
histData.forEach((d, i) => {
|
||
if (d[s.key] != null) {
|
||
current.push({ i, v: d[s.key] });
|
||
} else if (current.length > 0) {
|
||
polyParts.push(current);
|
||
current = [];
|
||
}
|
||
});
|
||
if (current.length > 0)
|
||
polyParts.push(current);
|
||
|
||
return polyParts.map((seg, si) => {
|
||
const linePts = seg
|
||
.map(
|
||
(p) =>
|
||
`${xOf(p.i)},${yOf(p.v)}`,
|
||
)
|
||
.join(" ");
|
||
const aPath =
|
||
`M ${xOf(seg[0].i)},${yOf(seg[0].v)} ` +
|
||
seg
|
||
.slice(1)
|
||
.map(
|
||
(p) =>
|
||
`L ${xOf(p.i)},${yOf(p.v)}`,
|
||
)
|
||
.join(" ") +
|
||
` L ${xOf(seg[seg.length - 1].i)},${PAD.top + iH} L ${xOf(seg[0].i)},${PAD.top + iH} Z`;
|
||
return (
|
||
<g key={`${s.key}_${si}`}>
|
||
<path
|
||
d={aPath}
|
||
fill={`url(#g_${s.key})`}
|
||
/>
|
||
<polyline
|
||
points={linePts}
|
||
fill="none"
|
||
stroke={s.color}
|
||
strokeWidth="2"
|
||
strokeLinejoin="round"
|
||
strokeLinecap="round"
|
||
/>
|
||
</g>
|
||
);
|
||
});
|
||
})}
|
||
|
||
{/* Hover zones + dots */}
|
||
{histData.map((d, i) => {
|
||
const hasAny = series.some(
|
||
(s) => d[s.key] != null,
|
||
);
|
||
if (!hasAny) return null;
|
||
return (
|
||
<rect
|
||
key={i}
|
||
x={xOf(i) - iW / (2 * (n - 1))}
|
||
y={PAD.top}
|
||
width={iW / (n - 1)}
|
||
height={iH}
|
||
fill="transparent"
|
||
onMouseEnter={() =>
|
||
setTooltip({ i, d })
|
||
}
|
||
onMouseLeave={() =>
|
||
setTooltip(null)
|
||
}
|
||
style={{ cursor: "crosshair" }}
|
||
/>
|
||
);
|
||
})}
|
||
|
||
{/* Active tooltip dots */}
|
||
{tooltip &&
|
||
series.map(
|
||
(s) =>
|
||
tooltip.d[s.key] != null && (
|
||
<circle
|
||
key={s.key}
|
||
cx={xOf(tooltip.i)}
|
||
cy={yOf(tooltip.d[s.key])}
|
||
r={4}
|
||
fill={s.color}
|
||
stroke="#fff"
|
||
strokeWidth="1.5"
|
||
/>
|
||
),
|
||
)}
|
||
|
||
{/* Tooltip box */}
|
||
{tooltip &&
|
||
(() => {
|
||
const tx =
|
||
tooltip.i > n * 0.7
|
||
? xOf(tooltip.i) - 180
|
||
: xOf(tooltip.i) + 12;
|
||
const visibleSeries = series.filter(
|
||
(s) => tooltip.d[s.key] != null,
|
||
);
|
||
const boxH =
|
||
20 + visibleSeries.length * 18;
|
||
return (
|
||
<g>
|
||
<rect
|
||
x={tx}
|
||
y={PAD.top + 4}
|
||
width={168}
|
||
height={boxH}
|
||
fill="#fff"
|
||
stroke={BD}
|
||
strokeWidth="1"
|
||
/>
|
||
<rect
|
||
x={tx}
|
||
y={PAD.top + 4}
|
||
width={3}
|
||
height={boxH}
|
||
fill={R}
|
||
/>
|
||
<text
|
||
x={tx + 10}
|
||
y={PAD.top + 18}
|
||
fontSize="11"
|
||
fontWeight="700"
|
||
fill={TF}
|
||
fontFamily="Courier New"
|
||
>
|
||
{tooltip.d.m}
|
||
</text>
|
||
{visibleSeries.map((s, si) => (
|
||
<text
|
||
key={s.key}
|
||
x={tx + 10}
|
||
y={
|
||
PAD.top +
|
||
34 +
|
||
si * 18
|
||
}
|
||
fontSize="10"
|
||
fill={s.color}
|
||
fontFamily="Courier New"
|
||
>
|
||
{s.shortLabel}:{" "}
|
||
{fmt(tooltip.d[s.key])}{" "}
|
||
€/t
|
||
</text>
|
||
))}
|
||
</g>
|
||
);
|
||
})()}
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Main App ───────────────────────────────────────────────────────────────
|
||
function App() {
|
||
const [grade, setGrade] = useState("316");
|
||
const [baseSS, setBaseSS] = useState(0);
|
||
const [deviance, setDeviance] = useState(6);
|
||
const [surcharges, setSurcharges] = useState(EMPTY_SURCHARGES);
|
||
const [history, setHistory] = useState([]);
|
||
const [dataLoaded, setDataLoaded] = useState(false);
|
||
const [saveStatus, setSaveStatus] = useState("");
|
||
const [activeTab, setActiveTab] = useState("calculator");
|
||
|
||
// History tab state
|
||
const [histGrade, setHistGrade] = useState("316");
|
||
const [histCat, setHistCat] = useState("B");
|
||
const [showSeries, setShowSeries] = useState({
|
||
baseSS: true,
|
||
lz: true,
|
||
indexVorLZ: true,
|
||
finalIdx: true,
|
||
});
|
||
|
||
useEffect(() => {
|
||
fetch(DATA_URL, { cache: "no-store" })
|
||
.then((res) => {
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
return res.json();
|
||
})
|
||
.then((data) => {
|
||
setSurcharges(data.surcharges || EMPTY_SURCHARGES);
|
||
setHistory(data.history || []);
|
||
setBaseSS(Number(data.baseSS) || 0);
|
||
setDataLoaded(true);
|
||
})
|
||
.catch((err) => {
|
||
console.error("data.json konnte nicht geladen werden", err);
|
||
setSaveStatus("data.json konnte nicht geladen werden. Bitte über einen Webserver starten.");
|
||
setDataLoaded(true);
|
||
});
|
||
}, []);
|
||
|
||
const gi = GRADES.find((g) => g.id === grade);
|
||
const sc = surcharges[grade] || EMPTY_SURCHARGES[grade];
|
||
|
||
const calc = (t) => {
|
||
const prod = PROD[t.cat].val,
|
||
sub = baseSS + prod,
|
||
dev = sub * (deviance / 100);
|
||
const noLZ = sub + dev,
|
||
total = noLZ + sc.value;
|
||
return { prod, sub, dev, noLZ, total };
|
||
};
|
||
const updSC = (id, field, val) =>
|
||
setSurcharges((prev) => ({
|
||
...prev,
|
||
[id]: { ...(prev[id] || EMPTY_SURCHARGES[id]), [field]: val },
|
||
}));
|
||
|
||
const downloadData = (data) => {
|
||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = "data.json";
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
};
|
||
|
||
const saveData = async () => {
|
||
const entry = buildHistoryEntry(surcharges, baseSS);
|
||
const nextHistory = upsertHistory(history, entry);
|
||
const nextData = { surcharges, history: nextHistory, baseSS: Number(baseSS) || 0 };
|
||
setHistory(nextHistory);
|
||
setSaveStatus("Speichere …");
|
||
try {
|
||
const res = await fetch("/api/data", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(nextData),
|
||
});
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
setSaveStatus("Gespeichert in data.json.");
|
||
} catch (err) {
|
||
console.warn("Direktes Speichern nicht möglich, JSON wird heruntergeladen", err);
|
||
downloadData(nextData);
|
||
setSaveStatus("Kein Schreibzugriff im statischen Browser-Modus. data.json wurde heruntergeladen; bitte Datei im Projekt ersetzen.");
|
||
}
|
||
};
|
||
|
||
const chartData = THICKNESSES.map((t) => ({
|
||
wt: t.wt,
|
||
cat: t.cat,
|
||
val: calc(t).noLZ,
|
||
}));
|
||
|
||
// Compute history series values for selected grade + category
|
||
const histComputed = history.map((d) => {
|
||
const lzKey = `lz${histGrade}`;
|
||
const lz = d[lzKey];
|
||
let indexVorLZ = null,
|
||
finalIdx = null;
|
||
if (d.ss != null) {
|
||
const prod = PROD[histCat].val,
|
||
sub = d.ss + prod,
|
||
dev = sub * (deviance / 100);
|
||
indexVorLZ = sub + dev;
|
||
if (lz != null) finalIdx = indexVorLZ + lz;
|
||
}
|
||
return { ...d, lz, indexVorLZ, finalIdx };
|
||
});
|
||
|
||
const SERIES_DEF = [
|
||
{
|
||
key: "ss",
|
||
shortLabel: "Basis SS",
|
||
label: "Basis Edelstahlpreis",
|
||
color: "#2563eb",
|
||
},
|
||
{
|
||
key: "lz",
|
||
shortLabel: `LZ ${histGrade}`,
|
||
label: `Legierungszuschlag AISI ${histGrade}`,
|
||
color:
|
||
histGrade === "304"
|
||
? "#d97706"
|
||
: histGrade === "316"
|
||
? R
|
||
: "#059669",
|
||
},
|
||
{
|
||
key: "indexVorLZ",
|
||
shortLabel: "Index vor LZ",
|
||
label: `Index vor LZ (Kat. ${histCat})`,
|
||
color: "#7c3aed",
|
||
},
|
||
{
|
||
key: "finalIdx",
|
||
shortLabel: "Finaler Index",
|
||
label: `Finaler Index (AISI ${histGrade}, Kat. ${histCat})`,
|
||
color: "#0f1419",
|
||
},
|
||
];
|
||
|
||
const activeSeries = SERIES_DEF.filter(
|
||
(s) =>
|
||
showSeries[
|
||
s.key === "ss"
|
||
? "baseSS"
|
||
: s.key === "lz"
|
||
? "lz"
|
||
: s.key === "indexVorLZ"
|
||
? "indexVorLZ"
|
||
: "finalIdx"
|
||
],
|
||
);
|
||
|
||
// Map computed data to series keys
|
||
const histChartData = histComputed.map((d) => ({
|
||
...d,
|
||
ss: d.ss,
|
||
lz: d.lz,
|
||
indexVorLZ: d.indexVorLZ,
|
||
finalIdx: d.finalIdx,
|
||
m: d.m,
|
||
y: d.y,
|
||
}));
|
||
|
||
// Shared styles
|
||
const card = {
|
||
background: S1,
|
||
border: `1px solid ${BD}`,
|
||
padding: "16px 18px",
|
||
};
|
||
const lbl = {
|
||
display: "block",
|
||
fontSize: "0.58rem",
|
||
color: TH,
|
||
letterSpacing: "0.12em",
|
||
textTransform: "uppercase",
|
||
marginBottom: 6,
|
||
};
|
||
const field = {
|
||
background: BG,
|
||
border: `1px solid ${BD}`,
|
||
color: TF,
|
||
padding: "7px 10px",
|
||
fontSize: "0.8rem",
|
||
width: "100%",
|
||
boxSizing: "border-box",
|
||
fontFamily: "inherit",
|
||
outline: "none",
|
||
};
|
||
const hint = { fontSize: "0.6rem", color: TH, marginTop: 4 };
|
||
const secH = {
|
||
fontSize: "0.62rem",
|
||
color: R,
|
||
letterSpacing: "0.14em",
|
||
textTransform: "uppercase",
|
||
fontWeight: 700,
|
||
marginTop: 0,
|
||
marginBottom: 14,
|
||
borderBottom: `1px solid ${BD}`,
|
||
paddingBottom: 8,
|
||
};
|
||
|
||
const tabs = [
|
||
["calculator", "⚙ Rechner"],
|
||
["history", "📈 Verlauf"],
|
||
["methodology", "∑ Methodik"],
|
||
["surcharge", "◈ Legierungszuschlag"],
|
||
];
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
background: BG,
|
||
minHeight: "100vh",
|
||
fontFamily: "'Courier New',monospace",
|
||
color: TM,
|
||
}}
|
||
>
|
||
<div style={{ maxWidth: 1020, margin: "0 auto" }}>
|
||
{/* Header */}
|
||
<div
|
||
style={{
|
||
background: S1,
|
||
borderBottom: `1px solid ${BD}`,
|
||
padding: "0 28px",
|
||
display: "flex",
|
||
alignItems: "stretch",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
padding: "18px 24px 18px 0",
|
||
borderRight: `1px solid ${BD}`,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
marginRight: 24,
|
||
}}
|
||
>
|
||
<Logo h={20} />
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
justifyContent: "center",
|
||
flex: 1,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: "0.6rem",
|
||
color: TH,
|
||
letterSpacing: "0.2em",
|
||
textTransform: "uppercase",
|
||
marginBottom: 3,
|
||
}}
|
||
>
|
||
Precision Strip · Cold Rolled Stainless
|
||
Steel
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: "1.05rem",
|
||
fontWeight: 700,
|
||
color: TF,
|
||
letterSpacing: "0.04em",
|
||
textTransform: "uppercase",
|
||
}}
|
||
>
|
||
Basispreis{" "}
|
||
<span style={{ color: R }}>Index</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
borderLeft: `1px solid ${BD}`,
|
||
paddingLeft: 20,
|
||
marginLeft: 16,
|
||
}}
|
||
>
|
||
<div style={{ textAlign: "right" }}>
|
||
<div
|
||
style={{
|
||
fontSize: "0.58rem",
|
||
color: TH,
|
||
letterSpacing: "0.15em",
|
||
textTransform: "uppercase",
|
||
marginBottom: 2,
|
||
}}
|
||
>
|
||
Aktive Güte
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: "0.9rem",
|
||
fontWeight: 700,
|
||
color: R,
|
||
}}
|
||
>
|
||
{gi.label} · {gi.en}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style={{
|
||
height: 3,
|
||
background: `linear-gradient(90deg,${R} 0%,${R}40 60%,transparent 100%)`,
|
||
}}
|
||
/>
|
||
|
||
<div style={{ padding: "24px 28px" }}>
|
||
{/* Tabs */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
marginBottom: 24,
|
||
borderBottom: `1px solid ${BD}`,
|
||
}}
|
||
>
|
||
{tabs.map(([id, label]) => (
|
||
<button
|
||
key={id}
|
||
onClick={() => setActiveTab(id)}
|
||
style={{
|
||
background: "transparent",
|
||
border: "none",
|
||
borderBottom:
|
||
activeTab === id
|
||
? `2px solid ${R}`
|
||
: "2px solid transparent",
|
||
color:
|
||
activeTab === id ? TF : TH,
|
||
padding: "10px 20px",
|
||
fontSize: "0.65rem",
|
||
letterSpacing: "0.12em",
|
||
textTransform: "uppercase",
|
||
cursor: "pointer",
|
||
fontFamily: "inherit",
|
||
marginBottom: -1,
|
||
}}
|
||
>
|
||
{label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* ──── RECHNER ──── */}
|
||
{activeTab === "calculator" && (
|
||
<div>
|
||
<FormulaBar />
|
||
<div
|
||
style={{
|
||
display: "grid",
|
||
gridTemplateColumns:
|
||
"1fr 1fr 1fr 1fr",
|
||
gap: 12,
|
||
marginBottom: 20,
|
||
}}
|
||
>
|
||
<div style={card}>
|
||
<label style={lbl}>
|
||
Stahlgüte
|
||
</label>
|
||
<select
|
||
value={grade}
|
||
onChange={(e) =>
|
||
setGrade(e.target.value)
|
||
}
|
||
style={field}
|
||
>
|
||
{GRADES.map((g) => (
|
||
<option
|
||
key={g.id}
|
||
value={g.id}
|
||
>
|
||
{g.label} · {g.en}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<div style={hint}>
|
||
{gi.outokumpu}
|
||
</div>
|
||
</div>
|
||
<div style={card}>
|
||
<label style={lbl}>
|
||
Basis Edelstahlpreis (€/t)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
step="10"
|
||
value={baseSS}
|
||
onChange={(e) =>
|
||
setBaseSS(
|
||
parseFloat(
|
||
e.target.value,
|
||
) || 0,
|
||
)
|
||
}
|
||
style={field}
|
||
/>
|
||
<div style={hint}>
|
||
CRC Stainless, kaltgewalzt
|
||
</div>
|
||
</div>
|
||
<div style={card}>
|
||
<label style={lbl}>
|
||
Deviance-Aufschlag (%)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
step="0.5"
|
||
value={deviance}
|
||
onChange={(e) =>
|
||
setDeviance(
|
||
parseFloat(
|
||
e.target.value,
|
||
) || 0,
|
||
)
|
||
}
|
||
style={field}
|
||
/>
|
||
<div style={hint}>
|
||
Toleranz · Yield ·
|
||
Modell-Unschärfe
|
||
</div>
|
||
</div>
|
||
<div
|
||
style={{
|
||
...card,
|
||
borderTop: `2px solid ${R}50`,
|
||
}}
|
||
>
|
||
<label
|
||
style={{
|
||
...lbl,
|
||
color: `${R}bb`,
|
||
}}
|
||
>
|
||
Legierungszuschlag (€/t)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
step="1"
|
||
value={sc.value}
|
||
onChange={(e) =>
|
||
updSC(
|
||
grade,
|
||
"value",
|
||
parseFloat(
|
||
e.target.value,
|
||
) || 0,
|
||
)
|
||
}
|
||
style={{
|
||
...field,
|
||
color: R,
|
||
borderColor: `${R}60`,
|
||
}}
|
||
/>
|
||
<div
|
||
style={{
|
||
...hint,
|
||
color: `${R}99`,
|
||
}}
|
||
>
|
||
Outokumpu · {sc.month}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
gap: 2,
|
||
marginBottom: 20,
|
||
}}
|
||
>
|
||
{Object.entries(PROD).map(
|
||
([cat, p]) => (
|
||
<div
|
||
key={cat}
|
||
style={{
|
||
flex: 1,
|
||
background: S2,
|
||
borderLeft: `3px solid ${CAT_COLORS[cat]}`,
|
||
padding:
|
||
"10px 14px",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize:
|
||
"0.58rem",
|
||
color: TH,
|
||
letterSpacing:
|
||
"0.1em",
|
||
textTransform:
|
||
"uppercase",
|
||
marginBottom: 2,
|
||
}}
|
||
>
|
||
Kategorie {cat} ·{" "}
|
||
{p.label}
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize:
|
||
"0.88rem",
|
||
color: TF,
|
||
fontWeight: 700,
|
||
}}
|
||
>
|
||
+ {fmt(p.val)}{" "}
|
||
<span
|
||
style={{
|
||
fontSize:
|
||
"0.62rem",
|
||
color: TH,
|
||
fontWeight: 400,
|
||
}}
|
||
>
|
||
€/t
|
||
</span>
|
||
</div>
|
||
</div>
|
||
),
|
||
)}
|
||
</div>
|
||
<SlopeChart
|
||
data={chartData}
|
||
deviance={deviance}
|
||
/>
|
||
<div style={{ overflowX: "auto" }}>
|
||
<table
|
||
style={{
|
||
width: "100%",
|
||
borderCollapse: "collapse",
|
||
fontSize: "0.75rem",
|
||
}}
|
||
>
|
||
<thead>
|
||
<tr
|
||
style={{
|
||
borderBottom: `2px solid ${BD}`,
|
||
}}
|
||
>
|
||
{[
|
||
[
|
||
"Wandstärke",
|
||
false,
|
||
],
|
||
["Kat.", false],
|
||
[
|
||
"Basis SS\n€/t",
|
||
true,
|
||
],
|
||
[
|
||
"+ Produktion\n€/t",
|
||
true,
|
||
],
|
||
[
|
||
"+ Deviance\n€/t",
|
||
true,
|
||
],
|
||
[
|
||
"Index vor LZ\n€/t",
|
||
true,
|
||
],
|
||
[
|
||
"+ Leg.zuschlag\n€/t",
|
||
true,
|
||
],
|
||
[
|
||
"= Index-Preis\n€/t",
|
||
true,
|
||
],
|
||
].map(([h, r], i) => (
|
||
<th
|
||
key={i}
|
||
style={{
|
||
padding:
|
||
"8px 10px",
|
||
textAlign: r
|
||
? "right"
|
||
: "left",
|
||
color: TH,
|
||
fontWeight: 600,
|
||
fontSize:
|
||
"0.6rem",
|
||
letterSpacing:
|
||
"0.08em",
|
||
textTransform:
|
||
"uppercase",
|
||
whiteSpace:
|
||
"pre-line",
|
||
fontFamily:
|
||
"inherit",
|
||
}}
|
||
>
|
||
{h}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{THICKNESSES.map(
|
||
(t, idx) => {
|
||
const c = calc(t);
|
||
return (
|
||
<tr
|
||
key={t.wt}
|
||
style={{
|
||
borderBottom: `1px solid ${BD}`,
|
||
background:
|
||
idx %
|
||
2 ===
|
||
0
|
||
? S2
|
||
: "transparent",
|
||
}}
|
||
>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"10px 10px",
|
||
fontWeight: 700,
|
||
color: TF,
|
||
}}
|
||
>
|
||
{t.wt}
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"10px 10px",
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
background:
|
||
CAT_COLORS[
|
||
t
|
||
.cat
|
||
] +
|
||
"22",
|
||
color: CAT_COLORS[
|
||
t
|
||
.cat
|
||
],
|
||
border: `1px solid ${CAT_COLORS[t.cat]}55`,
|
||
padding:
|
||
"2px 8px",
|
||
fontSize:
|
||
"0.65rem",
|
||
fontWeight: 700,
|
||
}}
|
||
>
|
||
{
|
||
t.cat
|
||
}
|
||
</span>
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"10px 10px",
|
||
textAlign:
|
||
"right",
|
||
color: TH,
|
||
}}
|
||
>
|
||
{fmt(
|
||
baseSS,
|
||
)}
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"10px 10px",
|
||
textAlign:
|
||
"right",
|
||
color: TM,
|
||
}}
|
||
>
|
||
+
|
||
{fmt(
|
||
c.prod,
|
||
)}
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"10px 10px",
|
||
textAlign:
|
||
"right",
|
||
color: TM,
|
||
}}
|
||
>
|
||
+
|
||
{fmt(
|
||
c.dev,
|
||
)}
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"10px 10px",
|
||
textAlign:
|
||
"right",
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
color: TF,
|
||
fontWeight: 700,
|
||
}}
|
||
>
|
||
{fmt(
|
||
c.noLZ,
|
||
)}
|
||
</span>
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"10px 10px",
|
||
textAlign:
|
||
"right",
|
||
color: R,
|
||
}}
|
||
>
|
||
+
|
||
{fmt(
|
||
sc.value,
|
||
)}
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"10px 10px",
|
||
textAlign:
|
||
"right",
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
background:
|
||
R,
|
||
color: "#fff",
|
||
fontWeight: 700,
|
||
fontSize:
|
||
"0.82rem",
|
||
padding:
|
||
"3px 10px",
|
||
}}
|
||
>
|
||
{fmt(
|
||
c.total,
|
||
)}
|
||
</span>
|
||
<span
|
||
style={{
|
||
color: TH,
|
||
fontSize:
|
||
"0.6rem",
|
||
marginLeft: 4,
|
||
}}
|
||
>
|
||
€/t
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
);
|
||
},
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div
|
||
style={{
|
||
marginTop: 12,
|
||
padding: "9px 14px",
|
||
borderLeft: `3px solid ${R}44`,
|
||
background: S1,
|
||
fontSize: "0.6rem",
|
||
color: TH,
|
||
}}
|
||
>
|
||
⚠ Legierungszuschlag Outokumpu
|
||
Precision Strip Europe · Stand:{" "}
|
||
<strong style={{ color: R }}>
|
||
{sc.month}
|
||
</strong>{" "}
|
||
· Monatlich unter Tab
|
||
"Legierungszuschlag" aktualisieren
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* ──── VERLAUF ──── */}
|
||
{activeTab === "history" && (
|
||
<div>
|
||
{/* Controls row */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
gap: 12,
|
||
marginBottom: 20,
|
||
flexWrap: "wrap",
|
||
alignItems: "flex-end",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
...card,
|
||
padding: "12px 14px",
|
||
minWidth: 160,
|
||
}}
|
||
>
|
||
<label style={lbl}>
|
||
Stahlgüte
|
||
</label>
|
||
<select
|
||
value={histGrade}
|
||
onChange={(e) =>
|
||
setHistGrade(
|
||
e.target.value,
|
||
)
|
||
}
|
||
style={field}
|
||
>
|
||
{GRADES.map((g) => (
|
||
<option
|
||
key={g.id}
|
||
value={g.id}
|
||
>
|
||
{g.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div
|
||
style={{
|
||
...card,
|
||
padding: "12px 14px",
|
||
minWidth: 160,
|
||
}}
|
||
>
|
||
<label style={lbl}>
|
||
Kategorie (Index-Berechnung)
|
||
</label>
|
||
<select
|
||
value={histCat}
|
||
onChange={(e) =>
|
||
setHistCat(
|
||
e.target.value,
|
||
)
|
||
}
|
||
style={field}
|
||
>
|
||
{Object.entries(PROD).map(
|
||
([cat, p]) => (
|
||
<option
|
||
key={cat}
|
||
value={cat}
|
||
>
|
||
Kat. {cat} ·{" "}
|
||
{p.label}
|
||
</option>
|
||
),
|
||
)}
|
||
</select>
|
||
</div>
|
||
<div
|
||
style={{
|
||
...card,
|
||
padding: "12px 16px",
|
||
flex: 1,
|
||
minWidth: 300,
|
||
}}
|
||
>
|
||
<label style={lbl}>
|
||
Sichtbare Linien
|
||
</label>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
gap: 12,
|
||
flexWrap: "wrap",
|
||
marginTop: 4,
|
||
}}
|
||
>
|
||
{[
|
||
{
|
||
key: "baseSS",
|
||
color: "#2563eb",
|
||
label: "Basis SS",
|
||
},
|
||
{
|
||
key: "lz",
|
||
color:
|
||
histGrade ===
|
||
"304"
|
||
? "#d97706"
|
||
: histGrade ===
|
||
"316"
|
||
? R
|
||
: "#059669",
|
||
label: `LZ ${histGrade}`,
|
||
},
|
||
{
|
||
key: "indexVorLZ",
|
||
color: "#7c3aed",
|
||
label: "Index vor LZ",
|
||
},
|
||
{
|
||
key: "finalIdx",
|
||
color: "#0f1419",
|
||
label: "Finaler Index",
|
||
},
|
||
].map((s) => (
|
||
<label
|
||
key={s.key}
|
||
style={{
|
||
display: "flex",
|
||
alignItems:
|
||
"center",
|
||
gap: 6,
|
||
cursor: "pointer",
|
||
fontSize:
|
||
"0.68rem",
|
||
color: showSeries[
|
||
s.key
|
||
]
|
||
? TF
|
||
: TH,
|
||
}}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={
|
||
showSeries[
|
||
s.key
|
||
]
|
||
}
|
||
onChange={(e) =>
|
||
setShowSeries(
|
||
(
|
||
prev,
|
||
) => ({
|
||
...prev,
|
||
[s.key]:
|
||
e
|
||
.target
|
||
.checked,
|
||
}),
|
||
)
|
||
}
|
||
style={{
|
||
accentColor:
|
||
s.color,
|
||
width: 14,
|
||
height: 14,
|
||
}}
|
||
/>
|
||
<span
|
||
style={{
|
||
display:
|
||
"flex",
|
||
alignItems:
|
||
"center",
|
||
gap: 4,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
width: 20,
|
||
height: 2,
|
||
background:
|
||
s.color,
|
||
display:
|
||
"inline-block",
|
||
verticalAlign:
|
||
"middle",
|
||
}}
|
||
/>
|
||
{s.label}
|
||
</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Chart */}
|
||
<HistoryChart
|
||
histData={histChartData}
|
||
series={activeSeries}
|
||
/>
|
||
|
||
{/* Info banner */}
|
||
<div
|
||
style={{
|
||
padding: "8px 14px",
|
||
borderLeft: `3px solid #2563eb44`,
|
||
background: S1,
|
||
fontSize: "0.6rem",
|
||
color: TH,
|
||
marginBottom: 20,
|
||
}}
|
||
>
|
||
📊 64 Datenpunkte · Jan 2021 – Apr
|
||
2026 · Basis SS-Preis:
|
||
SteelBenchmarker/S&P Global
|
||
(indicative, CRC Northern Europe) ·
|
||
Legierungszuschlag: Outokumpu{" "}
|
||
<strong style={{ color: TM }}>
|
||
Precision Strip
|
||
</strong>{" "}
|
||
· Deviance {deviance}% aus
|
||
Rechner-Tab
|
||
</div>
|
||
|
||
{/* Historical table */}
|
||
<div style={card}>
|
||
<h3 style={secH}>
|
||
Historische Daten —
|
||
Monatstabelle
|
||
</h3>
|
||
<div
|
||
style={{
|
||
overflowX: "auto",
|
||
overflowY: "auto",
|
||
maxHeight: 420,
|
||
}}
|
||
>
|
||
<table
|
||
style={{
|
||
width: "100%",
|
||
borderCollapse:
|
||
"collapse",
|
||
fontSize: "0.7rem",
|
||
minWidth: 700,
|
||
}}
|
||
>
|
||
<thead
|
||
style={{
|
||
position: "sticky",
|
||
top: 0,
|
||
background: S1,
|
||
zIndex: 1,
|
||
}}
|
||
>
|
||
<tr
|
||
style={{
|
||
borderBottom: `2px solid ${BD}`,
|
||
}}
|
||
>
|
||
{[
|
||
"Monat",
|
||
"Basis SS\n€/t",
|
||
`LZ ${histGrade}\n€/t`,
|
||
"Index vor LZ\n€/t",
|
||
`Finaler Index\n(${histGrade}, Kat.${histCat})\n€/t`,
|
||
"MoM Δ SS",
|
||
"MoM Δ LZ",
|
||
].map((h, i) => (
|
||
<th
|
||
key={i}
|
||
style={{
|
||
padding:
|
||
"8px 10px",
|
||
textAlign:
|
||
i ===
|
||
0
|
||
? "left"
|
||
: "right",
|
||
color: TH,
|
||
fontWeight: 600,
|
||
fontSize:
|
||
"0.58rem",
|
||
letterSpacing:
|
||
"0.07em",
|
||
textTransform:
|
||
"uppercase",
|
||
whiteSpace:
|
||
"pre-line",
|
||
fontFamily:
|
||
"inherit",
|
||
background:
|
||
S1,
|
||
}}
|
||
>
|
||
{h}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{histComputed.map(
|
||
(d, idx) => {
|
||
const prev =
|
||
idx > 0
|
||
? histComputed[
|
||
idx -
|
||
1
|
||
]
|
||
: null;
|
||
const dSS =
|
||
prev &&
|
||
d.ss !=
|
||
null &&
|
||
prev.ss !=
|
||
null
|
||
? d.ss -
|
||
prev.ss
|
||
: null;
|
||
const dLZ =
|
||
prev &&
|
||
d.lz !=
|
||
null &&
|
||
prev.lz !=
|
||
null
|
||
? d.lz -
|
||
prev.lz
|
||
: null;
|
||
const isNewYear =
|
||
idx === 0 ||
|
||
d.y !==
|
||
histComputed[
|
||
idx -
|
||
1
|
||
].y;
|
||
return (
|
||
<tr
|
||
key={
|
||
d.m
|
||
}
|
||
style={{
|
||
borderBottom: `1px solid ${BD}`,
|
||
background:
|
||
isNewYear
|
||
? `${R}08`
|
||
: idx %
|
||
2 ===
|
||
0
|
||
? S2
|
||
: "transparent",
|
||
}}
|
||
>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"8px 10px",
|
||
fontWeight:
|
||
isNewYear
|
||
? 700
|
||
: 400,
|
||
color: isNewYear
|
||
? TF
|
||
: TM,
|
||
whiteSpace:
|
||
"nowrap",
|
||
}}
|
||
>
|
||
{isNewYear && (
|
||
<span
|
||
style={{
|
||
color: R,
|
||
marginRight: 6,
|
||
fontWeight: 700,
|
||
}}
|
||
>
|
||
{
|
||
d.y
|
||
}
|
||
</span>
|
||
)}
|
||
{d.m.replace(
|
||
` ${d.y}`,
|
||
"",
|
||
)}
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"8px 10px",
|
||
textAlign:
|
||
"right",
|
||
color: TM,
|
||
}}
|
||
>
|
||
{fmt(
|
||
d.ss,
|
||
)}
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"8px 10px",
|
||
textAlign:
|
||
"right",
|
||
color: TM,
|
||
}}
|
||
>
|
||
{fmt(
|
||
d.lz,
|
||
)}
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"8px 10px",
|
||
textAlign:
|
||
"right",
|
||
color: "#7c3aed",
|
||
fontWeight: 500,
|
||
}}
|
||
>
|
||
{fmt(
|
||
d.indexVorLZ,
|
||
)}
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"8px 10px",
|
||
textAlign:
|
||
"right",
|
||
}}
|
||
>
|
||
{d.finalIdx !=
|
||
null ? (
|
||
<span
|
||
style={{
|
||
background:
|
||
R,
|
||
color: "#fff",
|
||
fontWeight: 700,
|
||
padding:
|
||
"2px 8px",
|
||
fontSize:
|
||
"0.78rem",
|
||
}}
|
||
>
|
||
{fmt(
|
||
d.finalIdx,
|
||
)}
|
||
</span>
|
||
) : (
|
||
<span
|
||
style={{
|
||
color: TH,
|
||
}}
|
||
>
|
||
—
|
||
</span>
|
||
)}
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"8px 10px",
|
||
textAlign:
|
||
"right",
|
||
fontSize:
|
||
"0.66rem",
|
||
color:
|
||
dSS ==
|
||
null
|
||
? "transparent"
|
||
: dSS >
|
||
0
|
||
? "#16a34a"
|
||
: dSS <
|
||
0
|
||
? "#dc2626"
|
||
: TH,
|
||
}}
|
||
>
|
||
{dSS !=
|
||
null
|
||
? (dSS >
|
||
0
|
||
? "+"
|
||
: "") +
|
||
fmt(
|
||
dSS,
|
||
)
|
||
: "—"}
|
||
</td>
|
||
<td
|
||
style={{
|
||
padding:
|
||
"8px 10px",
|
||
textAlign:
|
||
"right",
|
||
fontSize:
|
||
"0.66rem",
|
||
color:
|
||
dLZ ==
|
||
null
|
||
? "transparent"
|
||
: dLZ >
|
||
0
|
||
? "#16a34a"
|
||
: dLZ <
|
||
0
|
||
? "#dc2626"
|
||
: TH,
|
||
}}
|
||
>
|
||
{dLZ !=
|
||
null
|
||
? (dLZ >
|
||
0
|
||
? "+"
|
||
: "") +
|
||
fmt(
|
||
dLZ,
|
||
)
|
||
: "—"}
|
||
</td>
|
||
</tr>
|
||
);
|
||
},
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* ──── METHODIK ──── */}
|
||
{activeTab === "methodology" && (
|
||
<div style={{ display: "grid", gap: 14 }}>
|
||
<div style={card}>
|
||
<h3 style={secH}>
|
||
Preiskomponenten — Erläuterung
|
||
</h3>
|
||
{[
|
||
{
|
||
num: "01",
|
||
title: "Basis Edelstahlpreis",
|
||
text: "Öffentlich zugänglicher Referenzpreis für Cold Rolled Stainless Steel (CRC), kaltgewalzt. Wird quartalsweise mit den Supplier-Konditionen abgeglichen.",
|
||
},
|
||
{
|
||
num: "02",
|
||
title: "Produktionsaufschlag",
|
||
text: "Fixer Aufschlag je Wandstärken-Kategorie. Je dünner der Strip, desto höher der Aufschlag.",
|
||
extra: "Kat. A (0,15–0,19 mm): 1.293 €/t · Kat. B (0,20–0,29 mm): 1.063 €/t · Kat. C (0,30–0,40 mm): 763 €/t",
|
||
},
|
||
{
|
||
num: "03",
|
||
title: "Deviance-Aufschlag",
|
||
text: "Prozentualer Puffer auf (Basis + Produktion). Deckt Modell-Unschärfen, Toleranzschwankungen und Yield-Variabilität ab. Empfehlung: 6% Standard, bis 9% bei 0,15–0,18 mm.",
|
||
},
|
||
{
|
||
num: "04",
|
||
title: "Legierungszuschlag",
|
||
text: "Monatlich veröffentlichter Aufschlag von Outokumpu für Precision Strip (€/t). Basiert auf LME Nickel, Chrom und Ferromolybdän. Historische Daten: Outokumpu Precision Strip (Jan 2021 – Apr 2026). Nickel-Wertanteil ca. 52–58%.",
|
||
},
|
||
].map((s) => (
|
||
<div
|
||
key={s.num}
|
||
style={{
|
||
display: "flex",
|
||
gap: 16,
|
||
padding: "14px 0",
|
||
borderBottom: `1px solid ${BD}`,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: "1.6rem",
|
||
fontWeight: 700,
|
||
color: `${R}28`,
|
||
width: 36,
|
||
flexShrink: 0,
|
||
lineHeight: 1,
|
||
}}
|
||
>
|
||
{s.num}
|
||
</div>
|
||
<div>
|
||
<div
|
||
style={{
|
||
color: R,
|
||
fontWeight: 700,
|
||
marginBottom: 5,
|
||
fontSize:
|
||
"0.78rem",
|
||
letterSpacing:
|
||
"0.06em",
|
||
textTransform:
|
||
"uppercase",
|
||
}}
|
||
>
|
||
{s.title}
|
||
</div>
|
||
<div
|
||
style={{
|
||
color: TH,
|
||
fontSize:
|
||
"0.72rem",
|
||
lineHeight: 1.75,
|
||
}}
|
||
>
|
||
{s.text}
|
||
</div>
|
||
{s.extra && (
|
||
<div
|
||
style={{
|
||
color: TH,
|
||
fontSize:
|
||
"0.64rem",
|
||
marginTop: 8,
|
||
background:
|
||
S2,
|
||
padding:
|
||
"5px 12px",
|
||
borderLeft: `2px solid ${BD}`,
|
||
}}
|
||
>
|
||
{s.extra}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* ──── LEGIERUNGSZUSCHLAG ──── */}
|
||
{activeTab === "surcharge" && (
|
||
<div style={{ display: "grid", gap: 14 }}>
|
||
<div style={card}>
|
||
<h3 style={secH}>
|
||
Outokumpu — Monatliche
|
||
Aktualisierung
|
||
</h3>
|
||
<div
|
||
style={{
|
||
color: TH,
|
||
fontSize: "0.7rem",
|
||
marginBottom: 18,
|
||
lineHeight: 1.8,
|
||
}}
|
||
>
|
||
Quelle:{" "}
|
||
<span style={{ color: R }}>
|
||
outokumpu.com → Surcharges →
|
||
Precision Strip Europe
|
||
</span>
|
||
<br />
|
||
Einheit: EUR / Tonne · Monatlich
|
||
am 1. Werktag veröffentlicht
|
||
</div>
|
||
<div
|
||
style={{
|
||
borderTop: `1px solid ${BD}`,
|
||
paddingTop: 16,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: "0.58rem",
|
||
color: R,
|
||
letterSpacing: "0.14em",
|
||
textTransform:
|
||
"uppercase",
|
||
marginBottom: 12,
|
||
}}
|
||
>
|
||
Aktuellen Monat eintragen →
|
||
wird sofort im Rechner
|
||
übernommen
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "grid",
|
||
gap: 8,
|
||
}}
|
||
>
|
||
{GRADES.map((g) => (
|
||
<div
|
||
key={g.id}
|
||
style={{
|
||
display: "grid",
|
||
gridTemplateColumns:
|
||
"1.8fr 1fr 1fr",
|
||
gap: 10,
|
||
alignItems:
|
||
"center",
|
||
padding:
|
||
"12px 14px",
|
||
background: S2,
|
||
borderLeft:
|
||
grade ===
|
||
g.id
|
||
? `3px solid ${R}`
|
||
: "3px solid transparent",
|
||
border: `1px solid ${BD}`,
|
||
}}
|
||
>
|
||
<div>
|
||
<div
|
||
style={{
|
||
color: TF,
|
||
fontWeight: 700,
|
||
fontSize:
|
||
"0.76rem",
|
||
}}
|
||
>
|
||
{g.label} ·{" "}
|
||
{g.en}
|
||
</div>
|
||
<div
|
||
style={{
|
||
color: TH,
|
||
fontSize:
|
||
"0.62rem",
|
||
}}
|
||
>
|
||
{
|
||
g.outokumpu
|
||
}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label
|
||
style={{
|
||
...lbl,
|
||
marginBottom: 4,
|
||
}}
|
||
>
|
||
Monat
|
||
</label>
|
||
<input
|
||
value={
|
||
(surcharges[
|
||
g.id
|
||
] || EMPTY_SURCHARGES[g.id]).month
|
||
}
|
||
onChange={(
|
||
e,
|
||
) =>
|
||
updSC(
|
||
g.id,
|
||
"month",
|
||
e
|
||
.target
|
||
.value,
|
||
)
|
||
}
|
||
style={{
|
||
...field,
|
||
fontSize:
|
||
"0.7rem",
|
||
}}
|
||
placeholder="z.B. Mai 2026"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label
|
||
style={{
|
||
...lbl,
|
||
marginBottom: 4,
|
||
}}
|
||
>
|
||
Zuschlag
|
||
(€/t)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
step="1"
|
||
value={
|
||
(surcharges[
|
||
g.id
|
||
] || EMPTY_SURCHARGES[g.id]).value
|
||
}
|
||
onChange={(
|
||
e,
|
||
) =>
|
||
updSC(
|
||
g.id,
|
||
"value",
|
||
parseFloat(
|
||
e
|
||
.target
|
||
.value,
|
||
) ||
|
||
0,
|
||
)
|
||
}
|
||
style={{
|
||
...field,
|
||
color: R,
|
||
borderColor: `${R}60`,
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 12,
|
||
flexWrap: "wrap",
|
||
marginTop: 14,
|
||
}}
|
||
>
|
||
<button
|
||
type="button"
|
||
onClick={saveData}
|
||
style={{
|
||
background: R,
|
||
color: "#fff",
|
||
border: "none",
|
||
padding: "9px 16px",
|
||
fontSize: "0.72rem",
|
||
fontWeight: 700,
|
||
cursor: "pointer",
|
||
letterSpacing: "0.06em",
|
||
textTransform: "uppercase",
|
||
}}
|
||
>
|
||
Werte in data.json speichern
|
||
</button>
|
||
<span
|
||
style={{
|
||
color: saveStatus.startsWith("Gespeichert") ? "#059669" : TH,
|
||
fontSize: "0.66rem",
|
||
lineHeight: 1.6,
|
||
}}
|
||
>
|
||
{saveStatus || "Speichert den aktuellen Monat auch im Verlauf."}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style={{
|
||
...card,
|
||
borderLeft: `3px solid ${R}`,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: "0.58rem",
|
||
color: R,
|
||
letterSpacing: "0.14em",
|
||
textTransform: "uppercase",
|
||
marginBottom: 12,
|
||
}}
|
||
>
|
||
Update-Prozess
|
||
</div>
|
||
<ol
|
||
style={{
|
||
color: TH,
|
||
fontSize: "0.72rem",
|
||
paddingLeft: 18,
|
||
lineHeight: 2.4,
|
||
margin: 0,
|
||
}}
|
||
>
|
||
<li>
|
||
Am 1. Werktag jeden Monats{" "}
|
||
<strong
|
||
style={{ color: TF }}
|
||
>
|
||
outokumpu.com/surcharges
|
||
</strong>{" "}
|
||
aufrufen
|
||
</li>
|
||
<li>
|
||
Werte für{" "}
|
||
<strong
|
||
style={{ color: TF }}
|
||
>
|
||
Precision Strip Europe
|
||
</strong>{" "}
|
||
ablesen (€/t)
|
||
</li>
|
||
<li>
|
||
Im Tab oben Monat + Zuschlag
|
||
für alle drei Güten
|
||
eintragen
|
||
</li>
|
||
<li>
|
||
Button
|
||
<strong style={{ color: TF }}>
|
||
{" "}Werte in data.json speichern{" "}
|
||
</strong>
|
||
klicken
|
||
</li>
|
||
<li>
|
||
Werte werden im Rechner-Tab
|
||
übernommen und im Verlauf
|
||
ergänzt/aktualisiert
|
||
</li>
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<div
|
||
style={{
|
||
borderTop: `1px solid ${BD}`,
|
||
padding: "12px 28px",
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
background: S1,
|
||
}}
|
||
>
|
||
<Logo h={14} />
|
||
<div
|
||
style={{
|
||
fontSize: "0.58rem",
|
||
color: TH,
|
||
letterSpacing: "0.1em",
|
||
}}
|
||
>
|
||
PRECISION STRIP · BASISPREIS INDEX · INTERN
|
||
· LEGIERUNGSZUSCHLAG SEPARAT
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||
root.render(<App />);
|
||
</script>
|
||
</body>
|
||
</html>
|