diff --git a/components/kalkulator/KalkulatorClient.tsx b/components/kalkulator/KalkulatorClient.tsx index 3e8a81a..d6f45d5 100644 --- a/components/kalkulator/KalkulatorClient.tsx +++ b/components/kalkulator/KalkulatorClient.tsx @@ -1,16 +1,43 @@ 'use client'; -import { useState, useMemo } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { ArrowRight, ArrowLeft, Check, Thermometer, Home, Zap, ShoppingCart, Phone, RotateCcw } from 'lucide-react'; import { Product } from '@/content/products'; import { companyDetails } from '@/content/company'; +import { ProductCard } from '@/components/shop/ProductCard'; interface Props { products: Product[]; } +// Hook do zapisywania stanu w sessionStorage +function useSessionState(key: string, initialValue: T) { + const [state, setState] = useState(initialValue); + + useEffect(() => { + try { + const item = window.sessionStorage.getItem(key); + if (item) { + setState(JSON.parse(item)); + } + } catch (error) {} + }, [key]); + + const setValue = (value: T | ((val: T) => T)) => { + try { + const valueToStore = value instanceof Function ? value(state) : value; + setState(valueToStore); + if (typeof window !== 'undefined') { + window.sessionStorage.setItem(key, JSON.stringify(valueToStore)); + } + } catch (error) {} + }; + + return [state, setValue] as const; +} + // Oblicz wymaganą moc chłodniczą w kW function calculatePower(m2: number, floor: string, sun: string, building: string): number { let power = m2 * 0.1; // baza: 1 kW na 10 m² @@ -32,30 +59,34 @@ function calculatePower(m2: number, floor: string, sun: string, building: string } // Szacunkowy koszt montażu -function estimateInstall(pipes: string, rooms: number, installMethod: string): { min: number; max: number } { +function estimateInstall(pipes: string, rooms: number, installMethod: string, outdoorUnitLocation: string, hasSocket: string): { min: number; max: number } { const base = rooms === 1 ? 1400 : rooms === 2 ? 2400 : rooms === 3 ? 3200 : 3800; // Przybliżona liczba dodatkowych metrów (powyżej 3m) const extraMeters = pipes === 'short' ? 0 : pipes === 'medium' ? 3 : pipes === 'long' ? 7 : 15; const methodPrice = installMethod === 'bruzda' ? 90 : 80; // zł za metr // Zostawmy bazowe koszty rur miedzianych (np. miedź, kable) i dodajmy koszt bruzdy/korytka const pipeBaseCost = pipes === 'short' ? 0 : pipes === 'medium' ? 200 : pipes === 'long' ? 500 : 1000; - const pipeExtra = pipeBaseCost + (extraMeters * methodPrice); + // Elewacja: 400 zł (pompka) + ok. 600 zł (1h podnośnika jako bufor) + const facadeCost = outdoorUnitLocation === 'facade' ? 1000 : 0; + // Brak gniazdka: dodatkowy koszt ok 200 zł za dociągnięcie zasilania + const socketCost = hasSocket !== 'yes' ? 200 : 0; + const pipeExtra = pipeBaseCost + (extraMeters * methodPrice) + facadeCost + socketCost; return { min: base + pipeExtra, max: base + pipeExtra + 600 }; } -const STEPS = ['Typ', 'Pomieszczenie', 'Instalacja', 'Budżet'] as const; +const STEPS = ['Typ', 'Pomieszczenie', 'Instalacja', 'Budżet', 'Dobór'] as const; export function KalkulatorClient({ products }: Props) { - const [deviceType, setDeviceType] = useState<'klimatyzacja' | 'pompa'>('klimatyzacja'); + const [deviceType, setDeviceType] = useSessionState<'klimatyzacja' | 'pompa'>('calc_deviceType', 'klimatyzacja'); const [pumpFormSent, setPumpFormSent] = useState(false); const [pumpForm, setPumpForm] = useState({ name: '', phone: '', city: '', message: '' }); - const [step, setStep] = useState(0); - const [building, setBuilding] = useState('apartment'); - const [pomieszczenia, setPomieszczenia] = useState<{ nazwa: string; m2: number }[]>([ + const [step, setStep] = useSessionState('calc_step', 0); + const [building, setBuilding] = useSessionState('calc_building', 'apartment'); + const [pomieszczenia, setPomieszczenia] = useSessionState<{ nazwa: string; m2: number }[]>('calc_pomieszczenia', [ { nazwa: 'Pokój 1', m2: 20 } ]); - const [floor, setFloor] = useState('middle'); // ground | middle | top - const [sun, setSun] = useState('south'); // north | east | south | west + const [floor, setFloor] = useSessionState('calc_floor', 'middle'); // ground | middle | top + const [sun, setSun] = useSessionState('calc_sun', 'south'); // north | east | south | west const addRoom = () => { if (pomieszczenia.length < 5) { @@ -72,11 +103,16 @@ export function KalkulatorClient({ products }: Props) { const updateRoom = (i: number, m2: number) => { setPomieszczenia(prev => prev.map((r, idx) => idx === i ? { ...r, m2 } : r)); }; - const [pipes, setPipes] = useState('medium'); // short(<3m) | medium(3-6m) | long(6-10m) | verylong(>10m) - const [installMethod, setInstallMethod] = useState<'korytko' | 'bruzda'>('korytko'); - const [budget, setBudget] = useState(8000); - const [showResults, setShowResults] = useState(false); - const [preferredBrand, setPreferredBrand] = useState(''); + const [pipes, setPipes] = useSessionState('calc_pipes', 'medium'); // short(<3m) | medium(3-6m) | long(6-10m) | verylong(>10m) + const [installMethod, setInstallMethod] = useSessionState<'korytko' | 'bruzda'>('calc_installMethod', 'korytko'); + const [outdoorUnitLocation, setOutdoorUnitLocation] = useSessionState<'balcony' | 'facade' | 'roof'>('calc_outdoorLocation', 'balcony'); + const [hasSocket, setHasSocket] = useSessionState<'yes' | 'no' | 'dont_know'>('calc_hasSocket', 'yes'); + const [budget, setBudget] = useSessionState('calc_budget', 8000); + const [preferredBrand, setPreferredBrand] = useSessionState('calc_brand', ''); + const [energyClass, setEnergyClass] = useSessionState('calc_energy', ''); + const [maxNoise, setMaxNoise] = useSessionState('calc_maxNoise', null); + const [maxNoiseOut, setMaxNoiseOut] = useSessionState('calc_maxNoiseOut', null); + const [page, setPage] = useSessionState('calc_page', 1); const totalM2 = pomieszczenia.reduce((sum, p) => sum + p.m2, 0); const rooms = pomieszczenia.length; @@ -85,12 +121,12 @@ export function KalkulatorClient({ products }: Props) { [pomieszczenia, floor, sun, building] ); const requiredPowerRounded = Math.round(requiredPower * 10) / 10; - const installCost = useMemo(() => estimateInstall(pipes, rooms, installMethod), [pipes, rooms, installMethod]); + const installCost = useMemo(() => estimateInstall(pipes, rooms, installMethod, outdoorUnitLocation, hasSocket), [pipes, rooms, installMethod, outdoorUnitLocation, hasSocket]); const category = rooms === 1 ? 'scienne' : 'multisplit'; const results = useMemo(() => { - if (!showResults || deviceType === 'pompa') return []; + if (step < 4 || deviceType === 'pompa') return []; const maxPrice = budget * 0.75; const powerMin = requiredPower * 0.6; @@ -102,6 +138,27 @@ export function KalkulatorClient({ products }: Props) { if (preferredBrand && p.brand !== preferredBrand) return false; + if (energyClass) { + const pClass = p.specs?.['Klasa energetyczna']; + if (!pClass || !pClass.includes(energyClass)) return false; + } + + if (maxNoise !== null) { + const noiseStr = p.specs?.['Hałas wewnątrz (dB)']; + if (!noiseStr) return false; + // Hałas jest zapisany np. "19–38" - bierzemy dolną granicę + const minNoise = parseInt(noiseStr.split('–')[0].trim()); + if (isNaN(minNoise) || minNoise > maxNoise) return false; + } + + if (maxNoiseOut !== null) { + const noiseOutStr = p.specs?.['Hałas zewnątrz (dB)']; + // Jeśli produktu nie ma tego parametru, odrzucamy, jeśli nałożono ścisły filtr + if (!noiseOutStr) return false; + const minNoiseOut = parseInt(noiseOutStr.split('–')[0].trim()); + if (isNaN(minNoiseOut) || minNoiseOut > maxNoiseOut) return false; + } + if (category === 'multisplit') { if (p.price !== null && p.price > maxPrice) return false; } else { @@ -119,18 +176,64 @@ export function KalkulatorClient({ products }: Props) { }); return filtered.length > 0 - ? filtered.slice(0, 6) + ? filtered : products .filter(p => p.category === category && p.price !== null) - .sort((a, b) => (a.price || 0) - (b.price || 0)) - .slice(0, 6); - }, [showResults, deviceType, products, requiredPower, category, budget, preferredBrand]); + .sort((a, b) => (a.price || 0) - (b.price || 0)); + }, [step, deviceType, products, requiredPower, category, budget, preferredBrand, energyClass, maxNoise, maxNoiseOut]); - const handleFinish = () => setShowResults(true); - const handleReset = () => { setShowResults(false); setStep(0); }; + const paginatedResults = useMemo(() => results.slice((page - 1) * 9, page * 9), [results, page]); + const totalPages = Math.ceil(results.length / 9); + + const getPageNumbers = () => { + const pages = []; + if (totalPages <= 7) { + for (let i = 1; i <= totalPages; i++) pages.push(i); + } else { + if (page <= 4) { + pages.push(1, 2, 3, 4, 5, '...', totalPages); + } else if (page >= totalPages - 3) { + pages.push(1, '...', totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages); + } else { + pages.push(1, '...', page - 1, page, page + 1, '...', totalPages); + } + } + return pages; + }; + + const handleFinish = () => { + setStep(4); + setPage(1); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + const handleReset = () => { + setStep(0); + setPage(1); + Object.keys(window.sessionStorage).forEach(key => { + if (key.startsWith('calc_')) window.sessionStorage.removeItem(key); + }); + setDeviceType('klimatyzacja'); + setBuilding('apartment'); + setPomieszczenia([{ nazwa: 'Pokój 1', m2: 20 }]); + setFloor('middle'); + setSun('south'); + setPipes('medium'); + setInstallMethod('korytko'); + setOutdoorUnitLocation('balcony'); + setHasSocket('yes'); + setBudget(8000); + setPreferredBrand(''); + setEnergyClass(''); + setMaxNoise(null); + setMaxNoiseOut(null); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; const handleNext = () => { setStep(s => s + 1); + setPage(1); + window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handlePumpSubmit = async () => { @@ -139,180 +242,9 @@ export function KalkulatorClient({ products }: Props) { setPumpFormSent(true); }; - if (showResults) { - if (deviceType === 'pompa') { - return ( -
-
-
-

- Wycena pompy ciepła -

-

- Każda instalacja pompy ciepła wymaga indywidualnego doboru urządzenia do specyfiki budynku (izolacja, instalacja grzewcza). - Skontaktuj się z nami, a przygotujemy dla Ciebie bezpłatną, spersonalizowaną ofertę. -

- - - -
- -
-
-
-
- ); - } - - return ( -
-
- {/* Podsumowanie */} -
-

- Dopasowane klimatyzatory -

-
- {[ - { label: 'Wymagana moc', value: `${requiredPowerRounded} kW` }, - { label: 'Powierzchnia', value: `${totalM2} m²` }, - { label: 'Typ instalacji', value: category === 'scienne' ? 'Split (1 pokój)' : `Multisplit (${rooms} pokoje)` }, - { label: 'Szac. montaż', value: `${installCost.min.toLocaleString('pl-PL')}–${installCost.max.toLocaleString('pl-PL')} zł` }, - ].map((s, i) => ( -
-

{s.label}

-

{s.value}

-
- ))} -
-
- - {results.length > 0 ? ( -
- {results.map(product => { - const mocChlodnicza = parseFloat( - String(product.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0] - ); - const seer = mocChlodnicza > 0 ? mocChlodnicza / (mocChlodnicza * 0.5) : 5; - const pobor = mocChlodnicza / 5; // szacowany pobór przy SEER~5 - const zuzycieMiesiac = (pobor * 8 * 30).toFixed(0); // 8h/dzień 30 dni - - return ( -
-
- {product.name} - {product.specs?.['Klasa energetyczna'] && ( - - {product.specs['Klasa energetyczna'].split('/')[0]} - - )} -
-
-

{product.brand}

-

- {product.name} -

-
- {product.specs?.['Moc chłodnicza'] && ( - - {product.specs['Moc chłodnicza']} kW - - )} - {product.specs?.['Hałas wewnątrz (dB)'] && ( - - {product.specs['Hałas wewnątrz (dB)']} dB - - )} -
- -

- ~{zuzycieMiesiac} kWh/miesiąc (8h/dzień) -

- -
-
-

- {product.price?.toLocaleString('pl-PL')} zł -

-

+ montaż od {installCost.min.toLocaleString('pl-PL')} zł

-
- - - -
-
-
- ); - })} -
- ) : ( -
-

- Nie znaleziono produktów dla podanych kryteriów. Spróbuj zwiększyć budżet lub zadzwoń do nas — dobierzemy ofertę indywidualnie. -

- - - -
- )} - -
-

⚠️ Ceny mają charakter orientacyjny

-

- Podane ceny urządzeń są cenami brutto (23% VAT) i mogą się różnić - w zależności od aktualnej dostępności i kursu walut. - Koszt montażu jest szacunkowy — ostateczna wycena zależy od specyfiki instalacji. -

-

- Zachęcamy do skorzystania z bezpłatnej i niezobowiązującej wyceny. - Możesz też przesłać nam zdjęcia pomieszczeń — dobierzemy optymalne rozwiązanie. -

-
- - - - - - -
-
- -
- - - - -
-
-
- ); - } - return (
-
+
{/* Header */}

@@ -629,6 +561,45 @@ export function KalkulatorClient({ products }: Props) {

+
+ +
+ {[ + { value: 'balcony', label: 'Zwykły balkon / taras / posadzka', sub: 'Standardowy montaż, najtańszy i najwygodniejszy w serwisowaniu' }, + { value: 'facade', label: 'Elewacja / balkon francuski', sub: 'Wymaga użycia podnośnika koszowego (ok. 600 zł/h) i pompki skroplin (+ ok. 400 zł)' }, + { value: 'roof', label: 'Dach budynku', sub: 'Wymaga odpowiedniego stelaża lub konstrukcji dachowej' }, + ].map(o => ( + + ))} +
+ {building === 'apartment' && outdoorUnitLocation !== 'balcony' && ( +
+ ⚠️ Pamiętaj: Montaż agregatu na elewacji lub dachu w bloku wielorodzinnym wymaga uzyskania pisemnej zgody spółdzielni, wspólnoty mieszkaniowej lub zarządcy budynku. +
+ )} +
+ +
+ +
+ {[ + { value: 'yes', label: 'Tak', sub: 'Mam gniazdko na balkonie lub wyprowadzony kabel' }, + { value: 'no', label: 'Nie', sub: 'Instalator musi dociągnąć prąd z wewnątrz (+ ok. 200 zł)' }, + { value: 'dont_know', label: 'Nie wiem / Do ustalenia', sub: 'Do ustalenia w trakcie wizji lokalnej' }, + ].map(o => ( + + ))} +
+
+
Szacowany koszt montażu dla Twojej konfiguracji: {installCost.min.toLocaleString('pl-PL')}–{installCost.max.toLocaleString('pl-PL')} zł @@ -739,6 +710,189 @@ export function KalkulatorClient({ products }: Props) { ))}
+ +
+ +
+ {['Bez preferencji', 'A+++', 'A++', 'A+'].map(c => ( + + ))} +
+
+ +
+ +
+ {[ + { label: 'Bez znaczenia', val: null }, + { label: 'Bardzo ciche (do 20 dB)', val: 20 }, + { label: 'Ciche (do 25 dB)', val: 25 }, + { label: 'Standardowe (do 30 dB)', val: 30 } + ].map(n => ( + + ))} +
+
+ +
+ +
+ {[ + { label: 'Bez znaczenia', val: null }, + { label: 'Bardzo ciche (do 45 dB)', val: 45 }, + { label: 'Ciche (do 50 dB)', val: 50 }, + { label: 'Standardowe (do 55 dB)', val: 55 } + ].map(n => ( + + ))} +
+
+ + )} + + {/* Krok 4: Wyniki */} + {step === 4 && deviceType === 'pompa' && ( +
+
+

+ Wycena pompy ciepła +

+

+ Każda instalacja pompy ciepła wymaga indywidualnego doboru urządzenia do specyfiki budynku (izolacja, instalacja grzewcza). + Skontaktuj się z nami, a przygotujemy dla Ciebie bezpłatną, spersonalizowaną ofertę. +

+ + + +
+
+ )} + + {step === 4 && deviceType !== 'pompa' && ( +
+ {/* Podsumowanie */} +
+

+ Dopasowane klimatyzatory +

+
+ {[ + { label: 'Wymagana moc', value: `${requiredPowerRounded} kW` }, + { label: 'Powierzchnia', value: `${totalM2} m²` }, + { label: 'Typ instalacji', value: category === 'scienne' ? 'Split (1 pokój)' : `Multisplit (${rooms} pokoje)` }, + { label: 'Szac. montaż', value: `${installCost.min.toLocaleString('pl-PL')}–${installCost.max.toLocaleString('pl-PL')} zł` }, + ...(energyClass ? [{ label: 'Klasa energetyczna', value: energyClass }] : []), + ...(maxNoise ? [{ label: 'Max. hałas wewn.', value: `do ${maxNoise} dB` }] : []), + ...(maxNoiseOut ? [{ label: 'Max. hałas zewn.', value: `do ${maxNoiseOut} dB` }] : []), + ].map((s, i) => ( +
+

{s.label}

+

{s.value}

+
+ ))} +
+
+ + {results.length > 0 ? ( +
+
+ {paginatedResults.map(product => ( + + ))} +
+ {totalPages > 1 && ( +
+ {getPageNumbers().map((p, i) => ( + p === '...' ? ( + ... + ) : ( + + ) + ))} +
+ )} +
+ ) : ( +
+

+ Nie znaleziono produktów dla podanych kryteriów. Spróbuj zwiększyć budżet lub zadzwoń do nas — dobierzemy ofertę indywidualnie. +

+ + + +
+ )} + +
+

⚠️ Ceny mają charakter orientacyjny

+

+ Podane ceny urządzeń są cenami brutto (23% VAT). Koszt montażu jest szacunkowy. Zachęcamy do darmowej wyceny. +

+
+ + + + + + +
+
)} @@ -759,10 +913,11 @@ export function KalkulatorClient({ products }: Props) { ) ) : ( - + + + )}