Historia cen - liczby takze tam, gdzie nie ma jeszcze ogloszen
Deploy Polska Lokalnie / deploy (push) Successful in 21s
Deploy Polska Lokalnie / deploy (push) Successful in 21s
Dzielnica bez wlasnych ogloszen miala na wykresie pelne serie NBP i GUS, ale naglowek i wszystkie cztery rubryki pokazywaly kreski. - brak ogloszen: naglowek i rubryki licza srednia z NBP i GUS w kwartale - podpis mowi wprost, ze to dane transakcyjne dla calego miasta - tytul, cena i zmiana procentowa staja w jednej linii - "Zmiana w okresie" przy jednym zapisie ma podpis "Za krotka historia" Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+40
-12
@@ -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 }) {
|
||||
<section className="history-chart-card">
|
||||
<div className="history-chart-head">
|
||||
<div>
|
||||
<h2>Średnia cena za {areaSuffix} w {activeLocationLabel}</h2>
|
||||
<strong>{latestKnown ? formatPricePerArea(latestKnown.price) : '—'}</strong>
|
||||
{changePercent != null && (
|
||||
<p className={changePercent >= 0 ? 'positive' : 'negative'}>
|
||||
{changePercent >= 0 ? '+' : ''}{changePercent.toFixed(1).replace('.', ',')}% <span>(od {historySlots[firstKnown!.slotIndex].sinceLabel})</span>
|
||||
</p>
|
||||
)}
|
||||
<div className="history-chart-title-row">
|
||||
<h2>Średnia cena za {areaSuffix} w {activeLocationLabel}</h2>
|
||||
<strong>{latestKnown ? formatPricePerArea(latestKnown.price) : '—'}</strong>
|
||||
{changePercent != null && firstKnown && (
|
||||
<p className={changePercent >= 0 ? 'positive' : 'negative'}>
|
||||
{changePercent >= 0 ? '+' : ''}{changePercent.toFixed(1).replace('.', ',')}% <span>(od {historySlots[firstKnown.slotIndex].sinceLabel})</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<small className="history-source-note">
|
||||
Ś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.
|
||||
</>
|
||||
)}
|
||||
</small>
|
||||
</div>
|
||||
<span className="history-chart-mode">Cena za {areaSuffix}</span>
|
||||
@@ -21630,6 +21656,8 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) {
|
||||
<span>Zmiana w okresie</span>
|
||||
<strong>{changePercent == null ? '—' : `${changePercent >= 0 ? '+' : ''}${changePercent.toFixed(1).replace('.', ',')}%`}</strong>
|
||||
{changePercent != null && firstKnown && latestKnown && <small>{formatPricePerArea(latestKnown.price - firstKnown.price)}</small>}
|
||||
{/* Jeden przedzial z danymi to jeszcze nie trend - mowimy wprost, czego brakuje. */}
|
||||
{changePercent == null && knownPrices.length > 0 && <small>Za krótka historia</small>}
|
||||
</article>
|
||||
<article><span>Średnia cena</span><strong>{averageKnown != null ? formatPricePerArea(averageKnown) : '—'}</strong></article>
|
||||
</div>
|
||||
|
||||
+13
-4
@@ -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 {
|
||||
|
||||
@@ -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 <Link>/<NavLink>. 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ę');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user