Files
BPI/index.html

2897 lines
173 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="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,150,19 mm", val: 1293 },
B: { label: "0,200,29 mm", val: 1063 },
C: { label: "0,300,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 getSaveMonth(surcharges, editMode, editMonth) {
if (editMode) return editMonth;
return (
surcharges["316"]?.month ||
surcharges["304"]?.month ||
surcharges["444"]?.month ||
""
);
}
function cloneSurcharges(surcharges) {
return {
"304": {
...(surcharges["304"] || EMPTY_SURCHARGES["304"]),
},
"316": {
...(surcharges["316"] || EMPTY_SURCHARGES["316"]),
},
"444": {
...(surcharges["444"] || EMPTY_SURCHARGES["444"]),
},
};
}
function shouldUseEditedCurrentValues(
editMode,
editMonth,
editSnapshot,
) {
if (!editMode || !editSnapshot) return true;
return (
getSaveMonth(editSnapshot.surcharges, false, "") ===
editMonth
);
}
function buildSaveEntry(surcharges, baseSS, saveMonth) {
const entry = buildHistoryEntry(surcharges, baseSS);
return {
...entry,
m: saveMonth,
y: monthToYear(saveMonth),
};
}
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,
});
const [editMode, setEditMode] = useState(false);
const [editMonth, setEditMonth] = useState("");
const [editSnapshot, setEditSnapshot] = useState(null);
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 openEditMode = (month) => {
const entry = history.find((d) => d.m === month);
if (!entry) return;
if (!editSnapshot) {
setEditSnapshot({
baseSS: Number.isFinite(Number(baseSS))
? Number(baseSS)
: 0,
surcharges: cloneSurcharges(surcharges),
});
}
setEditMode(true);
setEditMonth(month);
setBaseSS(entry.ss != null ? entry.ss : 0);
setSurcharges({
"304": { month, value: entry.lz304 || 0 },
"316": { month, value: entry.lz316 || 0 },
"444": { month, value: entry.lz444 || 0 },
});
};
const finishEditMode = () => {
setEditMode(false);
setEditMonth("");
setEditSnapshot(null);
};
const startNewEntryMode = () => {
if (editSnapshot) {
setBaseSS(editSnapshot.baseSS);
setSurcharges(editSnapshot.surcharges);
}
finishEditMode();
};
const handleSaveData = async () => {
const saveMonth = getSaveMonth(
surcharges,
editMode,
editMonth,
);
const entry = buildSaveEntry(surcharges, baseSS, saveMonth);
const nextHistory = upsertHistory(history, entry);
const normalizedBaseSS = Number.isFinite(Number(baseSS))
? Number(baseSS)
: 0;
const useEditedCurrentValues = shouldUseEditedCurrentValues(
editMode,
editMonth,
editSnapshot,
);
const currentBaseSS = useEditedCurrentValues
? normalizedBaseSS
: editSnapshot.baseSS;
const currentSurcharges = useEditedCurrentValues
? surcharges
: editSnapshot.surcharges;
const nextData = {
surcharges: currentSurcharges,
history: nextHistory,
baseSS: currentBaseSS,
};
setBaseSS(currentBaseSS);
setSurcharges(currentSurcharges);
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.",
);
}
finishEditMode();
};
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"],
];
const historyMonthOptions = [...history].reverse();
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,150,19 mm): 1.293 €/t · Kat. B (0,200,29 mm): 1.063 €/t · Kat. C (0,300,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,150,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. 5258%.",
},
].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={{
display: "grid",
gridTemplateColumns:
"1.2fr 1fr",
gap: 12,
marginBottom: 12,
}}
>
<div
style={{
background: S2,
border: `1px solid ${BD}`,
padding: "10px 12px",
}}
>
<label style={lbl}>
Bestehenden Eintrag bearbeiten
</label>
<select
value={
editMode
? editMonth
: ""
}
onChange={(e) => {
const month =
e.target
.value;
if (!month) {
startNewEntryMode();
return;
}
openEditMode(
month,
);
}}
style={field}
>
<option value="">
Neuer Eintrag
</option>
{historyMonthOptions.map(
(entry) => (
<option
key={
entry.m
}
value={
entry.m
}
>
{
entry.m
}
</option>
),
)}
</select>
<div style={hint}>
Bestehende Monate
korrigieren oder neue
Werte erfassen
</div>
</div>
<div
style={{
background: S2,
border: `1px solid ${BD}`,
padding: "10px 12px",
}}
>
<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}>
Wird in Verlauf als
Basispreis (ss)
gespeichert
</div>
</div>
</div>
{editMode && (
<div
style={{
display: "flex",
justifyContent:
"space-between",
alignItems:
"center",
gap: 12,
flexWrap: "wrap",
marginBottom: 12,
background: `${R}10`,
borderLeft: `3px solid ${R}`,
padding: "10px 12px",
}}
>
<div
style={{
color: TF,
fontSize:
"0.72rem",
fontWeight: 700,
}}
>
Bearbeitung: {editMonth}
</div>
<button
type="button"
onClick={
startNewEntryMode
}
style={{
background: "transparent",
border: `1px solid ${BD}`,
color: TH,
padding:
"6px 10px",
cursor: "pointer",
fontSize:
"0.64rem",
letterSpacing:
"0.08em",
textTransform:
"uppercase",
}}
>
Neuen Eintrag erfassen
</button>
</div>
)}
<div
style={{
fontSize: "0.58rem",
color: R,
letterSpacing: "0.14em",
textTransform:
"uppercase",
marginBottom: 12,
}}
>
{editMode
? "Korrigierte Monatswerte speichern"
: "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={handleSaveData}
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 Basispreis,
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>