491 lines
13 KiB
JavaScript
491 lines
13 KiB
JavaScript
const stepCount = 6;
|
|
|
|
const catalogs = {
|
|
systems: {
|
|
mono_230: { name: "System 1-fazowy 230V", base: 159, meter: 89 },
|
|
tri_230: { name: "System 3-fazowy 230V", base: 249, meter: 129 },
|
|
magnetic_48: { name: "System magnetyczny 48V", base: 399, meter: 179 }
|
|
},
|
|
layouts: {
|
|
line: { name: "Układ liniowy", surcharge: 0 },
|
|
l_shape: { name: "Układ L", surcharge: 59 },
|
|
u_shape: { name: "Układ U", surcharge: 99 },
|
|
rectangle: { name: "Układ prostokątny", surcharge: 149 }
|
|
},
|
|
colors: {
|
|
black: { name: "Czarny mat", surcharge: 0, line: "#252627" },
|
|
white: { name: "Biały mat", surcharge: 0, line: "#f7f6f8" },
|
|
champagne: { name: "Champagne", surcharge: 40, line: "#d7bd8b" }
|
|
},
|
|
temperatures: {
|
|
"3000": "3000K ciepła",
|
|
"4000": "4000K neutralna",
|
|
cct: "CCT regulowana"
|
|
}
|
|
};
|
|
|
|
const state = {
|
|
step: 1,
|
|
system: null,
|
|
layout: null,
|
|
trackLength: 4,
|
|
roomHeight: 2.8,
|
|
trackColor: null,
|
|
temperature: null,
|
|
fixtures: {},
|
|
accessories: new Set()
|
|
};
|
|
|
|
const fixtureGrid = document.getElementById("fixtureGrid");
|
|
const accessoryList = document.getElementById("accessoryList");
|
|
const summaryList = document.getElementById("summaryList");
|
|
const summaryTotal = document.getElementById("summaryTotal");
|
|
const finalSummary = document.getElementById("finalSummary");
|
|
const previewCanvas = document.getElementById("previewCanvas");
|
|
const trackLengthInput = document.getElementById("trackLength");
|
|
const trackLengthOut = document.getElementById("trackLengthOut");
|
|
const roomHeightInput = document.getElementById("roomHeight");
|
|
const roomHeightOut = document.getElementById("roomHeightOut");
|
|
const prevBtn = document.getElementById("prevBtn");
|
|
const nextBtn = document.getElementById("nextBtn");
|
|
const toast = document.getElementById("toast");
|
|
|
|
const fixtures = buildFixtures();
|
|
const accessories = buildAccessories();
|
|
|
|
init();
|
|
|
|
function init() {
|
|
renderFixtures();
|
|
renderAccessories();
|
|
bindBaseEvents();
|
|
syncInputs();
|
|
updateUI();
|
|
}
|
|
|
|
function buildFixtures() {
|
|
const names = [
|
|
"Reflektor Punto",
|
|
"Spot Slim Tube",
|
|
"Lampa liniowa Aero",
|
|
"Reflektor Move Pro",
|
|
"Tubus Mono Flex",
|
|
"Panel Linear Edge",
|
|
"Oprawa Twist Mini"
|
|
];
|
|
|
|
return names.slice(0, 6).map((name, i) => ({
|
|
id: `fx_${i + 1}`,
|
|
code: `LT-FX-${1200 + i}`,
|
|
name,
|
|
price: randomInt(85, 279)
|
|
}));
|
|
}
|
|
|
|
function buildAccessories() {
|
|
const names = [
|
|
"Zasilacz końcowy",
|
|
"Łącznik prosty",
|
|
"Łącznik narożny L",
|
|
"Maskownica zasilania",
|
|
"Pilot CCT",
|
|
"Zawiesie sufitowe 1 m"
|
|
];
|
|
|
|
return names.map((name, i) => ({
|
|
id: `acc_${i + 1}`,
|
|
name,
|
|
price: randomInt(24, 119)
|
|
}));
|
|
}
|
|
|
|
function bindBaseEvents() {
|
|
document.addEventListener("change", (event) => {
|
|
const target = event.target;
|
|
|
|
if (target.name === "system") {
|
|
state.system = target.value;
|
|
}
|
|
|
|
if (target.name === "layout") {
|
|
state.layout = target.value;
|
|
}
|
|
|
|
if (target.name === "trackColor") {
|
|
state.trackColor = target.value;
|
|
}
|
|
|
|
if (target.name === "temperature") {
|
|
state.temperature = target.value;
|
|
}
|
|
|
|
if (target.matches("[data-accessory]")) {
|
|
if (target.checked) {
|
|
state.accessories.add(target.value);
|
|
} else {
|
|
state.accessories.delete(target.value);
|
|
}
|
|
}
|
|
|
|
updateUI();
|
|
});
|
|
|
|
fixtureGrid.addEventListener("click", (event) => {
|
|
const button = event.target.closest("button[data-fixture-id]");
|
|
if (!button) {
|
|
return;
|
|
}
|
|
|
|
const id = button.dataset.fixtureId;
|
|
const action = button.dataset.action;
|
|
const currentQty = state.fixtures[id] || 0;
|
|
const nextQty = action === "inc" ? currentQty + 1 : Math.max(0, currentQty - 1);
|
|
|
|
state.fixtures[id] = nextQty;
|
|
updateUI();
|
|
});
|
|
|
|
trackLengthInput.addEventListener("input", () => {
|
|
state.trackLength = Number(trackLengthInput.value);
|
|
syncInputs();
|
|
updateUI();
|
|
});
|
|
|
|
roomHeightInput.addEventListener("input", () => {
|
|
const clamped = Math.min(4.5, Math.max(2.2, Number(roomHeightInput.value || 2.8)));
|
|
state.roomHeight = Number(clamped.toFixed(1));
|
|
syncInputs();
|
|
});
|
|
|
|
prevBtn.addEventListener("click", () => {
|
|
if (state.step > 1) {
|
|
state.step -= 1;
|
|
updateUI();
|
|
}
|
|
});
|
|
|
|
nextBtn.addEventListener("click", () => {
|
|
if (state.step < stepCount) {
|
|
const valid = validateCurrentStep();
|
|
if (!valid.ok) {
|
|
showToast(valid.message);
|
|
return;
|
|
}
|
|
|
|
state.step += 1;
|
|
updateUI();
|
|
return;
|
|
}
|
|
|
|
showToast("Makieta: zestaw został przykładowo dodany do koszyka.");
|
|
});
|
|
|
|
document.querySelectorAll("[data-goto-step]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const goto = Number(button.dataset.gotoStep);
|
|
if (goto <= state.step || goto === state.step + 1) {
|
|
if (goto > state.step) {
|
|
const valid = validateCurrentStep();
|
|
if (!valid.ok) {
|
|
showToast(valid.message);
|
|
return;
|
|
}
|
|
}
|
|
state.step = goto;
|
|
updateUI();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderFixtures() {
|
|
fixtureGrid.innerHTML = fixtures
|
|
.map(
|
|
(fixture) => `
|
|
<article class="product-card">
|
|
<div class="product-code">${fixture.code}</div>
|
|
<div class="product-name">${fixture.name}</div>
|
|
<div class="product-meta">
|
|
<div class="price">${formatPLN(fixture.price)}</div>
|
|
<div class="qty">
|
|
<button type="button" data-fixture-id="${fixture.id}" data-action="dec">-</button>
|
|
<span>${state.fixtures[fixture.id] || 0}</span>
|
|
<button type="button" data-fixture-id="${fixture.id}" data-action="inc">+</button>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
`
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function renderAccessories() {
|
|
accessoryList.innerHTML = accessories
|
|
.map(
|
|
(item) => `
|
|
<article class="accessory-item">
|
|
<label>
|
|
<input type="checkbox" data-accessory value="${item.id}">
|
|
<span>${item.name}</span>
|
|
</label>
|
|
<strong>${formatPLN(item.price)}</strong>
|
|
</article>
|
|
`
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function updateUI() {
|
|
updateStepPanels();
|
|
updateStepButtons();
|
|
updateActions();
|
|
renderFixtures();
|
|
refreshAccessoryChecks();
|
|
renderSummary();
|
|
renderFinalSummary();
|
|
renderPreview();
|
|
}
|
|
|
|
function updateStepPanels() {
|
|
document.querySelectorAll(".step-panel").forEach((panel) => {
|
|
panel.classList.toggle("is-active", Number(panel.dataset.step) === state.step);
|
|
});
|
|
}
|
|
|
|
function updateStepButtons() {
|
|
document.querySelectorAll(".step-item").forEach((button) => {
|
|
const step = Number(button.dataset.gotoStep);
|
|
button.classList.toggle("is-active", step === state.step);
|
|
button.classList.toggle("is-done", step < state.step);
|
|
});
|
|
}
|
|
|
|
function updateActions() {
|
|
prevBtn.disabled = state.step === 1;
|
|
nextBtn.textContent = state.step === stepCount ? "Dodaj do koszyka" : "Dalej";
|
|
}
|
|
|
|
function renderSummary() {
|
|
const breakdown = getBreakdown();
|
|
summaryList.innerHTML = breakdown.items
|
|
.map((item) => `<li><span>${item.label}</span><strong>${formatPLN(item.value)}</strong></li>`)
|
|
.join("");
|
|
summaryTotal.textContent = formatPLN(breakdown.total);
|
|
}
|
|
|
|
function renderFinalSummary() {
|
|
const breakdown = getBreakdown();
|
|
finalSummary.innerHTML = `
|
|
<ul>
|
|
${breakdown.items
|
|
.map((item) => `<li><span>${item.label}</span><strong>${formatPLN(item.value)}</strong></li>`)
|
|
.join("")}
|
|
<li><span><strong>Razem brutto</strong></span><strong>${formatPLN(breakdown.total)}</strong></li>
|
|
</ul>
|
|
`;
|
|
}
|
|
|
|
function refreshAccessoryChecks() {
|
|
document.querySelectorAll("[data-accessory]").forEach((input) => {
|
|
input.checked = state.accessories.has(input.value);
|
|
});
|
|
}
|
|
|
|
function getBreakdown() {
|
|
const items = [];
|
|
let total = 0;
|
|
|
|
if (state.system) {
|
|
const system = catalogs.systems[state.system];
|
|
const value = system.base;
|
|
items.push({ label: system.name, value });
|
|
total += value;
|
|
}
|
|
|
|
if (state.layout) {
|
|
const layout = catalogs.layouts[state.layout];
|
|
if (layout.surcharge > 0) {
|
|
items.push({ label: layout.name, value: layout.surcharge });
|
|
total += layout.surcharge;
|
|
}
|
|
}
|
|
|
|
if (state.system) {
|
|
const system = catalogs.systems[state.system];
|
|
const trackValue = Math.round(state.trackLength * system.meter);
|
|
items.push({ label: `Szyny ${state.trackLength.toFixed(1)} m`, value: trackValue });
|
|
total += trackValue;
|
|
}
|
|
|
|
if (state.trackColor) {
|
|
const color = catalogs.colors[state.trackColor];
|
|
if (color.surcharge > 0) {
|
|
items.push({ label: `Wykończenie ${color.name}`, value: color.surcharge });
|
|
total += color.surcharge;
|
|
}
|
|
}
|
|
|
|
fixtures.forEach((fixture) => {
|
|
const qty = state.fixtures[fixture.id] || 0;
|
|
if (qty > 0) {
|
|
const value = qty * fixture.price;
|
|
items.push({ label: `${fixture.name} x${qty}`, value });
|
|
total += value;
|
|
}
|
|
});
|
|
|
|
accessories.forEach((acc) => {
|
|
if (state.accessories.has(acc.id)) {
|
|
items.push({ label: acc.name, value: acc.price });
|
|
total += acc.price;
|
|
}
|
|
});
|
|
|
|
if (items.length === 0) {
|
|
items.push({ label: "Brak wybranych pozycji", value: 0 });
|
|
}
|
|
|
|
return { items, total };
|
|
}
|
|
|
|
function renderPreview() {
|
|
const colorToken = state.trackColor ? catalogs.colors[state.trackColor].line : "#f7f6f8";
|
|
const spotCount = Math.max(1, Object.values(state.fixtures).reduce((sum, qty) => sum + qty, 0));
|
|
const points = getPreviewPoints(state.layout || "line");
|
|
|
|
const circles = Array.from({ length: spotCount }, (_, i) => {
|
|
const point = points[i % points.length];
|
|
const tone = getPreviewLightTone(state.temperature, i);
|
|
return `<circle cx="${point.x}" cy="${point.y}" r="5.2" fill="${tone.fill}" stroke="${tone.stroke}" stroke-width="2" />`;
|
|
}).join("");
|
|
|
|
const lines = getPreviewPath(state.layout || "line", colorToken);
|
|
|
|
previewCanvas.innerHTML = `
|
|
<svg viewBox="0 0 360 170" width="100%" height="170" aria-hidden="true">
|
|
${lines}
|
|
${circles}
|
|
</svg>
|
|
`;
|
|
}
|
|
|
|
function getPreviewPath(layout, color) {
|
|
const stroke = `<g stroke="${color}" stroke-width="9" fill="none" stroke-linecap="round" stroke-linejoin="round">`;
|
|
if (layout === "l_shape") {
|
|
return `${stroke}<path d="M48 38 L48 130 L280 130" /></g>`;
|
|
}
|
|
if (layout === "u_shape") {
|
|
return `${stroke}<path d="M45 38 L45 130 L300 130 L300 38" /></g>`;
|
|
}
|
|
if (layout === "rectangle") {
|
|
return `${stroke}<path d="M60 40 L300 40 L300 130 L60 130 Z" /></g>`;
|
|
}
|
|
return `${stroke}<path d="M45 86 L315 86" /></g>`;
|
|
}
|
|
|
|
function getPreviewPoints(layout) {
|
|
if (layout === "l_shape") {
|
|
return [
|
|
{ x: 48, y: 54 },
|
|
{ x: 48, y: 82 },
|
|
{ x: 48, y: 112 },
|
|
{ x: 98, y: 130 },
|
|
{ x: 150, y: 130 },
|
|
{ x: 205, y: 130 },
|
|
{ x: 260, y: 130 }
|
|
];
|
|
}
|
|
if (layout === "u_shape") {
|
|
return [
|
|
{ x: 45, y: 55 },
|
|
{ x: 45, y: 100 },
|
|
{ x: 95, y: 130 },
|
|
{ x: 145, y: 130 },
|
|
{ x: 195, y: 130 },
|
|
{ x: 245, y: 130 },
|
|
{ x: 300, y: 102 },
|
|
{ x: 300, y: 58 }
|
|
];
|
|
}
|
|
if (layout === "rectangle") {
|
|
return [
|
|
{ x: 76, y: 40 },
|
|
{ x: 128, y: 40 },
|
|
{ x: 196, y: 40 },
|
|
{ x: 262, y: 40 },
|
|
{ x: 300, y: 76 },
|
|
{ x: 300, y: 114 },
|
|
{ x: 250, y: 130 },
|
|
{ x: 175, y: 130 },
|
|
{ x: 100, y: 130 },
|
|
{ x: 60, y: 96 }
|
|
];
|
|
}
|
|
return [
|
|
{ x: 64, y: 86 },
|
|
{ x: 104, y: 86 },
|
|
{ x: 146, y: 86 },
|
|
{ x: 188, y: 86 },
|
|
{ x: 230, y: 86 },
|
|
{ x: 272, y: 86 },
|
|
{ x: 312, y: 86 }
|
|
];
|
|
}
|
|
|
|
function getPreviewLightTone(temperature, index) {
|
|
if (temperature === "3000") {
|
|
return { fill: "#ffb766", stroke: "#ffd4a2" };
|
|
}
|
|
|
|
if (temperature === "4000") {
|
|
return { fill: "#fff3d2", stroke: "#fff9ea" };
|
|
}
|
|
|
|
if (temperature === "cct") {
|
|
return index % 2 === 0
|
|
? { fill: "#ffbc77", stroke: "#ffe0b5" }
|
|
: { fill: "#c8e8ff", stroke: "#e9f6ff" };
|
|
}
|
|
|
|
return { fill: "#f7614d", stroke: "#ffd2cc" };
|
|
}
|
|
|
|
function validateCurrentStep() {
|
|
if (state.step === 1 && !state.system) {
|
|
return { ok: false, message: "Wybierz system szynowy, aby przejść dalej." };
|
|
}
|
|
|
|
if (state.step === 2 && !state.layout) {
|
|
return { ok: false, message: "Wybierz kształt konfiguracji." };
|
|
}
|
|
|
|
if (state.step === 3 && (!state.trackColor || !state.temperature)) {
|
|
return { ok: false, message: "Uzupełnij kolor szyny i temperaturę barwową." };
|
|
}
|
|
|
|
return { ok: true };
|
|
}
|
|
|
|
function syncInputs() {
|
|
trackLengthOut.textContent = state.trackLength.toFixed(1);
|
|
roomHeightInput.value = state.roomHeight.toFixed(1);
|
|
roomHeightOut.textContent = state.roomHeight.toFixed(1);
|
|
}
|
|
|
|
function showToast(text) {
|
|
toast.textContent = text;
|
|
toast.classList.add("is-visible");
|
|
clearTimeout(showToast.timer);
|
|
showToast.timer = setTimeout(() => {
|
|
toast.classList.remove("is-visible");
|
|
}, 2100);
|
|
}
|
|
|
|
function randomInt(min, max) {
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
}
|
|
|
|
function formatPLN(value) {
|
|
return `${value.toLocaleString("pl-PL")} zł`;
|
|
}
|