panel glowny poprawki
This commit is contained in:
@@ -31,6 +31,10 @@ import pl.polskalokalnie.notification.NotificationService;
|
||||
import pl.polskalokalnie.report.ListingReportResponse;
|
||||
import pl.polskalokalnie.report.ListingReportService;
|
||||
import pl.polskalokalnie.report.ResolveListingReportRequest;
|
||||
import pl.polskalokalnie.report.UserReportResponse;
|
||||
import pl.polskalokalnie.report.UserReportService;
|
||||
import pl.polskalokalnie.support.SupportMessageResponse;
|
||||
import pl.polskalokalnie.support.SupportMessageService;
|
||||
import pl.polskalokalnie.user.AppUser;
|
||||
import pl.polskalokalnie.user.BlockedEmail;
|
||||
import pl.polskalokalnie.user.BlockedEmailRepository;
|
||||
@@ -49,6 +53,8 @@ public class AdminController {
|
||||
private final BlockedEmailRepository blockedEmailRepository;
|
||||
private final ListingService listingService;
|
||||
private final ListingReportService listingReportService;
|
||||
private final UserReportService userReportService;
|
||||
private final SupportMessageService supportMessageService;
|
||||
private final MessageService messageService;
|
||||
private final TextModerationService textModerationService;
|
||||
private final AdminStatsService adminStatsService;
|
||||
@@ -57,6 +63,8 @@ public class AdminController {
|
||||
|
||||
public AdminController(UserRepository userRepository, BlockedEmailRepository blockedEmailRepository,
|
||||
ListingService listingService, ListingReportService listingReportService,
|
||||
UserReportService userReportService,
|
||||
SupportMessageService supportMessageService,
|
||||
MessageService messageService,
|
||||
TextModerationService textModerationService,
|
||||
AdminStatsService adminStatsService,
|
||||
@@ -66,6 +74,8 @@ public class AdminController {
|
||||
this.blockedEmailRepository = blockedEmailRepository;
|
||||
this.listingService = listingService;
|
||||
this.listingReportService = listingReportService;
|
||||
this.userReportService = userReportService;
|
||||
this.supportMessageService = supportMessageService;
|
||||
this.messageService = messageService;
|
||||
this.textModerationService = textModerationService;
|
||||
this.adminStatsService = adminStatsService;
|
||||
@@ -263,6 +273,43 @@ public class AdminController {
|
||||
return listingReportService.deleteListingAndResolve(id, authentication.getName());
|
||||
}
|
||||
|
||||
// --- Zgloszenia uzytkownikow (z rozmow w wiadomosciach) ---
|
||||
|
||||
@GetMapping("/reports/users")
|
||||
public List<UserReportResponse> userReports() {
|
||||
return userReportService.findAllForAdmin();
|
||||
}
|
||||
|
||||
@PostMapping("/reports/users/{id}/resolve")
|
||||
public UserReportResponse resolveUserReport(
|
||||
@PathVariable Long id,
|
||||
Authentication authentication,
|
||||
@RequestBody(required = false) ResolveListingReportRequest request
|
||||
) {
|
||||
return userReportService.resolve(id, authentication.getName(), request == null ? null : request.note());
|
||||
}
|
||||
|
||||
@PostMapping("/reports/users/{id}/delete-user")
|
||||
public UserReportResponse deleteUserFromReport(@PathVariable Long id, Authentication authentication) {
|
||||
return userReportService.deleteReportedUserAndResolve(id, authentication.getName());
|
||||
}
|
||||
|
||||
// --- Wiadomosci z formularza "Napisz do nas" (/konto/pomoc) ---
|
||||
|
||||
@GetMapping("/support-messages")
|
||||
public List<SupportMessageResponse> supportMessages() {
|
||||
return supportMessageService.findAllForAdmin();
|
||||
}
|
||||
|
||||
@PostMapping("/support-messages/{id}/handled")
|
||||
public SupportMessageResponse setSupportMessageHandled(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(defaultValue = "true") boolean handled,
|
||||
Authentication authentication
|
||||
) {
|
||||
return supportMessageService.setHandled(id, handled, authentication.getName());
|
||||
}
|
||||
|
||||
// --- Slowa zabronione ---
|
||||
|
||||
@GetMapping("/forbidden-words")
|
||||
|
||||
@@ -48,6 +48,18 @@ public class SchemaFixer {
|
||||
"ALTER TABLE IF EXISTS listing_report_attachments ADD COLUMN IF NOT EXISTS data_url TEXT"
|
||||
);
|
||||
|
||||
// Hibernate (ddl-auto=update) nie potrafi dodac kolumny NOT NULL do tabeli z danymi -
|
||||
// dokladamy ja z wartoscia domyslna, zeby istniejace zgloszenia sie nie wywracaly.
|
||||
jdbcTemplate.execute(
|
||||
"ALTER TABLE IF EXISTS user_reports ADD COLUMN IF NOT EXISTS reported_user_deleted BOOLEAN NOT NULL DEFAULT FALSE"
|
||||
);
|
||||
|
||||
// Blokowanie konta ze zgloszenia zostalo usuniete - encja nie mapuje juz tej kolumny,
|
||||
// wiec stare wiersze zostaja, a nowe wpisy wypelnia wartosc domyslna.
|
||||
jdbcTemplate.execute(
|
||||
"ALTER TABLE IF EXISTS user_reports ALTER COLUMN reported_user_blocked SET DEFAULT FALSE"
|
||||
);
|
||||
|
||||
// Hibernate (ddl-auto=update) nie aktualizuje CHECK constraintu enuma po dodaniu nowej wartosci.
|
||||
// Odtwarzamy go tak, aby dopuszczal status PAUSED (wstrzymane ogloszenie uzytkownika).
|
||||
jdbcTemplate.execute(
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package pl.polskalokalnie.listing;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Ranking miast i dzielnic liczony z aktualnie opublikowanych ogloszen.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/listings")
|
||||
public class AreaRankingController {
|
||||
|
||||
private static final int TREND_WINDOW_DAYS = 90;
|
||||
private static final int NEW_WINDOW_DAYS = 30;
|
||||
private static final int MIN_LISTINGS_FOR_TREND = 2;
|
||||
|
||||
private final ListingRepository listingRepository;
|
||||
private final ListingFavoriteRepository favoriteRepository;
|
||||
|
||||
public AreaRankingController(ListingRepository listingRepository, ListingFavoriteRepository favoriteRepository) {
|
||||
this.listingRepository = listingRepository;
|
||||
this.favoriteRepository = favoriteRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/areas")
|
||||
public AreaRankingResponse areas(
|
||||
@RequestParam(required = false) OfferType offerType,
|
||||
@RequestParam(required = false) PropertyType propertyType
|
||||
) {
|
||||
List<PropertyListing> listings = listingRepository.findAll().stream()
|
||||
.filter(listing -> listing.getStatus() == ListingStatus.APPROVED)
|
||||
.filter(listing -> listing.getPrice() != null && listing.getPrice().doubleValue() > 0)
|
||||
.filter(listing -> listing.getArea() != null && listing.getArea() > 0)
|
||||
.filter(listing -> listing.getCity() != null && !listing.getCity().isBlank())
|
||||
.filter(listing -> offerType == null || listing.getOfferType() == offerType)
|
||||
.filter(listing -> propertyType == null || listing.getPropertyType() == propertyType)
|
||||
.toList();
|
||||
|
||||
Map<Long, Long> savesByListing = savesFor(listings);
|
||||
|
||||
List<AreaRankingResponse.AreaStats> cities = group(
|
||||
listings, listing -> listing.getCity().trim(), savesByListing, false);
|
||||
List<AreaRankingResponse.AreaStats> districts = group(
|
||||
listings.stream().filter(listing -> listing.getDistrict() != null && !listing.getDistrict().isBlank()).toList(),
|
||||
listing -> listing.getCity().trim() + "|" + listing.getDistrict().trim(), savesByListing, true);
|
||||
|
||||
return new AreaRankingResponse(Instant.now(), listings.size(), cities, districts);
|
||||
}
|
||||
|
||||
private Map<Long, Long> savesFor(List<PropertyListing> listings) {
|
||||
List<Long> ids = listings.stream().map(PropertyListing::getId).toList();
|
||||
if (ids.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
return favoriteRepository.findByListingIdIn(ids).stream()
|
||||
.collect(Collectors.groupingBy(ListingFavorite::getListingId, Collectors.counting()));
|
||||
}
|
||||
|
||||
private List<AreaRankingResponse.AreaStats> group(
|
||||
List<PropertyListing> listings,
|
||||
Function<PropertyListing, String> keyOf,
|
||||
Map<Long, Long> savesByListing,
|
||||
boolean withDistrict
|
||||
) {
|
||||
Map<String, List<PropertyListing>> grouped = listings.stream()
|
||||
.collect(Collectors.groupingBy(keyOf, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
List<AreaRankingResponse.AreaStats> stats = new ArrayList<>();
|
||||
Instant newSince = Instant.now().minus(NEW_WINDOW_DAYS, ChronoUnit.DAYS);
|
||||
Instant trendSince = Instant.now().minus(TREND_WINDOW_DAYS, ChronoUnit.DAYS);
|
||||
|
||||
grouped.forEach((key, items) -> {
|
||||
String city = withDistrict ? key.substring(0, key.indexOf('|')) : key;
|
||||
String district = withDistrict ? key.substring(key.indexOf('|') + 1) : null;
|
||||
|
||||
List<Double> pricesPerM2 = items.stream().map(AreaRankingController::pricePerM2).sorted().toList();
|
||||
double avg = pricesPerM2.stream().mapToDouble(Double::doubleValue).average().orElse(0);
|
||||
|
||||
long views = items.stream().mapToLong(item -> item.getViewsCount() == null ? 0L : item.getViewsCount()).sum();
|
||||
long saves = items.stream().mapToLong(item -> savesByListing.getOrDefault(item.getId(), 0L)).sum();
|
||||
int newCount = (int) items.stream().filter(item -> item.getCreatedAt() != null && item.getCreatedAt().isAfter(newSince)).count();
|
||||
|
||||
stats.add(new AreaRankingResponse.AreaStats(
|
||||
city,
|
||||
district,
|
||||
items.size(),
|
||||
Math.round(avg),
|
||||
Math.round(pricesPerM2.get(0)),
|
||||
Math.round(pricesPerM2.get(pricesPerM2.size() - 1)),
|
||||
Math.round(items.stream().mapToDouble(PropertyListing::getArea).average().orElse(0) * 10) / 10.0,
|
||||
views,
|
||||
saves,
|
||||
newCount,
|
||||
trendPercent(items, trendSince),
|
||||
averageCoordinate(items, PropertyListing::getLat),
|
||||
averageCoordinate(items, PropertyListing::getLng)
|
||||
));
|
||||
});
|
||||
|
||||
stats.sort(Comparator.comparingLong(AreaRankingResponse.AreaStats::avgPricePerM2).reversed());
|
||||
return stats;
|
||||
}
|
||||
|
||||
/** Zmiana sredniej ceny za m2 miedzy ofertami z ostatnich 90 dni a starszymi. */
|
||||
private static Double trendPercent(List<PropertyListing> items, Instant trendSince) {
|
||||
List<Double> recent = items.stream()
|
||||
.filter(item -> item.getCreatedAt() != null && item.getCreatedAt().isAfter(trendSince))
|
||||
.map(AreaRankingController::pricePerM2)
|
||||
.toList();
|
||||
List<Double> older = items.stream()
|
||||
.filter(item -> item.getCreatedAt() != null && !item.getCreatedAt().isAfter(trendSince))
|
||||
.map(AreaRankingController::pricePerM2)
|
||||
.toList();
|
||||
|
||||
if (recent.size() < MIN_LISTINGS_FOR_TREND || older.size() < MIN_LISTINGS_FOR_TREND) {
|
||||
return null;
|
||||
}
|
||||
double recentAvg = recent.stream().mapToDouble(Double::doubleValue).average().orElse(0);
|
||||
double olderAvg = older.stream().mapToDouble(Double::doubleValue).average().orElse(0);
|
||||
if (olderAvg <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.round(((recentAvg - olderAvg) / olderAvg) * 1000) / 10.0;
|
||||
}
|
||||
|
||||
private static Double averageCoordinate(List<PropertyListing> items, Function<PropertyListing, Double> getter) {
|
||||
List<Double> values = items.stream().map(getter).filter(java.util.Objects::nonNull).toList();
|
||||
if (values.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return values.stream().mapToDouble(Double::doubleValue).average().orElse(0);
|
||||
}
|
||||
|
||||
private static double pricePerM2(PropertyListing listing) {
|
||||
return listing.getPrice().doubleValue() / listing.getArea();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package pl.polskalokalnie.listing;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ranking obszarow liczony na biezaco z opublikowanych ogloszen. Kazde dodane, zatwierdzone,
|
||||
* wstrzymane lub usuniete ogloszenie zmienia wynik - nie ma tu zadnych danych wpisanych na sztywno.
|
||||
*/
|
||||
public record AreaRankingResponse(
|
||||
Instant generatedAt,
|
||||
int totalListings,
|
||||
List<AreaStats> cities,
|
||||
List<AreaStats> districts
|
||||
) {
|
||||
public record AreaStats(
|
||||
String city,
|
||||
// null dla wiersza opisujacego cale miasto
|
||||
String district,
|
||||
int listingsCount,
|
||||
long avgPricePerM2,
|
||||
long minPricePerM2,
|
||||
long maxPricePerM2,
|
||||
double avgArea,
|
||||
long totalViews,
|
||||
long totalSaves,
|
||||
int newLast30Days,
|
||||
// Zmiana sredniej ceny za m2: ostatnie 90 dni wobec starszych ofert.
|
||||
// null, gdy po ktorejkolwiek stronie jest mniej niz 2 oferty - wtedy trend nic nie znaczy.
|
||||
Double trendPercent,
|
||||
Double lat,
|
||||
Double lng
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -40,12 +40,17 @@ public class ListingController {
|
||||
return listingService.findMine(authentication.getName());
|
||||
}
|
||||
|
||||
// Endpoint jest publiczny - authentication bywa puste, a wtedy widac tylko ogloszenia opublikowane.
|
||||
@GetMapping("/{id}")
|
||||
public ListingDetailResponse getById(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(defaultValue = "false") boolean incrementView
|
||||
@RequestParam(defaultValue = "false") boolean incrementView,
|
||||
Authentication authentication
|
||||
) {
|
||||
return listingService.getById(id, incrementView);
|
||||
String viewerEmail = authentication == null ? null : authentication.getName();
|
||||
boolean viewerIsAdmin = authentication != null && authentication.getAuthorities().stream()
|
||||
.anyMatch(authority -> "ROLE_ADMIN".equals(authority.getAuthority()));
|
||||
return listingService.getById(id, incrementView, viewerEmail, viewerIsAdmin);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
|
||||
@@ -46,12 +46,15 @@ public record ListingDetailResponse(
|
||||
List<String> photos,
|
||||
String virtualTourUrl,
|
||||
String ownerEmail,
|
||||
// Konto wlasciciela, gdy ogloszenie nalezy do zarejestrowanego uzytkownika - po tym
|
||||
// identyfikatorze frontend otwiera jego profil publiczny.
|
||||
Long ownerId,
|
||||
ListingStatus status,
|
||||
Instant createdAt,
|
||||
Long viewsCount,
|
||||
Instant promotedUntil
|
||||
) {
|
||||
public static ListingDetailResponse from(PropertyListing listing) {
|
||||
public static ListingDetailResponse from(PropertyListing listing, Long ownerId) {
|
||||
return new ListingDetailResponse(
|
||||
listing.getId(),
|
||||
listing.getTitle(),
|
||||
@@ -90,6 +93,7 @@ public record ListingDetailResponse(
|
||||
List.copyOf(listing.getPhotos()),
|
||||
listing.getVirtualTourUrl(),
|
||||
listing.getOwnerEmail(),
|
||||
ownerId,
|
||||
listing.getStatus(),
|
||||
listing.getCreatedAt(),
|
||||
listing.getViewsCount(),
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package pl.polskalokalnie.listing;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Zapisanie ogloszenia do ulubionych przez uzytkownika. Trzymane po stronie serwera, zeby
|
||||
* wlasciciel ogloszenia widzial realna liczbe zapisan, a nie tylko stan swojej przegladarki.
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
name = "listing_favorites",
|
||||
uniqueConstraints = @UniqueConstraint(name = "uk_listing_favorite", columnNames = {"listing_id", "user_email"})
|
||||
)
|
||||
public class ListingFavorite {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "listing_id", nullable = false)
|
||||
private Long listingId;
|
||||
|
||||
@Column(name = "user_email", nullable = false, length = 180)
|
||||
private String userEmail;
|
||||
|
||||
@Column(nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = Instant.now();
|
||||
}
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Long getListingId() {
|
||||
return listingId;
|
||||
}
|
||||
|
||||
public void setListingId(Long listingId) {
|
||||
this.listingId = listingId;
|
||||
}
|
||||
|
||||
public String getUserEmail() {
|
||||
return userEmail;
|
||||
}
|
||||
|
||||
public void setUserEmail(String userEmail) {
|
||||
this.userEmail = userEmail;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package pl.polskalokalnie.listing;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
/**
|
||||
* Zapisywanie ogloszen do ulubionych. Frontend trzyma liste ulubionych lokalnie (szybkie UI),
|
||||
* a tutaj rejestrujemy sam fakt zapisania - dzieki temu wlasciciel widzi, ile osob zapisalo oferte.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/listings")
|
||||
public class ListingFavoriteController {
|
||||
|
||||
private final ListingFavoriteRepository favoriteRepository;
|
||||
private final ListingRepository listingRepository;
|
||||
|
||||
public ListingFavoriteController(ListingFavoriteRepository favoriteRepository, ListingRepository listingRepository) {
|
||||
this.favoriteRepository = favoriteRepository;
|
||||
this.listingRepository = listingRepository;
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/favorite")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> addFavorite(@PathVariable Long id, Authentication authentication) {
|
||||
if (!listingRepository.existsById(id)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Listing not found");
|
||||
}
|
||||
save(id, authentication.getName());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/favorite")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> removeFavorite(@PathVariable Long id, Authentication authentication) {
|
||||
favoriteRepository.deleteByListingIdAndUserEmailIgnoreCase(id, authentication.getName());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ustawia ulubione uzytkownika dokladnie na przeslana liste. Front trzyma ulubione lokalnie
|
||||
* i po kazdej zmianie wysyla caly zbior - dzieki temu serwer zna stan niezaleznie od tego,
|
||||
* z ktorego miejsca w aplikacji uzytkownik dodal lub usunal ogloszenie.
|
||||
*/
|
||||
@PostMapping("/favorites/sync")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> sync(@RequestBody SyncRequest request, Authentication authentication) {
|
||||
if (request == null || request.listingIds() == null) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
String userEmail = authentication.getName();
|
||||
List<Long> wanted = request.listingIds().stream()
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.distinct()
|
||||
.filter(listingRepository::existsById)
|
||||
.toList();
|
||||
|
||||
favoriteRepository.findByUserEmailIgnoreCase(userEmail).stream()
|
||||
.filter(favorite -> !wanted.contains(favorite.getListingId()))
|
||||
.forEach(favoriteRepository::delete);
|
||||
|
||||
wanted.forEach(listingId -> save(listingId, userEmail));
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/** Liczba zapisan dla ogloszen zalogowanego uzytkownika: {listingId: liczba}. */
|
||||
@GetMapping("/mine/saves")
|
||||
public Map<Long, Long> mySaves(Authentication authentication) {
|
||||
List<Long> myListingIds = listingRepository.findAll().stream()
|
||||
.filter(listing -> authentication.getName().equalsIgnoreCase(listing.getOwnerEmail()))
|
||||
.map(PropertyListing::getId)
|
||||
.toList();
|
||||
|
||||
Map<Long, Long> counts = new HashMap<>();
|
||||
myListingIds.forEach(listingId -> counts.put(listingId, 0L));
|
||||
if (myListingIds.isEmpty()) {
|
||||
return counts;
|
||||
}
|
||||
favoriteRepository.findByListingIdIn(myListingIds)
|
||||
.forEach(favorite -> counts.merge(favorite.getListingId(), 1L, Long::sum));
|
||||
return counts;
|
||||
}
|
||||
|
||||
private void save(Long listingId, String userEmail) {
|
||||
if (favoriteRepository.existsByListingIdAndUserEmailIgnoreCase(listingId, userEmail)) {
|
||||
return;
|
||||
}
|
||||
ListingFavorite favorite = new ListingFavorite();
|
||||
favorite.setListingId(listingId);
|
||||
favorite.setUserEmail(userEmail);
|
||||
favoriteRepository.save(favorite);
|
||||
}
|
||||
|
||||
public record SyncRequest(List<Long> listingIds) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package pl.polskalokalnie.listing;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ListingFavoriteRepository extends JpaRepository<ListingFavorite, Long> {
|
||||
|
||||
boolean existsByListingIdAndUserEmailIgnoreCase(Long listingId, String userEmail);
|
||||
|
||||
void deleteByListingIdAndUserEmailIgnoreCase(Long listingId, String userEmail);
|
||||
|
||||
long countByListingId(Long listingId);
|
||||
|
||||
List<ListingFavorite> findByUserEmailIgnoreCase(String userEmail);
|
||||
|
||||
List<ListingFavorite> findByListingIdIn(List<Long> listingIds);
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package pl.polskalokalnie.listing;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ListingRepository extends JpaRepository<PropertyListing, Long> {
|
||||
|
||||
List<PropertyListing> findByOwnerEmailIgnoreCaseAndStatusOrderByCreatedAtDesc(String ownerEmail, ListingStatus status);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import pl.polskalokalnie.moderation.TextModerationService;
|
||||
import pl.polskalokalnie.notification.NotificationService;
|
||||
import pl.polskalokalnie.user.AppUser;
|
||||
import pl.polskalokalnie.user.UserRepository;
|
||||
|
||||
@Service
|
||||
public class ListingService {
|
||||
@@ -20,12 +22,22 @@ public class ListingService {
|
||||
private final ListingRepository listingRepository;
|
||||
private final TextModerationService textModerationService;
|
||||
private final NotificationService notificationService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public ListingService(ListingRepository listingRepository, TextModerationService textModerationService,
|
||||
NotificationService notificationService) {
|
||||
NotificationService notificationService, UserRepository userRepository) {
|
||||
this.listingRepository = listingRepository;
|
||||
this.textModerationService = textModerationService;
|
||||
this.notificationService = notificationService;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
// Konto wlasciciela ogloszenia - null, gdy ogloszenie nie ma odpowiednika wsrod uzytkownikow.
|
||||
private Long ownerIdOf(PropertyListing listing) {
|
||||
if (listing.getOwnerEmail() == null || listing.getOwnerEmail().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return userRepository.findByEmailIgnoreCase(listing.getOwnerEmail()).map(AppUser::getId).orElse(null);
|
||||
}
|
||||
|
||||
// Promowane (aktywne wyroznienie) na gorze, potem najnowsze wg daty dodania.
|
||||
@@ -45,17 +57,28 @@ public class ListingService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Szczegoly ogloszenia. Publicznie dostepne sa wylacznie ogloszenia opublikowane - wstrzymane,
|
||||
* oczekujace i odrzucone widzi tylko wlasciciel i administracja, takze przy wejsciu z linku.
|
||||
*/
|
||||
@Transactional
|
||||
public ListingDetailResponse getById(Long id, boolean incrementView) {
|
||||
public ListingDetailResponse getById(Long id, boolean incrementView, String viewerEmail, boolean viewerIsAdmin) {
|
||||
PropertyListing listing = listingRepository.findById(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Listing not found"));
|
||||
|
||||
if (incrementView) {
|
||||
boolean isOwner = viewerEmail != null && listing.getOwnerEmail() != null
|
||||
&& viewerEmail.equalsIgnoreCase(listing.getOwnerEmail());
|
||||
if (listing.getStatus() != ListingStatus.APPROVED && !isOwner && !viewerIsAdmin) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Listing not found");
|
||||
}
|
||||
|
||||
// Podglad wlasnego ogloszenia nie zawyza licznika wyswietlen.
|
||||
if (incrementView && !isOwner) {
|
||||
listing.setViewsCount((listing.getViewsCount() == null ? 0L : listing.getViewsCount()) + 1L);
|
||||
listing = listingRepository.save(listing);
|
||||
}
|
||||
|
||||
return ListingDetailResponse.from(listing);
|
||||
return ListingDetailResponse.from(listing, ownerIdOf(listing));
|
||||
}
|
||||
|
||||
public List<ListingResponse> findMine(String ownerEmail) {
|
||||
@@ -110,7 +133,8 @@ public class ListingService {
|
||||
listing.setOwnerEmail(ownerEmail);
|
||||
listing.setStatus(ListingStatus.PENDING);
|
||||
|
||||
return ListingDetailResponse.from(listingRepository.save(listing));
|
||||
PropertyListing saved = listingRepository.save(listing);
|
||||
return ListingDetailResponse.from(saved, ownerIdOf(saved));
|
||||
}
|
||||
|
||||
// Edycja wlasnego ogloszenia. Waliduje tresc filtrem, ale NIE zmienia statusu -
|
||||
@@ -128,7 +152,8 @@ public class ListingService {
|
||||
applyRequest(listing, request);
|
||||
// ownerEmail i status pozostaja bez zmian.
|
||||
|
||||
return ListingDetailResponse.from(listingRepository.save(listing));
|
||||
PropertyListing saved = listingRepository.save(listing);
|
||||
return ListingDetailResponse.from(saved, ownerIdOf(saved));
|
||||
}
|
||||
|
||||
// Wspolne mapowanie pol requestu na encje (uzywane przez create i updateOwn).
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package pl.polskalokalnie.report;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import java.util.List;
|
||||
|
||||
public record CreateUserReportRequest(
|
||||
@NotBlank String reportedName,
|
||||
// Wypelniony tylko dla rozmow z istniejacym kontem - wtedy admin moze je zablokowac.
|
||||
String reportedEmail,
|
||||
String conversationId,
|
||||
// Ogloszenie, w sprawie ktorego toczyla sie rozmowa (jesli watek jest z nim powiazany).
|
||||
Long listingId,
|
||||
String listingTitle,
|
||||
@NotBlank String reasonId,
|
||||
@NotBlank String reasonTitle,
|
||||
String details,
|
||||
List<String> attachmentNames,
|
||||
List<ListingReportAttachmentPayload> attachmentFiles,
|
||||
// Migawka rozmowy dolaczona do zgloszenia.
|
||||
List<UserReportMessagePayload> conversation
|
||||
) {
|
||||
}
|
||||
@@ -12,9 +12,11 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class ListingReportController {
|
||||
|
||||
private final ListingReportService listingReportService;
|
||||
private final UserReportService userReportService;
|
||||
|
||||
public ListingReportController(ListingReportService listingReportService) {
|
||||
public ListingReportController(ListingReportService listingReportService, UserReportService userReportService) {
|
||||
this.listingReportService = listingReportService;
|
||||
this.userReportService = userReportService;
|
||||
}
|
||||
|
||||
@PostMapping("/listings")
|
||||
@@ -24,4 +26,13 @@ public class ListingReportController {
|
||||
) {
|
||||
return listingReportService.create(request, authentication.getName());
|
||||
}
|
||||
|
||||
// Zgloszenie uzytkownika z rozmowy w wiadomosciach.
|
||||
@PostMapping("/users")
|
||||
public UserReportResponse createUserReport(
|
||||
@Valid @RequestBody CreateUserReportRequest request,
|
||||
Authentication authentication
|
||||
) {
|
||||
return userReportService.create(request, authentication.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package pl.polskalokalnie.report;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -15,11 +14,6 @@ import pl.polskalokalnie.user.UserRepository;
|
||||
@Service
|
||||
public class ListingReportService {
|
||||
|
||||
private static final int MAX_ATTACHMENTS = 5;
|
||||
private static final int MAX_FILE_NAME_LENGTH = 255;
|
||||
private static final int MAX_FILE_TYPE_LENGTH = 120;
|
||||
private static final int MAX_DATA_URL_LENGTH = 16_000_000;
|
||||
|
||||
private final ListingReportRepository listingReportRepository;
|
||||
private final ListingRepository listingRepository;
|
||||
private final UserRepository userRepository;
|
||||
@@ -47,7 +41,7 @@ public class ListingReportService {
|
||||
report.setReasonId(request.reasonId().trim());
|
||||
report.setReasonTitle(request.reasonTitle().trim());
|
||||
report.setDetails(trimToNull(request.details()));
|
||||
report.setAttachments(cleanAttachments(request.attachmentNames(), request.attachmentFiles()));
|
||||
report.setAttachments(ReportAttachments.clean(request.attachmentNames(), request.attachmentFiles()));
|
||||
report.setReporterEmail(reporterEmail);
|
||||
report.setReporterName(userRepository.findByEmailIgnoreCase(reporterEmail).map(AppUser::getFullName).orElse(null));
|
||||
report.setStatus(ListingReportStatus.OPEN);
|
||||
@@ -96,72 +90,6 @@ public class ListingReportService {
|
||||
}
|
||||
|
||||
private static String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private static List<ListingReportAttachment> cleanAttachments(
|
||||
List<String> names,
|
||||
List<ListingReportAttachmentPayload> files
|
||||
) {
|
||||
List<ListingReportAttachment> cleaned = new ArrayList<>();
|
||||
|
||||
if (files != null) {
|
||||
for (ListingReportAttachmentPayload payload : files) {
|
||||
if (payload == null) {
|
||||
continue;
|
||||
}
|
||||
String fileName = truncate(trimToNull(payload.fileName()), MAX_FILE_NAME_LENGTH);
|
||||
if (fileName == null) {
|
||||
continue;
|
||||
}
|
||||
ListingReportAttachment attachment = new ListingReportAttachment();
|
||||
attachment.setFileName(fileName);
|
||||
attachment.setFileType(truncate(trimToNull(payload.fileType()), MAX_FILE_TYPE_LENGTH));
|
||||
String dataUrl = trimToNull(payload.dataUrl());
|
||||
if (dataUrl != null && dataUrl.length() > MAX_DATA_URL_LENGTH) {
|
||||
dataUrl = null;
|
||||
}
|
||||
attachment.setDataUrl(dataUrl);
|
||||
cleaned.add(attachment);
|
||||
if (cleaned.size() >= MAX_ATTACHMENTS) {
|
||||
return cleaned;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (names != null) {
|
||||
for (String name : names) {
|
||||
String trimmed = truncate(trimToNull(name), MAX_FILE_NAME_LENGTH);
|
||||
if (trimmed == null) {
|
||||
continue;
|
||||
}
|
||||
boolean exists = cleaned.stream().anyMatch(item -> trimmed.equalsIgnoreCase(item.getFileName()));
|
||||
if (exists) {
|
||||
continue;
|
||||
}
|
||||
ListingReportAttachment attachment = new ListingReportAttachment();
|
||||
attachment.setFileName(trimmed);
|
||||
cleaned.add(attachment);
|
||||
if (cleaned.size() >= MAX_ATTACHMENTS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private static String truncate(String value, int maxLength) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value.length() <= maxLength) {
|
||||
return value;
|
||||
}
|
||||
return value.substring(0, maxLength);
|
||||
return ReportAttachments.trimToNull(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package pl.polskalokalnie.report;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Czyszczenie zalacznikow zgloszenia - wspolne dla zgloszen ogloszen i uzytkownikow.
|
||||
* Nazwy plikow przycinamy do dlugosci kolumny, zbyt duzy podglad (dataUrl) odrzucamy,
|
||||
* a liczbe zalacznikow ograniczamy do MAX_ATTACHMENTS.
|
||||
*/
|
||||
final class ReportAttachments {
|
||||
|
||||
private static final int MAX_ATTACHMENTS = 5;
|
||||
private static final int MAX_FILE_NAME_LENGTH = 255;
|
||||
private static final int MAX_FILE_TYPE_LENGTH = 120;
|
||||
private static final int MAX_DATA_URL_LENGTH = 16_000_000;
|
||||
|
||||
private ReportAttachments() {
|
||||
}
|
||||
|
||||
static List<ListingReportAttachment> clean(List<String> names, List<ListingReportAttachmentPayload> files) {
|
||||
List<ListingReportAttachment> cleaned = new ArrayList<>();
|
||||
|
||||
if (files != null) {
|
||||
for (ListingReportAttachmentPayload payload : files) {
|
||||
if (payload == null) {
|
||||
continue;
|
||||
}
|
||||
String fileName = truncate(trimToNull(payload.fileName()), MAX_FILE_NAME_LENGTH);
|
||||
if (fileName == null) {
|
||||
continue;
|
||||
}
|
||||
ListingReportAttachment attachment = new ListingReportAttachment();
|
||||
attachment.setFileName(fileName);
|
||||
attachment.setFileType(truncate(trimToNull(payload.fileType()), MAX_FILE_TYPE_LENGTH));
|
||||
String dataUrl = trimToNull(payload.dataUrl());
|
||||
if (dataUrl != null && dataUrl.length() > MAX_DATA_URL_LENGTH) {
|
||||
dataUrl = null;
|
||||
}
|
||||
attachment.setDataUrl(dataUrl);
|
||||
cleaned.add(attachment);
|
||||
if (cleaned.size() >= MAX_ATTACHMENTS) {
|
||||
return cleaned;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (names != null) {
|
||||
for (String name : names) {
|
||||
String trimmed = truncate(trimToNull(name), MAX_FILE_NAME_LENGTH);
|
||||
if (trimmed == null) {
|
||||
continue;
|
||||
}
|
||||
boolean exists = cleaned.stream().anyMatch(item -> trimmed.equalsIgnoreCase(item.getFileName()));
|
||||
if (exists) {
|
||||
continue;
|
||||
}
|
||||
ListingReportAttachment attachment = new ListingReportAttachment();
|
||||
attachment.setFileName(trimmed);
|
||||
cleaned.add(attachment);
|
||||
if (cleaned.size() >= MAX_ATTACHMENTS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
static String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private static String truncate(String value, int maxLength) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value.length() <= maxLength) {
|
||||
return value;
|
||||
}
|
||||
return value.substring(0, maxLength);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package pl.polskalokalnie.report;
|
||||
|
||||
import jakarta.persistence.CollectionTable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.ElementCollection;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.OrderColumn;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Zgloszenie uzytkownika wyslane z rozmowy w wiadomosciach. Zgloszony jest opisany nazwa z watku;
|
||||
* e-mail wypelniamy tylko wtedy, gdy rozmowa dotyczy istniejacego konta - dopiero wtedy admin moze
|
||||
* zablokowac konto prosto ze zgloszenia.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "user_reports")
|
||||
public class UserReport {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 160)
|
||||
private String reportedName;
|
||||
|
||||
@Column(length = 180)
|
||||
private String reportedEmail;
|
||||
|
||||
// Identyfikator rozmowy, z ktorej wyszlo zgloszenie - kontekst dla obslugi.
|
||||
@Column(length = 120)
|
||||
private String conversationId;
|
||||
|
||||
// Ogloszenie, w sprawie ktorego toczyla sie rozmowa (jesli watek jest z nim powiazany).
|
||||
private Long listingId;
|
||||
|
||||
@Column(length = 180)
|
||||
private String listingTitle;
|
||||
|
||||
// Migawka rozmowy z chwili zgloszenia - dowod dla obslugi.
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "user_report_messages", joinColumns = @JoinColumn(name = "report_id"))
|
||||
@OrderColumn(name = "position")
|
||||
private List<UserReportMessage> conversation = new ArrayList<>();
|
||||
|
||||
@Column(nullable = false, length = 80)
|
||||
private String reasonId;
|
||||
|
||||
@Column(nullable = false, length = 180)
|
||||
private String reasonTitle;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String details;
|
||||
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "user_report_attachments", joinColumns = @JoinColumn(name = "report_id"))
|
||||
private List<ListingReportAttachment> attachments = new ArrayList<>();
|
||||
|
||||
@Column(nullable = false, length = 180)
|
||||
private String reporterEmail;
|
||||
|
||||
@Column(length = 120)
|
||||
private String reporterName;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 20)
|
||||
private ListingReportStatus status = ListingReportStatus.OPEN;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean reportedUserDeleted = false;
|
||||
|
||||
@Column(nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
private Instant resolvedAt;
|
||||
|
||||
@Column(length = 180)
|
||||
private String resolvedByEmail;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String resolutionNote;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = Instant.now();
|
||||
}
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getReportedName() {
|
||||
return reportedName;
|
||||
}
|
||||
|
||||
public void setReportedName(String reportedName) {
|
||||
this.reportedName = reportedName;
|
||||
}
|
||||
|
||||
public String getReportedEmail() {
|
||||
return reportedEmail;
|
||||
}
|
||||
|
||||
public void setReportedEmail(String reportedEmail) {
|
||||
this.reportedEmail = reportedEmail;
|
||||
}
|
||||
|
||||
public String getConversationId() {
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
public void setConversationId(String conversationId) {
|
||||
this.conversationId = conversationId;
|
||||
}
|
||||
|
||||
public Long getListingId() {
|
||||
return listingId;
|
||||
}
|
||||
|
||||
public void setListingId(Long listingId) {
|
||||
this.listingId = listingId;
|
||||
}
|
||||
|
||||
public String getListingTitle() {
|
||||
return listingTitle;
|
||||
}
|
||||
|
||||
public void setListingTitle(String listingTitle) {
|
||||
this.listingTitle = listingTitle;
|
||||
}
|
||||
|
||||
public List<UserReportMessage> getConversation() {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
public void setConversation(List<UserReportMessage> conversation) {
|
||||
this.conversation = conversation == null ? new ArrayList<>() : new ArrayList<>(conversation);
|
||||
}
|
||||
|
||||
public boolean isReportedUserDeleted() {
|
||||
return reportedUserDeleted;
|
||||
}
|
||||
|
||||
public void setReportedUserDeleted(boolean reportedUserDeleted) {
|
||||
this.reportedUserDeleted = reportedUserDeleted;
|
||||
}
|
||||
|
||||
public String getReasonId() {
|
||||
return reasonId;
|
||||
}
|
||||
|
||||
public void setReasonId(String reasonId) {
|
||||
this.reasonId = reasonId;
|
||||
}
|
||||
|
||||
public String getReasonTitle() {
|
||||
return reasonTitle;
|
||||
}
|
||||
|
||||
public void setReasonTitle(String reasonTitle) {
|
||||
this.reasonTitle = reasonTitle;
|
||||
}
|
||||
|
||||
public String getDetails() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public void setDetails(String details) {
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public List<ListingReportAttachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public void setAttachments(List<ListingReportAttachment> attachments) {
|
||||
this.attachments = attachments == null ? new ArrayList<>() : new ArrayList<>(attachments);
|
||||
}
|
||||
|
||||
public String getReporterEmail() {
|
||||
return reporterEmail;
|
||||
}
|
||||
|
||||
public void setReporterEmail(String reporterEmail) {
|
||||
this.reporterEmail = reporterEmail;
|
||||
}
|
||||
|
||||
public String getReporterName() {
|
||||
return reporterName;
|
||||
}
|
||||
|
||||
public void setReporterName(String reporterName) {
|
||||
this.reporterName = reporterName;
|
||||
}
|
||||
|
||||
public ListingReportStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(ListingReportStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Instant getResolvedAt() {
|
||||
return resolvedAt;
|
||||
}
|
||||
|
||||
public void setResolvedAt(Instant resolvedAt) {
|
||||
this.resolvedAt = resolvedAt;
|
||||
}
|
||||
|
||||
public String getResolvedByEmail() {
|
||||
return resolvedByEmail;
|
||||
}
|
||||
|
||||
public void setResolvedByEmail(String resolvedByEmail) {
|
||||
this.resolvedByEmail = resolvedByEmail;
|
||||
}
|
||||
|
||||
public String getResolutionNote() {
|
||||
return resolutionNote;
|
||||
}
|
||||
|
||||
public void setResolutionNote(String resolutionNote) {
|
||||
this.resolutionNote = resolutionNote;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package pl.polskalokalnie.report;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Pojedyncza wiadomosc z rozmowy dolaczona do zgloszenia uzytkownika. To migawka z chwili
|
||||
* zgloszenia - obsluga widzi dokladnie te tresc, ktora zgloszil uzytkownik, nawet gdy rozmowa
|
||||
* zostanie pozniej usunieta przez ktoras ze stron.
|
||||
*/
|
||||
@Embeddable
|
||||
public class UserReportMessage {
|
||||
|
||||
@Column(name = "from_reporter", nullable = false)
|
||||
private boolean fromReporter;
|
||||
|
||||
@Column(name = "content", columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@Column(name = "sent_at")
|
||||
private Instant sentAt;
|
||||
|
||||
@Column(name = "has_photo", nullable = false)
|
||||
private boolean hasPhoto;
|
||||
|
||||
public boolean isFromReporter() {
|
||||
return fromReporter;
|
||||
}
|
||||
|
||||
public void setFromReporter(boolean fromReporter) {
|
||||
this.fromReporter = fromReporter;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public Instant getSentAt() {
|
||||
return sentAt;
|
||||
}
|
||||
|
||||
public void setSentAt(Instant sentAt) {
|
||||
this.sentAt = sentAt;
|
||||
}
|
||||
|
||||
public boolean isHasPhoto() {
|
||||
return hasPhoto;
|
||||
}
|
||||
|
||||
public void setHasPhoto(boolean hasPhoto) {
|
||||
this.hasPhoto = hasPhoto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package pl.polskalokalnie.report;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record UserReportMessagePayload(
|
||||
boolean fromReporter,
|
||||
String content,
|
||||
Instant sentAt,
|
||||
boolean hasPhoto
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package pl.polskalokalnie.report;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface UserReportRepository extends JpaRepository<UserReport, Long> {
|
||||
|
||||
List<UserReport> findAllByOrderByCreatedAtDesc();
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package pl.polskalokalnie.report;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public record UserReportResponse(
|
||||
Long id,
|
||||
String reportedName,
|
||||
String reportedEmail,
|
||||
String conversationId,
|
||||
Long listingId,
|
||||
String listingTitle,
|
||||
String reasonId,
|
||||
String reasonTitle,
|
||||
String details,
|
||||
List<String> attachmentNames,
|
||||
List<ListingReportAttachmentResponse> attachments,
|
||||
List<UserReportMessagePayload> conversation,
|
||||
String reporterEmail,
|
||||
String reporterName,
|
||||
ListingReportStatus status,
|
||||
boolean reportedUserDeleted,
|
||||
// Czy zgloszony ma konto w serwisie - od tego zalezy, czy admin moze je zablokowac lub usunac.
|
||||
boolean reportedUserExists,
|
||||
Instant createdAt,
|
||||
Instant resolvedAt,
|
||||
String resolvedByEmail,
|
||||
String resolutionNote
|
||||
) {
|
||||
public static UserReportResponse from(UserReport report, boolean reportedUserExists) {
|
||||
return new UserReportResponse(
|
||||
report.getId(),
|
||||
report.getReportedName(),
|
||||
report.getReportedEmail(),
|
||||
report.getConversationId(),
|
||||
report.getListingId(),
|
||||
report.getListingTitle(),
|
||||
report.getReasonId(),
|
||||
report.getReasonTitle(),
|
||||
report.getDetails(),
|
||||
report.getAttachments().stream().map(ListingReportAttachment::getFileName).toList(),
|
||||
report.getAttachments().stream().map(ListingReportAttachmentResponse::from).toList(),
|
||||
report.getConversation().stream()
|
||||
.map(message -> new UserReportMessagePayload(
|
||||
message.isFromReporter(),
|
||||
message.getContent(),
|
||||
message.getSentAt(),
|
||||
message.isHasPhoto()))
|
||||
.toList(),
|
||||
report.getReporterEmail(),
|
||||
report.getReporterName(),
|
||||
report.getStatus(),
|
||||
report.isReportedUserDeleted(),
|
||||
reportedUserExists,
|
||||
report.getCreatedAt(),
|
||||
report.getResolvedAt(),
|
||||
report.getResolvedByEmail(),
|
||||
report.getResolutionNote()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package pl.polskalokalnie.report;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import pl.polskalokalnie.user.AppUser;
|
||||
import pl.polskalokalnie.user.Role;
|
||||
import pl.polskalokalnie.user.UserRepository;
|
||||
|
||||
@Service
|
||||
public class UserReportService {
|
||||
|
||||
// Rozmowa dolaczana do zgloszenia - limity chronia baze przed zbyt duzym wpisem.
|
||||
private static final int MAX_CONVERSATION_MESSAGES = 50;
|
||||
private static final int MAX_MESSAGE_LENGTH = 2000;
|
||||
|
||||
private final UserReportRepository userReportRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserReportService(UserReportRepository userReportRepository, UserRepository userRepository) {
|
||||
this.userReportRepository = userReportRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserReportResponse create(CreateUserReportRequest request, String reporterEmail) {
|
||||
UserReport report = new UserReport();
|
||||
report.setReportedName(request.reportedName().trim());
|
||||
report.setConversationId(ReportAttachments.trimToNull(request.conversationId()));
|
||||
report.setListingId(request.listingId());
|
||||
report.setListingTitle(ReportAttachments.trimToNull(request.listingTitle()));
|
||||
report.setConversation(cleanConversation(request.conversation()));
|
||||
report.setReasonId(request.reasonId().trim());
|
||||
report.setReasonTitle(request.reasonTitle().trim());
|
||||
report.setDetails(ReportAttachments.trimToNull(request.details()));
|
||||
report.setAttachments(ReportAttachments.clean(request.attachmentNames(), request.attachmentFiles()));
|
||||
report.setReporterEmail(reporterEmail);
|
||||
report.setReporterName(userRepository.findByEmailIgnoreCase(reporterEmail).map(AppUser::getFullName).orElse(null));
|
||||
report.setStatus(ListingReportStatus.OPEN);
|
||||
|
||||
// Konto zgloszonego wiazemy tylko wtedy, gdy naprawde istnieje - rozmowy demonstracyjne
|
||||
// nie maja odpowiednika w bazie i zostaja opisane sama nazwa z watku.
|
||||
AppUser reported = findReported(request.reportedEmail());
|
||||
if (reported != null) {
|
||||
report.setReportedEmail(reported.getEmail());
|
||||
report.setReportedName(reported.getFullName() != null ? reported.getFullName() : report.getReportedName());
|
||||
}
|
||||
|
||||
UserReport saved = userReportRepository.save(report);
|
||||
return UserReportResponse.from(saved, reported != null);
|
||||
}
|
||||
|
||||
public List<UserReportResponse> findAllForAdmin() {
|
||||
return userReportRepository.findAllByOrderByCreatedAtDesc().stream()
|
||||
.map(report -> UserReportResponse.from(report, findReported(report.getReportedEmail()) != null))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserReportResponse resolve(Long reportId, String adminEmail, String note) {
|
||||
UserReport report = requireReport(reportId);
|
||||
report.setStatus(ListingReportStatus.RESOLVED);
|
||||
report.setResolvedAt(Instant.now());
|
||||
report.setResolvedByEmail(adminEmail);
|
||||
report.setResolutionNote(ReportAttachments.trimToNull(note));
|
||||
return UserReportResponse.from(userReportRepository.save(report), findReported(report.getReportedEmail()) != null);
|
||||
}
|
||||
|
||||
/** Usuniecie konta zgloszonego uzytkownika wraz z zamknieciem zgloszenia. */
|
||||
@Transactional
|
||||
public UserReportResponse deleteReportedUserAndResolve(Long reportId, String adminEmail) {
|
||||
UserReport report = requireReport(reportId);
|
||||
AppUser reported = findReported(report.getReportedEmail());
|
||||
if (reported == null) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Zgłoszony użytkownik nie ma konta w serwisie - nie ma czego usunąć");
|
||||
}
|
||||
if (reported.getRole() == Role.ADMIN) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nie można usunąć konta administratora");
|
||||
}
|
||||
|
||||
userRepository.delete(reported);
|
||||
|
||||
report.setReportedUserDeleted(true);
|
||||
report.setStatus(ListingReportStatus.RESOLVED);
|
||||
report.setResolvedAt(Instant.now());
|
||||
report.setResolvedByEmail(adminEmail);
|
||||
if (report.getResolutionNote() == null || report.getResolutionNote().isBlank()) {
|
||||
report.setResolutionNote("Konto usunięte przez administratora po zgłoszeniu.");
|
||||
}
|
||||
return UserReportResponse.from(userReportRepository.save(report), false);
|
||||
}
|
||||
|
||||
// Zapisujemy koncowke rozmowy - to ostatnie wiadomosci sa istotne dla zgloszenia.
|
||||
private static List<UserReportMessage> cleanConversation(List<UserReportMessagePayload> payloads) {
|
||||
if (payloads == null || payloads.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<UserReportMessagePayload> tail = payloads.size() > MAX_CONVERSATION_MESSAGES
|
||||
? payloads.subList(payloads.size() - MAX_CONVERSATION_MESSAGES, payloads.size())
|
||||
: payloads;
|
||||
|
||||
List<UserReportMessage> cleaned = new ArrayList<>();
|
||||
for (UserReportMessagePayload payload : tail) {
|
||||
if (payload == null) {
|
||||
continue;
|
||||
}
|
||||
String content = payload.content() == null ? "" : payload.content().trim();
|
||||
if (content.length() > MAX_MESSAGE_LENGTH) {
|
||||
content = content.substring(0, MAX_MESSAGE_LENGTH);
|
||||
}
|
||||
UserReportMessage message = new UserReportMessage();
|
||||
message.setFromReporter(payload.fromReporter());
|
||||
message.setContent(content);
|
||||
message.setSentAt(payload.sentAt());
|
||||
message.setHasPhoto(payload.hasPhoto());
|
||||
cleaned.add(message);
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private AppUser findReported(String email) {
|
||||
if (email == null || email.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return userRepository.findByEmailIgnoreCase(email).orElse(null);
|
||||
}
|
||||
|
||||
private UserReport requireReport(Long id) {
|
||||
return userReportRepository.findById(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Report not found"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package pl.polskalokalnie.support;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record CreateSupportMessageRequest(
|
||||
@NotBlank @Size(max = 160) String fullName,
|
||||
@NotBlank @Email @Size(max = 180) String email,
|
||||
@NotBlank @Size(max = 120) String topic,
|
||||
@NotBlank @Size(max = 4000) String content
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package pl.polskalokalnie.support;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Wiadomosc z formularza "Napisz do nas" (/konto/pomoc). Osobny kanal od czatu z administracja -
|
||||
* obsluga widzi tu zgloszenia z tematem i danymi kontaktowymi podanymi w formularzu.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "support_messages")
|
||||
public class SupportMessage {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
// Dane wpisane w formularzu - moga sie roznic od danych konta.
|
||||
@Column(nullable = false, length = 160)
|
||||
private String fullName;
|
||||
|
||||
@Column(nullable = false, length = 180)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false, length = 120)
|
||||
private String topic;
|
||||
|
||||
@Column(nullable = false, length = 4000)
|
||||
private String content;
|
||||
|
||||
// Konto, z ktorego wyslano formularz.
|
||||
@Column(nullable = false, length = 180)
|
||||
private String accountEmail;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean handled = false;
|
||||
|
||||
private Instant handledAt;
|
||||
|
||||
@Column(length = 180)
|
||||
private String handledByEmail;
|
||||
|
||||
@Column(nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = Instant.now();
|
||||
}
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
public void setFullName(String fullName) {
|
||||
this.fullName = fullName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getTopic() {
|
||||
return topic;
|
||||
}
|
||||
|
||||
public void setTopic(String topic) {
|
||||
this.topic = topic;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getAccountEmail() {
|
||||
return accountEmail;
|
||||
}
|
||||
|
||||
public void setAccountEmail(String accountEmail) {
|
||||
this.accountEmail = accountEmail;
|
||||
}
|
||||
|
||||
public boolean isHandled() {
|
||||
return handled;
|
||||
}
|
||||
|
||||
public void setHandled(boolean handled) {
|
||||
this.handled = handled;
|
||||
}
|
||||
|
||||
public Instant getHandledAt() {
|
||||
return handledAt;
|
||||
}
|
||||
|
||||
public void setHandledAt(Instant handledAt) {
|
||||
this.handledAt = handledAt;
|
||||
}
|
||||
|
||||
public String getHandledByEmail() {
|
||||
return handledByEmail;
|
||||
}
|
||||
|
||||
public void setHandledByEmail(String handledByEmail) {
|
||||
this.handledByEmail = handledByEmail;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package pl.polskalokalnie.support;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** Formularz "Napisz do nas" z /konto/pomoc. */
|
||||
@RestController
|
||||
@RequestMapping("/api/support")
|
||||
public class SupportMessageController {
|
||||
|
||||
private final SupportMessageService service;
|
||||
|
||||
public SupportMessageController(SupportMessageService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@PostMapping("/messages")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public SupportMessageResponse create(@Valid @RequestBody CreateSupportMessageRequest request, Authentication authentication) {
|
||||
return service.create(request, authentication.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package pl.polskalokalnie.support;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface SupportMessageRepository extends JpaRepository<SupportMessage, Long> {
|
||||
|
||||
List<SupportMessage> findAllByOrderByCreatedAtDesc();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package pl.polskalokalnie.support;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record SupportMessageResponse(
|
||||
Long id,
|
||||
String fullName,
|
||||
String email,
|
||||
String topic,
|
||||
String content,
|
||||
String accountEmail,
|
||||
boolean handled,
|
||||
Instant handledAt,
|
||||
String handledByEmail,
|
||||
Instant createdAt
|
||||
) {
|
||||
public static SupportMessageResponse from(SupportMessage message) {
|
||||
return new SupportMessageResponse(
|
||||
message.getId(),
|
||||
message.getFullName(),
|
||||
message.getEmail(),
|
||||
message.getTopic(),
|
||||
message.getContent(),
|
||||
message.getAccountEmail(),
|
||||
message.isHandled(),
|
||||
message.getHandledAt(),
|
||||
message.getHandledByEmail(),
|
||||
message.getCreatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package pl.polskalokalnie.support;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import pl.polskalokalnie.moderation.TextModerationService;
|
||||
|
||||
@Service
|
||||
public class SupportMessageService {
|
||||
|
||||
private final SupportMessageRepository repository;
|
||||
private final TextModerationService textModerationService;
|
||||
|
||||
public SupportMessageService(SupportMessageRepository repository, TextModerationService textModerationService) {
|
||||
this.repository = repository;
|
||||
this.textModerationService = textModerationService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SupportMessageResponse create(CreateSupportMessageRequest request, String accountEmail) {
|
||||
textModerationService.validateOrThrow(request.fullName(), request.content());
|
||||
|
||||
SupportMessage message = new SupportMessage();
|
||||
message.setFullName(request.fullName().trim());
|
||||
message.setEmail(request.email().trim());
|
||||
message.setTopic(request.topic().trim());
|
||||
message.setContent(request.content().trim());
|
||||
message.setAccountEmail(accountEmail);
|
||||
return SupportMessageResponse.from(repository.save(message));
|
||||
}
|
||||
|
||||
public List<SupportMessageResponse> findAllForAdmin() {
|
||||
return repository.findAllByOrderByCreatedAtDesc().stream().map(SupportMessageResponse::from).toList();
|
||||
}
|
||||
|
||||
/** Oznaczenie wiadomosci jako obsluzonej (lub cofniecie tego oznaczenia). */
|
||||
@Transactional
|
||||
public SupportMessageResponse setHandled(Long id, boolean handled, String adminEmail) {
|
||||
SupportMessage message = repository.findById(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Wiadomość nie istnieje"));
|
||||
message.setHandled(handled);
|
||||
message.setHandledAt(handled ? Instant.now() : null);
|
||||
message.setHandledByEmail(handled ? adminEmail : null);
|
||||
return SupportMessageResponse.from(repository.save(message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package pl.polskalokalnie.user;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import pl.polskalokalnie.listing.ListingRepository;
|
||||
import pl.polskalokalnie.listing.ListingStatus;
|
||||
import pl.polskalokalnie.listing.PropertyListing;
|
||||
|
||||
/**
|
||||
* Profil uzytkownika widziany przez innych zalogowanych - otwierany m.in. z rozmowy w wiadomosciach.
|
||||
* Zwraca tylko dane jawne: nazwe, typ konta, date dolaczenia i opublikowane ogloszenia.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class PublicProfileController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final ListingRepository listingRepository;
|
||||
|
||||
public PublicProfileController(UserRepository userRepository, ListingRepository listingRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.listingRepository = listingRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/profile")
|
||||
public PublicProfileResponse profile(@PathVariable Long id) {
|
||||
AppUser user = userRepository.findById(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Nie znaleziono takiego użytkownika"));
|
||||
|
||||
List<PropertyListing> listings = user.getEmail() == null
|
||||
? List.of()
|
||||
: listingRepository.findByOwnerEmailIgnoreCaseAndStatusOrderByCreatedAtDesc(user.getEmail(), ListingStatus.APPROVED);
|
||||
|
||||
return new PublicProfileResponse(
|
||||
user.getId(),
|
||||
user.getFullName(),
|
||||
user.getAccountType(),
|
||||
user.isVerified(),
|
||||
user.getCreatedAt(),
|
||||
listings.size(),
|
||||
listings.stream()
|
||||
.map(listing -> new PublicProfileResponse.PublicProfileListing(
|
||||
listing.getId(),
|
||||
listing.getTitle(),
|
||||
listing.getCity(),
|
||||
listing.getDistrict(),
|
||||
listing.getPrice(),
|
||||
listing.getOfferType().name(),
|
||||
listing.getArea(),
|
||||
listing.getRooms(),
|
||||
listing.getFloor(),
|
||||
listing.getBuildingFloors(),
|
||||
listing.getCoverPhoto(),
|
||||
listing.getCreatedAt()))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package pl.polskalokalnie.user;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Publiczny profil uzytkownika ogladany przez innych (np. z rozmowy w wiadomosciach).
|
||||
* Swiadomie nie zawiera danych kontaktowych - e-mail, telefon i adres zostaja po stronie serwera.
|
||||
*/
|
||||
public record PublicProfileResponse(
|
||||
Long id,
|
||||
String fullName,
|
||||
AccountType accountType,
|
||||
boolean verified,
|
||||
Instant memberSince,
|
||||
int listingsCount,
|
||||
List<PublicProfileListing> listings
|
||||
) {
|
||||
public record PublicProfileListing(
|
||||
Long id,
|
||||
String title,
|
||||
String city,
|
||||
String district,
|
||||
BigDecimal price,
|
||||
String offerType,
|
||||
Double area,
|
||||
Integer rooms,
|
||||
String floor,
|
||||
String buildingFloors,
|
||||
String coverPhoto,
|
||||
Instant createdAt
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user