diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b850776..977847f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21136,18 +21136,32 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) { }); const nbpPrices = historySlots.map((slot) => historyQuarterValue(history?.nbp ?? [], slot)); const gusPrices = historySlots.map((slot) => historyQuarterValue(history?.gus ?? [], slot)); - const knownPrices = offerPrices + const offerKnownPrices = offerPrices .map((price, slotIndex) => ({ price, slotIndex })) .filter((entry): entry is { price: number; slotIndex: number } => entry.price != null); - const hasOfferData = knownPrices.length > 0; + const hasOfferData = offerKnownPrices.length > 0; const hasNbpData = nbpPrices.some((price) => price != null); const hasGusData = gusPrices.some((price) => price != null); const hasHistoryData = hasOfferData || hasNbpData || hasGusData; + // Dzielnica bez ogloszen nie ma wlasnej sredniej, ale wykres pokazuje w niej ceny + // transakcyjne dla calego miasta. Liczby nad wykresem i w rubrykach biora wtedy + // srednia z tego, co w danym kwartale podaly NBP i GUS - zrodlo jest podpisane, + // zeby nikt nie wzial danych miasta za dane dzielnicy. + const externalKnownPrices = historySlots + .map((_slot, slotIndex) => { + const sources = [nbpPrices[slotIndex], gusPrices[slotIndex]].filter((price): price is number => price != null); + return sources.length > 0 + ? { price: Math.round(sources.reduce((sum, price) => sum + price, 0) / sources.length), slotIndex } + : null; + }) + .filter((entry): entry is { price: number; slotIndex: number } => entry != null); + const knownPrices = hasOfferData ? offerKnownPrices : externalKnownPrices; + const statsFromOffers = hasOfferData; const firstKnown = knownPrices[0] ?? null; const latestKnown = knownPrices[knownPrices.length - 1] ?? null; const lowestKnown = knownPrices.reduce<{ price: number; slotIndex: number } | null>((lowest, entry) => (!lowest || entry.price < lowest.price ? entry : lowest), null); const highestKnown = knownPrices.reduce<{ price: number; slotIndex: number } | null>((highest, entry) => (!highest || entry.price > highest.price ? entry : highest), null); - const averageKnown = hasOfferData ? Math.round(knownPrices.reduce((sum, entry) => sum + entry.price, 0) / knownPrices.length) : null; + const averageKnown = knownPrices.length > 0 ? Math.round(knownPrices.reduce((sum, entry) => sum + entry.price, 0) / knownPrices.length) : null; // Zmiana ma sens dopiero przy dwoch roznych przedzialach z danymi. const changePercent = firstKnown && latestKnown && knownPrices.length > 1 && firstKnown.price > 0 ? ((latestKnown.price - firstKnown.price) / firstKnown.price) * 100 @@ -21540,16 +21554,28 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) {
-

Średnia cena za {areaSuffix} w {activeLocationLabel}

- {latestKnown ? formatPricePerArea(latestKnown.price) : '—'} - {changePercent != null && ( -

= 0 ? 'positive' : 'negative'}> - {changePercent >= 0 ? '+' : ''}{changePercent.toFixed(1).replace('.', ',')}% (od {historySlots[firstKnown!.slotIndex].sinceLabel}) -

- )} +
+

Średnia cena za {areaSuffix} w {activeLocationLabel}

+ {latestKnown ? formatPricePerArea(latestKnown.price) : '—'} + {changePercent != null && firstKnown && ( +

= 0 ? 'positive' : 'negative'}> + {changePercent >= 0 ? '+' : ''}{changePercent.toFixed(1).replace('.', ',')}% (od {historySlots[firstKnown.slotIndex].sinceLabel}) +

+ )} +
- Średnie ceny ofertowe z ogłoszeń w serwisie Polska Lokalnie - {history?.trackedSince ? `, zapisywane codziennie od ${formatArticleDate(history.trackedSince)}` : ''}. + {statsFromOffers || !hasHistoryData ? ( + <> + Średnie ceny ofertowe z ogłoszeń w serwisie Polska Lokalnie + {history?.trackedSince ? `, zapisywane codziennie od ${formatArticleDate(history.trackedSince)}` : ''}. + + ) : ( + <> + Średnia z cen transakcyjnych NBP i GUS dla całego miasta + {selectedMarket ? ` ${selectedMarket.city}` : ''} - w tej lokalizacji nie ma jeszcze ogłoszeń + w serwisie Polska Lokalnie. + + )}
Cena za {areaSuffix} @@ -21630,6 +21656,8 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) { Zmiana w okresie {changePercent == null ? '—' : `${changePercent >= 0 ? '+' : ''}${changePercent.toFixed(1).replace('.', ',')}%`} {changePercent != null && firstKnown && latestKnown && {formatPricePerArea(latestKnown.price - firstKnown.price)}} + {/* Jeden przedzial z danymi to jeszcze nie trend - mowimy wprost, czego brakuje. */} + {changePercent == null && knownPrices.length > 0 && Za krótka historia}
Średnia cena{averageKnown != null ? formatPricePerArea(averageKnown) : '—'}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 79b8f55..f3dac2e 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -8638,6 +8638,16 @@ a.city-card:hover { margin-bottom: 24px; } +/* Tytul, cena i zmiana stoja w jednej linii - osobne wiersze zostawialy nad wykresem + duza pusta przestrzen. Na wezszym ekranie wiersz zawija sie naturalnie. */ +.history-chart-title-row { + align-items: baseline; + column-gap: 16px; + display: flex; + flex-wrap: wrap; + row-gap: 4px; +} + .history-chart-head h2 { align-items: center; color: #14223a; @@ -8645,7 +8655,7 @@ a.city-card:hover { gap: 8px; font-size: 20px; line-height: 1.25; - margin: 0 0 16px; + margin: 0; } .history-chart-head h2 svg { @@ -8655,11 +8665,10 @@ a.city-card:hover { .history-chart-head strong { color: #0d1b35; - display: block; - font-size: 40px; + font-size: 30px; font-weight: 950; line-height: 1; - margin-bottom: 9px; + white-space: nowrap; } .history-chart-head p { diff --git a/frontend/tests/routing.spec.ts b/frontend/tests/routing.spec.ts index d4bc894..cd8693e 100644 --- a/frontend/tests/routing.spec.ts +++ b/frontend/tests/routing.spec.ts @@ -1,5 +1,5 @@ import { test, expect, type Page } from '@playwright/test'; -import { ROUTES, listingPath, negotiationPath, ADMIN_CONVERSATION_SLUG, conversationPath, newConversationPath, paymentPath } from '../src/routes'; +import { ROUTES, listingPath, negotiationPath, ADMIN_CONVERSATION_SLUG, conversationPath, newConversationPath, paymentPath, priceHistoryPath } from '../src/routes'; // Standard projektu (AGENTS.md): kazdy widok ma wlasny URL obslugiwany przez React Router, // a nawigacja odbywa sie przez /. Te testy pilnuja tej zasady. @@ -748,3 +748,28 @@ test.describe('Nawigacja mobilna', () => { await expect(page.locator('.nav')).toBeVisible(); }); }); + +// Dzielnica bez wlasnych ogloszen nadal ma na wykresie ceny transakcyjne dla calego miasta - +// naglowek i rubryki licza wtedy srednia z NBP i GUS, zamiast pokazywac same kreski. +test.describe('Historia cen', () => { + test('rubryki pokazuja liczby takze tam, gdzie nie ma jeszcze ogloszen', async ({ page }) => { + await goto(page, priceHistoryPath('Gdańsk, Wrzeszcz')); + + await expect(page.locator('.history-chart-title-row strong')).not.toHaveText('—'); + await expect(page.locator('.history-source-note')).toContainText(/NBP i GUS|Polska Lokalnie/); + + // Najnizsza, najwyzsza i srednia maja sens przy jednym punkcie; zmiana w okresie wymaga dwoch, + // wiec jej nie sprawdzamy - ma wtedy wlasny podpis "Za krotka historia". + const values = page.locator('.history-stat-grid article strong'); + await expect(values).toHaveCount(4); + for (const index of [0, 1, 3]) { + await expect(values.nth(index)).not.toHaveText('—'); + } + }); + + test('bez wybranej lokalizacji nie zmyslamy zadnych liczb', async ({ page }) => { + await goto(page, ROUTES.priceHistory); + await expect(page.locator('.history-chart-title-row strong')).toHaveText('—'); + await expect(page.locator('.history-chart-empty')).toContainText('Wybierz lokalizację'); + }); +});