From 94dd300c5395759559ede681f6047b10024d54a4 Mon Sep 17 00:00:00 2001 From: Przemek Zatorski Date: Tue, 22 Sep 2026 14:21:22 +0200 Subject: [PATCH] Wycena na prawdziwych danych zamiast zmyslonej tabeli cen Stawka bazowa za m2 pochodzila z tabeli opisanej jako "SonarHome 2026" i zmyslonych mnoznikow dzielnic. Teraz korzysta z /api/price-history: - najnowsza cena transakcyjna mieszkan NBP dla miasta i rynku, potem GUS (dane najwyzej 3-letnie), a bez nich srednia cen ofertowych z ogloszen; - korekta dzielnicy liczona z ogloszen (min. 3 w dzielnicy i 5 w miescie); - domy: srednia z ogloszen domow, gdy sa co najmniej 3, inaczej ceny mieszkan z korekta modelu - opisane przy wyniku; - zrodlo stawki w podgladzie, zalozeniach i raporcie PDF; - brak danych dla lokalizacji to komunikat zamiast wymyslonej kwoty. Korekty modelu (stan, pietro, rok budowy, udogodnienia) bez zmian. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/src/App.tsx | 333 +++++++++++++++++++++++----------------- frontend/src/styles.css | 10 ++ 2 files changed, 200 insertions(+), 143 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8f7f9d2..61b3a69 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17130,6 +17130,10 @@ type ValuationResult = { validUntil: string; details: { icon: string; label: string; value: string }[]; mainFactors: string[]; + // Dlaczego nie ma wyniku, gdy sa juz podstawowe dane (np. brak danych rynkowych dla lokalizacji). + unavailableReason?: string; + // Zrodlo stawki bazowej - do wyniku i raportu PDF. + sourceLabel?: string; }; const INITIAL_VALUATION_FORM: ValuationForm = { @@ -17173,14 +17177,121 @@ const VALUATION_HOUSE_BUILDING_TYPES = ['Wolnostojący', 'Bliźniak', 'Szeregowy const VALUATION_COMMERCIAL_BUILDING_TYPES = ['Lokal handlowy', 'Biuro', 'Usługi', 'Gastronomia', 'Magazyn']; const VALUATION_COMMERCIAL_PURPOSES = ['Handel', 'Biuro', 'Usługi', 'Gastronomia', 'Medyczny', 'Magazyn']; -type ValuationMarketReference = { - label: string; +// Stawka bazowa wyceny z prawdziwych danych: cena transakcyjna NBP/GUS dla miasta albo srednia +// cen ofertowych z ogloszen serwisu. sourceLabel trafia do wyniku i raportu PDF. +type ValuationBase = { basePerMeter: number; - precision: 'city' | 'region' | 'national'; - locationType: string; + sourceLabel: string; + precisionLabel: string; + // false, gdy stawka juz dotyczy wycenianego typu (np. srednia z ogloszen domow). + appliesPropertyMultiplier: boolean; }; -const VALUATION_2026_SOURCE_NOTE = 'model 2026: ceny transakcyjne SonarHome 2026-06-01 + korekty regionalne'; +// Dane rynkowe dla wyceny - odpowiedzi /api/price-history dla miasta i dzielnicy. +type ValuationMarketData = { + city: string; + district: string; + apartmentCity: ApiPriceHistory | null; + apartmentDistrict: ApiPriceHistory | null; + houseCity: ApiPriceHistory | null; + houseDistrict: ApiPriceHistory | null; +}; + +const VALUATION_MIN_DISTRICT_LISTINGS = 3; +const VALUATION_MIN_CITY_LISTINGS_FOR_RATIO = 5; +// GUS publikuje z opoznieniem - starszych danych nie uzywamy jako stawki. +const VALUATION_MAX_GUS_AGE_YEARS = 3; + +const VALUATION_PLACE_TYPE_WORDS = new Set(['miasto', 'wies', 'dzielnica', 'osiedle', 'miejscowosc', 'gmina', 'powiat', 'przysiolek', 'kolonia', 'osada']); + +// "Kraków, Podgórze" -> miasto Kraków, dzielnica Podgórze. Podpowiedzi mapy maja postac +// "Kraków, miasto, Kraków, małopolskie" - opisy typu, powiatu i wojewodztwa pomijamy. +function parseValuationLocation(location: string): { city: string; district: string } { + const parts = location.split(',').map((part) => part.trim()).filter(Boolean); + const city = parts[0] ?? ''; + const district = parts.slice(1).find((part) => { + const normalized = normalizeHistoryText(part.replace(/^powiat\s+/i, '')); + return !VALUATION_PLACE_TYPE_WORDS.has(normalized) && !isHistoryRegionName(part) + && normalized !== normalizeHistoryText(city); + }) ?? ''; + return { city, district }; +} + +// Dom albo lokal liczony od cen mieszkan - zaznaczamy to w opisie zrodla. +function buildValuationBase(form: ValuationForm, data: ValuationMarketData | null): ValuationBase | null { + const base = buildValuationBaseFromData(form, data); + if (base && base.appliesPropertyMultiplier && (form.propertyType === 'Dom' || form.propertyType === 'Lokal użytkowy')) { + const what = form.propertyType === 'Dom' ? 'domu' : 'lokalu użytkowego'; + return { ...base, sourceLabel: `${base.sourceLabel}, przeliczona dla ${what} wg modelu` }; + } + return base; +} + +function buildValuationBaseFromData(form: ValuationForm, data: ValuationMarketData | null): ValuationBase | null { + if (!data || !data.city) { + return null; + } + const where = [data.city, data.district].filter(Boolean).join(', '); + const cityListings = data.apartmentCity?.current ?? null; + const districtListings = data.apartmentDistrict?.current ?? null; + // Korekta dzielnicy z naszych ogloszen - tylko przy wystarczajacej liczbie ogloszen w obu grupach. + const districtRatio = data.district && districtListings && cityListings + && districtListings.listingsCount >= VALUATION_MIN_DISTRICT_LISTINGS + && cityListings.listingsCount >= VALUATION_MIN_CITY_LISTINGS_FOR_RATIO + ? clampNumber(districtListings.avgPricePerM2 / cityListings.avgPricePerM2, 0.7, 1.4) + : null; + const districtNote = districtRatio != null + ? `, korekta dzielnicy wg ogłoszeń ${districtRatio >= 1 ? '+' : ''}${((districtRatio - 1) * 100).toFixed(1).replace('.', ',')}%` + : ''; + + if (form.propertyType === 'Dom') { + const houseListings = [data.houseDistrict?.current, data.houseCity?.current] + .find((current) => current && current.listingsCount >= VALUATION_MIN_DISTRICT_LISTINGS) ?? null; + if (houseListings) { + return { + basePerMeter: houseListings.avgPricePerM2, + sourceLabel: `średnia cen ofertowych domów z ${houseListings.listingsCount} ogłoszeń w serwisie (${where})`, + precisionLabel: 'Dane: ogłoszenia domów w tej lokalizacji', + appliesPropertyMultiplier: false, + }; + } + } + + const nbp = data.apartmentCity?.nbp?.[data.apartmentCity.nbp.length - 1] ?? null; + const gusAll = data.apartmentCity?.gus ?? []; + const gus = gusAll[gusAll.length - 1] ?? null; + const gusFresh = gus && gus.year >= new Date().getFullYear() - VALUATION_MAX_GUS_AGE_YEARS ? gus : null; + const transaction = nbp + ? { price: nbp.pricePerM2, label: `cena transakcyjna mieszkań NBP, rynek ${(data.apartmentCity?.nbpMarket ?? 'Wtórny').toLowerCase()}, ${HISTORY_ROMAN_QUARTERS[nbp.quarter - 1]} kw. ${nbp.year} (${data.city})` } + : gusFresh + ? { price: gusFresh.pricePerM2, label: `cena transakcyjna mieszkań GUS, ${HISTORY_ROMAN_QUARTERS[gusFresh.quarter - 1]} kw. ${gusFresh.year} (${data.city})` } + : null; + if (transaction) { + return { + basePerMeter: Math.round(transaction.price * (districtRatio ?? 1)), + sourceLabel: `${transaction.label}${districtNote}`, + precisionLabel: districtRatio != null ? 'Dokładność: dzielnica' : 'Dokładność: całe miasto', + appliesPropertyMultiplier: true, + }; + } + + // Bez cen transakcyjnych - srednia cen ofertowych mieszkan z naszych ogloszen. + const listings = districtListings && districtListings.listingsCount >= VALUATION_MIN_DISTRICT_LISTINGS + ? { current: districtListings, place: where } + : cityListings ? { current: cityListings, place: data.city } : null; + if (listings) { + return { + basePerMeter: listings.current.avgPricePerM2, + sourceLabel: `średnia cen ofertowych mieszkań z ${listings.current.listingsCount} ogłoszeń w serwisie (${listings.place})`, + precisionLabel: listings.current.listingsCount < VALUATION_MIN_DISTRICT_LISTINGS + ? 'Dokładność: orientacyjna (mało ogłoszeń)' + : 'Dokładność: ogłoszenia w tej lokalizacji', + appliesPropertyMultiplier: true, + }; + } + return null; +} + const VALUATION_PRICE_PER_SQM_BY_CITY: Record = { Warszawa: 15700, @@ -17230,53 +17341,6 @@ const VALUATION_PRICE_PER_SQM_BY_CITY: Record = { Ostrołęka: 7000, }; -const VALUATION_PRICE_PER_SQM_BY_REGION: Record = { - mazowieckie: 8200, - małopolskie: 8300, - pomorskie: 8600, - dolnośląskie: 7900, - wielkopolskie: 7400, - łódzkie: 6500, - śląskie: 6700, - podkarpackie: 6600, - lubelskie: 6300, - zachodniopomorskie: 7000, - 'kujawsko-pomorskie': 6100, - podlaskie: 6200, - 'warmińsko-mazurskie': 6500, - świętokrzyskie: 5900, - opolskie: 6100, - lubuskie: 6100, -}; - -const VALUATION_NATIONAL_BASE_PER_SQM = 6900; - -const VALUATION_DISTRICT_MULTIPLIERS: { keyword: string; multiplier: number }[] = [ - { keyword: 'śródmieście', multiplier: 1.18 }, - { keyword: 'stare miasto', multiplier: 1.16 }, - { keyword: 'mokotów', multiplier: 1.13 }, - { keyword: 'żoliborz', multiplier: 1.12 }, - { keyword: 'wola', multiplier: 1.1 }, - { keyword: 'wilanów', multiplier: 1.1 }, - { keyword: 'ursynów', multiplier: 1.05 }, - { keyword: 'zabłocie', multiplier: 1.08 }, - { keyword: 'podgórze', multiplier: 1.05 }, - { keyword: 'kazimierz', multiplier: 1.12 }, - { keyword: 'krowodrza', multiplier: 1.04 }, - { keyword: 'oliwa', multiplier: 1.09 }, - { keyword: 'wrzeszcz', multiplier: 1.06 }, - { keyword: 'przymorze', multiplier: 1.04 }, - { keyword: 'krzyki', multiplier: 1.05 }, - { keyword: 'nadodrze', multiplier: 1.03 }, - { keyword: 'jeżyce', multiplier: 1.04 }, - { keyword: 'łazarz', multiplier: 1.02 }, - { keyword: 'bemowo', multiplier: 1.02 }, - { keyword: 'praga', multiplier: 0.98 }, - { keyword: 'nowa huta', multiplier: 0.92 }, - { keyword: 'bielany', multiplier: 0.99 }, - { keyword: 'targówek', multiplier: 0.94 }, -]; - const VALUATION_CONDITION_OPTIONS = [ { icon: 'paint', title: 'Do remontu', text: 'Nieruchomość wymaga generalnego remontu.', multiplier: 0.88 }, { icon: 'hammer', title: 'Do odświeżenia', text: 'Nieruchomość wymaga odświeżenia lub drobnych napraw.', multiplier: 0.95 }, @@ -17353,78 +17417,6 @@ function parsePositiveNumber(value: string): number | null { return parsed != null && parsed > 0 ? parsed : null; } -function findValuationCity(location: string): string | null { - const normalizedLocation = location.toLowerCase(); - return Object.keys(VALUATION_PRICE_PER_SQM_BY_CITY).find((city) => normalizedLocation.includes(city.toLowerCase())) ?? null; -} - -function getLocationTypeAdjustment(location: string): { multiplier: number; label: string } { - const normalizedLocation = location.toLowerCase(); - if (normalizedLocation.includes('dzielnica') || normalizedLocation.includes('osiedle')) { - return { multiplier: 1, label: 'dzielnica' }; - } - if (normalizedLocation.includes('wieś') || normalizedLocation.includes('hamlet') || normalizedLocation.includes('osada')) { - return { multiplier: 0.72, label: 'wieś' }; - } - if (normalizedLocation.includes('powiat')) { - return { multiplier: 0.78, label: 'powiat' }; - } - if (normalizedLocation.includes('miejscowość')) { - return { multiplier: 0.82, label: 'miejscowość' }; - } - if (normalizedLocation.includes('miasto')) { - return { multiplier: 0.9, label: 'miasto' }; - } - return { multiplier: 0.86, label: 'lokalizacja' }; -} - -function resolveValuationMarket(location: string): ValuationMarketReference | null { - const trimmedLocation = location.trim(); - if (trimmedLocation.length < 2) { - return null; - } - const city = findValuationCity(trimmedLocation); - if (city) { - return { - label: city, - basePerMeter: VALUATION_PRICE_PER_SQM_BY_CITY[city], - precision: 'city', - locationType: 'miasto', - }; - } - const normalizedLocation = trimmedLocation.toLowerCase(); - const region = Object.keys(VALUATION_PRICE_PER_SQM_BY_REGION).find((regionName) => normalizedLocation.includes(regionName)); - const locationType = getLocationTypeAdjustment(trimmedLocation); - if (region) { - return { - label: `${region}, ${locationType.label}`, - basePerMeter: Math.round(VALUATION_PRICE_PER_SQM_BY_REGION[region] * locationType.multiplier / 50) * 50, - precision: 'region', - locationType: locationType.label, - }; - } - const knownCity = POLISH_CITIES.find((cityName) => normalizedLocation.includes(cityName.toLowerCase())); - if (knownCity) { - return { - label: knownCity, - basePerMeter: Math.round(VALUATION_NATIONAL_BASE_PER_SQM * 1.04 / 50) * 50, - precision: 'region', - locationType: 'miasto', - }; - } - return { - label: `Polska, ${locationType.label}`, - basePerMeter: Math.round(VALUATION_NATIONAL_BASE_PER_SQM * locationType.multiplier / 50) * 50, - precision: 'national', - locationType: locationType.label, - }; -} - -function getDistrictMultiplier(location: string): number { - const normalizedLocation = location.toLowerCase(); - return VALUATION_DISTRICT_MULTIPLIERS.find((item) => normalizedLocation.includes(item.keyword))?.multiplier ?? 1; -} - function getAreaMultiplier(area: number): number { if (area < 35) { return 1.08; @@ -17586,15 +17578,16 @@ type ValuationDisplay = { formatPricePerAreaNumber: (plnPerSquareMeter: number) => string; }; -function calculateApartmentValuation(form: ValuationForm, display: ValuationDisplay): ValuationResult { +// base - stawka z danych rynkowych (null: jeszcze sie wczytuje albo brak danych dla lokalizacji). +function calculateApartmentValuation(form: ValuationForm, display: ValuationDisplay, base: ValuationBase | null, + baseLoading: boolean): ValuationResult { // Metraz przychodzi w jednostce wybranej przez uzytkownika, a caly model cen operuje na m² - // dlatego normalizujemy go tutaj, zanim trafi do progow i mnoznikow. const areaInput = parsePositiveNumber(form.area); const area = areaInput ? display.toSquareMeters(areaInput) : null; const plotAreaInput = parsePositiveNumber(form.plotArea); const plotArea = plotAreaInput ? display.toSquareMeters(plotAreaInput) : null; - const marketReference = resolveValuationMarket(form.location); - if (!area || !marketReference) { + if (!area || !base) { return { ready: false, minValue: 0, @@ -17606,13 +17599,20 @@ function calculateApartmentValuation(form: ValuationForm, display: ValuationDisp validUntil: '', details: [], mainFactors: [], + unavailableReason: area && form.location.trim().length >= 2 + ? baseLoading + ? 'Pobieramy dane rynkowe dla tej lokalizacji…' + : 'Brak danych rynkowych dla tej lokalizacji - nie mamy cen transakcyjnych NBP/GUS ani ogłoszeń w serwisie. Spróbuj wpisać większe miasto w formacie „Miasto, dzielnica”.' + : undefined, }; } - const basePerMeter = marketReference.basePerMeter; + const basePerMeter = base.basePerMeter; const conditionMultiplier = VALUATION_CONDITION_OPTIONS.find((option) => option.title === form.condition)?.multiplier ?? 1; - const marketMultiplier = form.market === 'Pierwotny' ? 1.07 : 1; - const propertyMultiplier = form.propertyType === 'Dom' ? 0.86 : form.propertyType === 'Lokal użytkowy' ? 0.78 : 1; + // NBP podaje rynek wprost - korekta rynku tylko, gdy stawka nie dotyczy juz wybranego rynku. + const marketMultiplier = form.market === 'Pierwotny' && !base.sourceLabel.includes('pierwotny') ? 1.07 : 1; + const propertyMultiplier = !base.appliesPropertyMultiplier ? 1 + : form.propertyType === 'Dom' ? 0.86 : form.propertyType === 'Lokal użytkowy' ? 0.78 : 1; const buildingMultiplier = getBuildingTypeMultiplier(form); const materialMultiplier = form.buildingMaterial === 'Cegła' ? 1.025 : form.buildingMaterial === 'Żelbet' ? 1.015 : form.buildingMaterial === 'Wielka płyta' ? 0.975 : 1; const kitchenMultiplier = form.propertyType !== 'Lokal użytkowy' && form.kitchen === 'Oddzielna' && area >= 50 ? 1.01 : 1; @@ -17621,7 +17621,6 @@ function calculateApartmentValuation(form: ValuationForm, display: ValuationDisp const rentMultiplier = rent && area ? clampNumber(1 - Math.max(0, rent / area - 16) * 0.002, 0.97, 1.01) : 1; const adjustedPerMeter = basePerMeter - * getDistrictMultiplier(form.location) * marketMultiplier * propertyMultiplier * getAreaMultiplier(area) @@ -17689,13 +17688,13 @@ function calculateApartmentValuation(form: ValuationForm, display: ValuationDisp { icon: 'money', label: form.propertyType === 'Dom' ? 'Koszty utrzymania' : form.propertyType === 'Lokal użytkowy' ? 'Czynsz / koszty' : 'Czynsz administracyjny', value: rent ? `${formatPln(rent)} / mies.` : 'Nie podano' }, ], mainFactors: [ - `${marketReference.label}: baza ${display.formatPricePerAreaNumber(basePerMeter)} za ${display.areaSuffix}`, - marketReference.precision === 'city' ? 'Dokładność: miasto' : marketReference.precision === 'region' ? 'Dokładność: region i typ lokalizacji' : 'Dokładność: średnia krajowa', - `Lokalizacja: x${getDistrictMultiplier(form.location).toFixed(2)}`, + `Stawka bazowa: ${display.formatPricePerAreaNumber(basePerMeter)} za ${display.areaSuffix} - ${base.sourceLabel}`, + base.precisionLabel, `Stan: x${conditionMultiplier.toFixed(2)}`, `Rok, budynek i typ: x${(getYearMultiplier(form.yearBuilt) * buildingMultiplier * materialMultiplier * getPropertyTypeDetailMultiplier(form, plotArea)).toFixed(2)}`, - VALUATION_2026_SOURCE_NOTE, + 'Korekty (stan, piętro, rok budowy, udogodnienia) to model orientacyjny, nie operat szacunkowy.', ], + sourceLabel: base.sourceLabel, }; } @@ -17798,7 +17797,7 @@ async function downloadValuationReportPdf(form: ValuationForm, valuation: Valuat }, { text: 'Raport szacunkowej wyceny nieruchomości', style: 'title' }, { - text: 'Dokument przygotowany automatycznie na podstawie danych podanych w formularzu oraz modelu cenowego 2026. Raport ma charakter informacyjny i nie jest operatem szacunkowym.', + text: `Dokument przygotowany automatycznie na podstawie danych z formularza. Stawka bazowa: ${valuation.sourceLabel ?? 'dane rynkowe serwisu'}. Raport ma charakter informacyjny i nie jest operatem szacunkowym.`, style: 'lead', margin: [0, 0, 0, 18], }, @@ -18539,8 +18538,55 @@ function ValuationPage() { const [valuationLocationSuggestOpen, setValuationLocationSuggestOpen] = useState(false); const [valuationLocationSuggestions, setValuationLocationSuggestions] = useState([]); const [valuationLocationSuggestLoading, setValuationLocationSuggestLoading] = useState(false); - const valuation = calculateApartmentValuation(valuationForm, { toSquareMeters, areaSuffix, formatAreaNumber, formatPricePerAreaNumber }); - const valuationMarketReference = resolveValuationMarket(valuationForm.location); + // Dane rynkowe dla lokalizacji z formularza - te same co w Historii cen (ogloszenia, NBP, GUS). + const [valuationMarketData, setValuationMarketData] = useState(null); + const [valuationMarketLoading, setValuationMarketLoading] = useState(false); + const valuationPlace = parseValuationLocation(valuationForm.location); + useEffect(() => { + const { city, district } = valuationPlace; + if (city.length < 2) { + setValuationMarketData(null); + setValuationMarketLoading(false); + return; + } + let cancelled = false; + setValuationMarketLoading(true); + const today = new Date().toISOString().slice(0, 10); + const load = (propertyType: 'APARTMENT' | 'HOUSE', withDistrict: boolean) => { + if (withDistrict && !district) { + return Promise.resolve(null); + } + const params = new URLSearchParams({ + city, + district: withDistrict ? district : '', + propertyType, + market: valuationForm.market, + from: today, + }); + return apiFetch(`/price-history?${params.toString()}`).catch(() => null); + }; + const timer = window.setTimeout(() => { + const wantsHouses = valuationForm.propertyType === 'Dom'; + Promise.all([ + load('APARTMENT', false), + load('APARTMENT', true), + wantsHouses ? load('HOUSE', false) : Promise.resolve(null), + wantsHouses ? load('HOUSE', true) : Promise.resolve(null), + ]).then(([apartmentCity, apartmentDistrict, houseCity, houseDistrict]) => { + if (!cancelled) { + setValuationMarketData({ city, district, apartmentCity, apartmentDistrict, houseCity, houseDistrict }); + setValuationMarketLoading(false); + } + }); + }, 400); + return () => { + cancelled = true; + window.clearTimeout(timer); + }; + }, [valuationPlace.city, valuationPlace.district, valuationForm.market, valuationForm.propertyType]); + const valuationBase = buildValuationBase(valuationForm, valuationMarketData); + const valuation = calculateApartmentValuation(valuationForm, { toSquareMeters, areaSuffix, formatAreaNumber, formatPricePerAreaNumber }, + valuationBase, valuationMarketLoading); const isValuationDetailsStep = step === 1; const isValuationConditionStep = step === 2; const isValuationResultStep = step === 3; @@ -18550,7 +18596,7 @@ function ValuationPage() { const valuationBuildingTypeOptions = isHouseValuation ? VALUATION_HOUSE_BUILDING_TYPES : isCommercialValuation ? VALUATION_COMMERCIAL_BUILDING_TYPES : VALUATION_APARTMENT_BUILDING_TYPES; const visibleValuationAmenityOptions = getValuationAmenityOptions(valuationForm.propertyType); const nextStepLabel = isValuationConditionStep ? 'Dalej: Wynik wyceny' : isValuationDetailsStep ? 'Dalej: Stan i udogodnienia' : 'Dalej: Szczegóły nieruchomości'; - const isLocationComplete = Boolean(valuationMarketReference && valuationForm.market && valuationForm.propertyType); + const isLocationComplete = Boolean(valuationPlace.city.length >= 2 && valuationForm.market && valuationForm.propertyType); const isDetailsComplete = Boolean( parsePositiveNumber(valuationForm.area) && valuationForm.rooms @@ -18593,8 +18639,8 @@ function ValuationPage() { { icon: 'target', label: 'Powtórz wycenę' }, ]; const valuationMissingItems = [ - !valuationMarketReference - ? { icon: 'pin', title: 'Podaj lokalizację', text: 'Wpisz miasto, dzielnicę, miejscowość albo wieś i wybierz podpowiedź z map.' } + valuationPlace.city.length < 2 + ? { icon: 'pin', title: 'Podaj lokalizację', text: 'Wpisz miasto, najlepiej z dzielnicą, np. „Kraków, Podgórze”.' } : null, !parsePositiveNumber(valuationForm.area) ? { icon: 'house', title: `Podaj metraż ${valuationPropertyLabel}`, text: 'Wycena startuje dopiero po wpisaniu powierzchni w m².' } @@ -18764,7 +18810,7 @@ function ValuationPage() {

Szacowana wartość maksymalna{valuation.ready ? formatPrice(valuation.maxValue) : '---'}

Średnia wartość{valuation.ready ? formatPrice(valuation.averageValue) : '---'}

Cena za {areaSuffix} (min - max){valuation.ready ? `${formatPricePerAreaNumber(valuation.minPerMeter)} - ${formatPricePerArea(valuation.maxPerMeter)}` : '---'}

- {valuation.ready ? `Wycena ważna do ${valuation.validUntil}` : 'Podaj dane, aby obliczyć wycenę'} + {valuation.ready ? `Wycena ważna do ${valuation.validUntil}` : valuation.unavailableReason ?? 'Podaj dane, aby obliczyć wycenę'}
@@ -18978,7 +19024,8 @@ function ValuationPage() { {valuation.ready ? {formatPrice(valuation.minValue)} - {formatPrice(valuation.maxValue)} : 0} Przedział cenowy {valuation.ready ? `${formatPricePerAreaNumber(valuation.minPerMeter)} - ${formatPricePerArea(valuation.maxPerMeter)}` : formatPricePerArea(0)} -

{valuation.ready ? `Średnio: ${formatPrice(valuation.averageValue)} (${formatPricePerArea(valuation.averagePerMeter)}).` : 'Podaj miasto oraz powierzchnię, aby zobaczyć wynik wyceny.'}

+

{valuation.ready ? `Średnio: ${formatPrice(valuation.averageValue)} (${formatPricePerArea(valuation.averagePerMeter)}).` : valuation.unavailableReason ?? 'Podaj miasto oraz powierzchnię, aby zobaczyć wynik wyceny.'}

+ {valuation.ready && valuation.sourceLabel && Na podstawie: {valuation.sourceLabel}.}