diff --git a/backend/src/main/java/pl/polskalokalnie/user/UserSettings.java b/backend/src/main/java/pl/polskalokalnie/user/UserSettings.java index 4f2d993..06a525f 100644 --- a/backend/src/main/java/pl/polskalokalnie/user/UserSettings.java +++ b/backend/src/main/java/pl/polskalokalnie/user/UserSettings.java @@ -114,6 +114,14 @@ public class UserSettings { @ColumnDefault("false") private boolean notifyProductNews = false; + /** + * Lokalizacje ostatnio sprawdzone w Historii cen (JSON, maks. kilka pozycji). Lista jest + * na koncie, a nie w przegladarce, wiec porownanie cen wyglada tak samo na kazdym urzadzeniu. + * To zwykla historia przegladania - nie ma z nia zwiazanych zadnych powiadomien. + */ + @Column(name = "price_history_markets", columnDefinition = "text") + private String priceHistoryMarkets; + // --- Preferencje wyszukiwania (podpowiadane przy filtrowaniu ofert) --- @Column(length = 255) @@ -314,6 +322,14 @@ public class UserSettings { return searchLocations; } + public String getPriceHistoryMarkets() { + return priceHistoryMarkets; + } + + public void setPriceHistoryMarkets(String priceHistoryMarkets) { + this.priceHistoryMarkets = priceHistoryMarkets; + } + public void setSearchLocations(String searchLocations) { this.searchLocations = searchLocations; } diff --git a/backend/src/main/java/pl/polskalokalnie/user/UserSettingsRequest.java b/backend/src/main/java/pl/polskalokalnie/user/UserSettingsRequest.java index f509b37..e47bec2 100644 --- a/backend/src/main/java/pl/polskalokalnie/user/UserSettingsRequest.java +++ b/backend/src/main/java/pl/polskalokalnie/user/UserSettingsRequest.java @@ -25,6 +25,7 @@ public record UserSettingsRequest( Boolean notifyPriceAlerts, Boolean notifyMessages, Boolean notifyProductNews, + String priceHistoryMarkets, String searchLocations, String searchPropertyType, Integer searchBudgetMax, diff --git a/backend/src/main/java/pl/polskalokalnie/user/UserSettingsResponse.java b/backend/src/main/java/pl/polskalokalnie/user/UserSettingsResponse.java index acf5a1d..5a240fe 100644 --- a/backend/src/main/java/pl/polskalokalnie/user/UserSettingsResponse.java +++ b/backend/src/main/java/pl/polskalokalnie/user/UserSettingsResponse.java @@ -22,6 +22,7 @@ public record UserSettingsResponse( boolean notifyPriceAlerts, boolean notifyMessages, boolean notifyProductNews, + String priceHistoryMarkets, String searchLocations, String searchPropertyType, Integer searchBudgetMax, @@ -52,6 +53,7 @@ public record UserSettingsResponse( settings.isNotifyPriceAlerts(), settings.isNotifyMessages(), settings.isNotifyProductNews(), + settings.getPriceHistoryMarkets(), settings.getSearchLocations(), settings.getSearchPropertyType(), settings.getSearchBudgetMax(), diff --git a/backend/src/main/java/pl/polskalokalnie/user/UserSettingsService.java b/backend/src/main/java/pl/polskalokalnie/user/UserSettingsService.java index df02372..47c8d6f 100644 --- a/backend/src/main/java/pl/polskalokalnie/user/UserSettingsService.java +++ b/backend/src/main/java/pl/polskalokalnie/user/UserSettingsService.java @@ -135,6 +135,10 @@ public class UserSettingsService { if (request.notifyProductNews() != null) { settings.setNotifyProductNews(request.notifyProductNews()); } + if (request.priceHistoryMarkets() != null) { + String markets = request.priceHistoryMarkets().trim(); + settings.setPriceHistoryMarkets(markets.isEmpty() ? null : markets); + } if (request.searchLocations() != null) { String locations = request.searchLocations().trim(); textModerationService.validateOrThrow(locations); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e05af14..6edb6e5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -444,7 +444,27 @@ function toSavedSearchPayload(search: SavedSearchRecord | SavedSearchDraft) { }; } // Lokalizacje, ktorych cene za m2 sprawdzil uzytkownik - najnowsza pierwsza, przechowywane per konto. +// Klucz zostaje tylko po to, zeby raz przeniesc na konto lokalizacje zapisane w przegladarce +// przed przeniesieniem tej listy na serwer. const PRICE_HISTORY_MARKETS_SUFFIX = 'price-history-markets'; + +function parsePriceHistoryMarkets(raw: string | null): PriceHistoryMarket[] { + if (!raw) { + return []; + } + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed.filter((item) => item && typeof item.key === 'string') : []; + } catch { + return []; + } +} + +/** Lista lokalizacji z Historii cen jest ustawieniem konta - zapisujemy ja przy kazdej zmianie. */ +function savePriceHistoryMarkets(markets: PriceHistoryMarket[]): void { + apiFetch('/me/settings', { method: 'PUT', body: JSON.stringify({ priceHistoryMarkets: JSON.stringify(markets) }) }) + .catch(() => { /* brak sieci - lista wroci do stanu z konta przy nastepnym wejsciu */ }); +} const PRICE_HISTORY_MARKETS_LIMIT = 5; const PRICE_ALERT_EVENTS_LIMIT = 40; const SAVED_SEARCHES_LIMIT = 60; @@ -21484,9 +21504,33 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) { const [suggestLoading, setSuggestLoading] = useState(false); const [hoveredHistoryIndex, setHoveredHistoryIndex] = useState(null); // Historia sprawdzonych lokalizacji tego konta - najnowsza na poczatku, maksymalnie 5 pozycji. - const [recentMarkets, setRecentMarkets] = useState( - () => readUserJsonStorage(user, PRICE_HISTORY_MARKETS_SUFFIX, []), - ); + // Lista siedzi w ustawieniach konta (/api/me/settings), wiec porownanie wyglada tak samo + // na kazdym urzadzeniu. Wpisy z przegladarki przenosimy raz, przy pierwszym wejsciu. + const [recentMarkets, setRecentMarkets] = useState([]); + useEffect(() => { + if (!user) { + setRecentMarkets([]); + return; + } + let cancelled = false; + const local = readUserJsonStorage(user, PRICE_HISTORY_MARKETS_SUFFIX, []); + apiFetch<{ priceHistoryMarkets: string | null }>('/me/settings') + .then((settings) => { + if (cancelled) { + return; + } + const fromAccount = parsePriceHistoryMarkets(settings.priceHistoryMarkets); + if (fromAccount.length === 0 && local.length > 0) { + setRecentMarkets(local); + savePriceHistoryMarkets(local); + } else { + setRecentMarkets(fromAccount); + } + writeUserJsonStorage(user, PRICE_HISTORY_MARKETS_SUFFIX, []); + }) + .catch(() => { if (!cancelled) { setRecentMarkets(local); } }); + return () => { cancelled = true; }; + }, [user]); // Ostatnio otwarte oferte (sprzedaz i wynajem) - wspolny rejestr zasilany przy wejsciu na oferte. const recentListings = useMemo(() => readRecentlyViewed(user).slice(0, RECENTLY_VIEWED_PANEL_LIMIT), [user]); const skipNextFetch = useRef(false); @@ -21719,7 +21763,7 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) { // Nowa lokalizacja wchodzi na poczatek listy, najstarsza wypada po przekroczeniu limitu. setRecentMarkets((current) => { const next = [market, ...current.filter((item) => item.key !== market.key)].slice(0, PRICE_HISTORY_MARKETS_LIMIT); - writeUserJsonStorage(user, PRICE_HISTORY_MARKETS_SUFFIX, next); + savePriceHistoryMarkets(next); return next; }); }; @@ -21729,18 +21773,18 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) { }; // Usuwanie pojedynczej lokalizacji z porownania i czyszczenie calej listy - zapis leci od razu - // do pamieci konta, zeby stan zgadzal sie po odswiezeniu strony. + // na konto, zeby stan zgadzal sie po odswiezeniu i na innym urzadzeniu. const removeComparison = (marketKey: string) => { setRecentMarkets((current) => { const next = current.filter((item) => item.key !== marketKey); - writeUserJsonStorage(user, PRICE_HISTORY_MARKETS_SUFFIX, next); + savePriceHistoryMarkets(next); return next; }); }; const clearComparisons = () => { setRecentMarkets([]); - writeUserJsonStorage(user, PRICE_HISTORY_MARKETS_SUFFIX, []); + savePriceHistoryMarkets([]); }; // Wejscie z adresu z parametrami (np. "Zobacz historie cen" przy alercie lokalizacji) diff --git a/frontend/src/auth.tsx b/frontend/src/auth.tsx index 412fd66..6cd1547 100644 --- a/frontend/src/auth.tsx +++ b/frontend/src/auth.tsx @@ -37,6 +37,8 @@ export type UserSettings = { notifyPriceAlerts: boolean; notifyMessages: boolean; notifyProductNews: boolean; + // Lokalizacje sprawdzone w Historii cen (JSON) - historia przegladania, bez powiadomien. + priceHistoryMarkets: string | null; searchLocations: string | null; searchPropertyType: string | null; searchBudgetMax: number | null; diff --git a/frontend/tests/routing.spec.ts b/frontend/tests/routing.spec.ts index 416301b..44c4352 100644 --- a/frontend/tests/routing.spec.ts +++ b/frontend/tests/routing.spec.ts @@ -597,3 +597,25 @@ test.describe('Ulubione', () => { await expect(page.locator('main')).not.toContainText(title.trim().slice(0, 20)); }); }); + +// Lista sprawdzonych lokalizacji w Historii cen jest ustawieniem konta, nie pamiecia przegladarki. +test.describe('Historia cen', () => { + test('sprawdzone lokalizacje przezywaja wyczyszczenie pamieci przegladarki', async ({ page }) => { + await login(page); + await goto(page, ROUTES.priceHistory); + await page.locator('.history-location-display').click(); + await page.locator('.history-location-pop input').fill('Kraków'); + await page.locator('.history-location-pop button', { hasText: 'Kraków' }).first().click(); + await expect(page.locator('main')).toContainText('Kraków'); + + await page.evaluate(() => { + const token = window.localStorage.getItem('polskalokalnie-auth-token'); + window.localStorage.clear(); + if (token) { + window.localStorage.setItem('polskalokalnie-auth-token', token); + } + }); + await goto(page, ROUTES.priceHistory); + await expect(page.locator('main')).toContainText('Kraków'); + }); +});