feat: refactor calculator room logic, update pricing, add dynamic prepositions

This commit is contained in:
MichalC
2026-07-05 21:22:09 +02:00
parent 59fd9a9b97
commit dbbf35a401
6 changed files with 349 additions and 128 deletions
+108 -98
View File
@@ -38,7 +38,7 @@ function estimateInstall(pipes: string, rooms: number): { min: number; max: numb
return { min: base + pipeExtra, max: base + pipeExtra + 600 };
}
const STEPS = ['Typ', 'Pomieszczenie', 'Konfiguracja', 'Instalacja', 'Budżet'] as const;
const STEPS = ['Typ', 'Pomieszczenie', 'Instalacja', 'Budżet'] as const;
export function KalkulatorClient({ products }: Props) {
const [deviceType, setDeviceType] = useState<'klimatyzacja' | 'pompa'>('klimatyzacja');
@@ -46,21 +46,42 @@ export function KalkulatorClient({ products }: Props) {
const [pumpForm, setPumpForm] = useState({ name: '', phone: '', city: '', message: '' });
const [step, setStep] = useState(0);
const [building, setBuilding] = useState('apartment');
const [m2, setM2] = useState(25);
const [pomieszczenia, setPomieszczenia] = useState<{ nazwa: string; m2: number }[]>([
{ nazwa: 'Pokój 1', m2: 20 }
]);
const [floor, setFloor] = useState('middle'); // ground | middle | top
const [sun, setSun] = useState('south'); // north | east | south | west
const [systemType, setSystemType] = useState<'auto' | 'scienne' | 'multisplit'>('auto');
const [rooms, setRooms] = useState(1);
const addRoom = () => {
if (pomieszczenia.length < 5) {
setPomieszczenia(prev => [...prev, { nazwa: `Pokój ${prev.length + 1}`, m2: 15 }]);
}
};
const removeRoom = (i: number) => {
if (pomieszczenia.length > 1) {
setPomieszczenia(prev => prev.filter((_, idx) => idx !== i));
}
};
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 [budget, setBudget] = useState(8000);
const [showResults, setShowResults] = useState(false);
const [preferredBrand, setPreferredBrand] = useState('');
const requiredPower = useMemo(() => calculatePower(m2, floor, sun, building), [m2, floor, sun, building]);
const totalM2 = pomieszczenia.reduce((sum, p) => sum + p.m2, 0);
const rooms = pomieszczenia.length;
const requiredPower = useMemo(() =>
pomieszczenia.reduce((sum, p) => sum + calculatePower(p.m2, floor, sun, building), 0),
[pomieszczenia, floor, sun, building]
);
const requiredPowerRounded = Math.round(requiredPower * 10) / 10;
const installCost = useMemo(() => estimateInstall(pipes, rooms), [pipes, rooms]);
const category = systemType === 'auto' ? (rooms === 1 ? 'scienne' : 'multisplit') : systemType;
const category = rooms === 1 ? 'scienne' : 'multisplit';
const results = useMemo(() => {
if (!showResults || deviceType === 'pompa') return [];
@@ -151,8 +172,8 @@ export function KalkulatorClient({ products }: Props) {
</h1>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{[
{ label: 'Wymagana moc', value: `${requiredPower} kW` },
{ label: 'Powierzchnia', value: `${m2} m²` },
{ 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) => (
@@ -410,35 +431,80 @@ export function KalkulatorClient({ products }: Props) {
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<span className="text-xs text-text-muted">Powierzchnia</span>
<span className="text-xs font-bold text-primary bg-accent/10 px-2 py-0.5 rounded-md">{m2} m²</span>
</div>
<div className="relative h-5 flex items-center">
<div className="absolute w-full h-1.5 bg-slate-200 rounded-full" />
<div
className="absolute h-1.5 bg-accent rounded-full"
style={{ width: `${((m2 - 10) / 90) * 100}%` }}
/>
<input
type="range" min={10} max={100} step={5} value={m2}
onChange={e => setM2(Number(e.target.value))}
className="absolute w-full h-1.5 opacity-0 cursor-pointer z-10"
/>
<div
className="absolute size-4 bg-white border-2 border-accent rounded-full shadow-md pointer-events-none"
style={{ left: `calc(${((m2 - 10) / 90) * 100}% - 8px)` }}
/>
</div>
<div className="flex justify-between text-xs text-text-muted">
<span>10 m²</span>
<span>100 m²</span>
</div>
<div className="mt-3 p-3 bg-blue-50 rounded-xl text-sm text-blue-700 font-medium">
Wymagana moc: ok. <strong>{requiredPower} kW</strong>
<span className="text-xs font-normal ml-2">(dla {m2} m²)</span>
</div>
{/* Lista pomieszczeń */}
<div className="flex flex-col gap-3">
{pomieszczenia.map((p, i) => (
<div key={i} className="bg-slate-50 rounded-xl border border-border p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-semibold text-primary">{p.nazwa}</span>
{pomieszczenia.length > 1 && (
<button
onClick={() => removeRoom(i)}
className="text-xs text-red-400 hover:text-red-600 transition-colors"
>
Usuń
</button>
)}
</div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<span className="text-xs text-text-muted">Powierzchnia</span>
<span className="text-xs font-bold text-primary bg-accent/10 px-2 py-0.5 rounded-md">{p.m2} m²</span>
</div>
<div className="relative h-5 flex items-center">
<div className="absolute w-full h-1.5 bg-slate-200 rounded-full" />
<div
className="absolute h-1.5 bg-accent rounded-full"
style={{ width: `${((p.m2 - 5) / 95) * 100}%` }}
/>
<input
type="range" min={5} max={100} step={5} value={p.m2}
onChange={e => updateRoom(i, Number(e.target.value))}
className="absolute w-full h-1.5 opacity-0 cursor-pointer z-10"
/>
<div
className="absolute size-4 bg-white border-2 border-accent rounded-full shadow-md pointer-events-none"
style={{ left: `calc(${((p.m2 - 5) / 95) * 100}% - 8px)` }}
/>
</div>
<div className="flex justify-between text-xs text-text-muted">
<span>5 m²</span>
<span>100 m²</span>
</div>
{(() => {
const mocPokoju = Math.round(calculatePower(p.m2, floor, sun, building) * 10) / 10;
const dostepneMoce = [2.5, 3.5, 5.0, 7.0];
const zalecana = dostepneMoce.reduce((prev, curr) =>
Math.abs(curr - mocPokoju) < Math.abs(prev - mocPokoju) ? curr : prev
);
return (
<div className="mt-2 flex items-center justify-between bg-white border border-border rounded-lg px-3 py-2">
<span className="text-xs text-text-muted">Zalecana moc jednostki</span>
<span className="text-sm font-bold text-accent">{zalecana} kW</span>
</div>
);
})()}
</div>
</div>
))}
</div>
{pomieszczenia.length < 5 && (
<button
onClick={addRoom}
className="flex items-center gap-2 text-sm text-accent font-semibold hover:text-accent/80 transition-colors border border-dashed border-accent/40 rounded-xl px-4 py-3 w-full justify-center"
>
+ Dodaj kolejne pomieszczenie
</button>
)}
<div className="p-3 bg-accent/10 rounded-xl">
<p className="text-sm text-accent font-semibold">
Łączna wymagana moc: ok. <strong>{requiredPowerRounded} kW</strong>
<span className="text-xs font-normal text-text-muted ml-2">
({pomieszczenia.length === 1 ? 'klimatyzator split' : `multisplit ${pomieszczenia.length}+1`}, łącznie {totalM2} m²)
</span>
</p>
</div>
<div>
@@ -513,64 +579,8 @@ export function KalkulatorClient({ products }: Props) {
</div>
)}
{/* Krok 2: Konfiguracja */}
{/* Krok 2: Instalacja */}
{step === 2 && (
<div className="flex flex-col gap-6">
<div className="flex items-center gap-3 mb-2">
<div className="size-10 rounded-xl bg-purple-50 flex items-center justify-center">
<Thermometer className="size-5 text-purple-600" />
</div>
<div>
<h2 className="text-lg font-bold text-primary font-heading">Konfiguracja</h2>
<p className="text-xs text-text-muted">Ilość pomieszczeń i typ instalacji</p>
</div>
</div>
<div>
<label className="text-sm font-semibold text-primary mb-3 block">Wybierz typ systemu</label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
{[
{ value: 'auto', label: 'Dobierz automatycznie' },
{ value: 'scienne', label: 'Split (1 agregat = 1 pokój)' },
{ value: 'multisplit', label: 'Multisplit (1 agregat = wiele)' },
].map(o => (
<button key={o.value} onClick={() => setSystemType(o.value as 'auto'|'scienne'|'multisplit')}
className={`py-2 px-3 rounded-xl text-sm font-semibold border-2 transition-all ${systemType === o.value ? 'border-accent bg-accent text-white' : 'border-border bg-white text-primary hover:border-accent'}`}>
{o.label}
</button>
))}
</div>
</div>
<div>
<label className="text-sm font-semibold text-primary mb-3 block">Ile pomieszczeń chcesz chłodzić?</label>
<div className="grid grid-cols-2 gap-3">
{[
{ value: 1, label: '1 pomieszczenie', sub: 'Pojedynczy pokój', icon: '🏠' },
{ value: 2, label: '2 pomieszczenia', sub: 'Dwa pokoje', icon: '🏡' },
{ value: 3, label: '3 pomieszczenia', sub: 'Trzy pokoje', icon: '🏘️' },
{ value: 4, label: '4-5 pomieszczeń', sub: 'Wiele pokoi', icon: '🏗️' },
].map(o => (
<button key={o.value} onClick={() => setRooms(o.value)}
className={`p-4 rounded-xl border-2 text-left transition-all ${rooms === o.value ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
<span className="text-2xl mb-2 block">{o.icon}</span>
<p className="text-sm font-bold text-primary">{o.label}</p>
<p className="text-xs text-text-muted">{o.sub}</p>
</button>
))}
</div>
</div>
<div className="p-3 bg-slate-50 rounded-xl text-xs text-text-muted">
{category === 'scienne'
? '✅ Wybrano: Klimatyzator split — jedna jednostka wewnętrzna i jedna zewnętrzna. Dedykowane dla jednego pokoju.'
: `✅ Wybrano: System multisplit — jedna jednostka zewnętrzna obsłuży ${rooms} jednostki wewnętrzne. Lepsza estetyka na zewnątrz budynku.`}
</div>
</div>
)}
{/* Krok 3: Instalacja */}
{step === 3 && (
<div className="flex flex-col gap-6">
<div className="flex items-center gap-3 mb-2">
<div className="size-10 rounded-xl bg-orange-50 flex items-center justify-center">
@@ -628,8 +638,8 @@ export function KalkulatorClient({ products }: Props) {
</div>
)}
{/* Krok 4: Budżet */}
{step === 4 && (
{/* Krok 3: Budżet */}
{step === 3 && (
<div className="flex flex-col gap-6">
<div className="flex items-center gap-3 mb-2">
<div className="size-10 rounded-xl bg-green-50 flex items-center justify-center">
@@ -684,7 +694,7 @@ export function KalkulatorClient({ products }: Props) {
</div>
<div className="p-3 bg-blue-50 rounded-xl text-xs text-blue-700">
💡 Podsumowanie: szukamy klimatyzatora <strong>{category === 'scienne' ? 'split ściennego' : 'multisplit'}</strong> o mocy ok. <strong>{requiredPower} kW</strong> w cenie do <strong>{(budget * 0.75).toLocaleString('pl-PL')} zł</strong>
💡 Podsumowanie: szukamy klimatyzatora <strong>{category === 'scienne' ? 'split ściennego' : 'multisplit'}</strong> o mocy ok. <strong>{requiredPowerRounded} kW</strong> w cenie do <strong>{(budget * 0.75).toLocaleString('pl-PL')} zł</strong>
</div>
<div>