Spotkania na serwerze: umawianie ogladania z druga strona
Deploy Polska Lokalnie / deploy (push) Successful in 12s
Deploy Polska Lokalnie / deploy (push) Successful in 12s
Nowa tabela meetings i /api/meetings. Termin proponuje jedna ze stron rozmowy o ogloszeniu, druga potwierdza albo odrzuca; odwolac moze kazda ze stron. Kalendarz konta pokazuje oba rodzaje wpisow - umowione ogladania i wlasne notatki - i jest ten sam na kazdym urzadzeniu. Powiadomienia: wpis w watku i dzwonek przy kazdej zmianie, e-mail wylacznie przy potwierdzeniu terminu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,28 @@ public class AccountEmailService {
|
||||
template("Nowa wiadomość", greeting(name), body, "Odpowiedz", link), "WIADOMOŚĆ", link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Potwierdzony termin ogladania. Jedyny e-mail modulu spotkan - propozycje, odmowy
|
||||
* i odwolania zostaja w serwisie (watek rozmowy i dzwonek).
|
||||
*/
|
||||
public void sendMeetingConfirmed(String email, String name, String otherName, String listingTitle,
|
||||
String when, String location, Long conversationId) {
|
||||
String link = baseUrl + (conversationId == null ? "/konto/spotkania" : "/wiadomosci/" + conversationId);
|
||||
String where = location == null || location.isBlank() ? "" :
|
||||
"<p style=\"margin:0 0 4px\"><strong>Miejsce:</strong> " + HtmlUtils.htmlEscape(location) + "</p>";
|
||||
String about = listingTitle == null || listingTitle.isBlank() ? "" :
|
||||
"<p style=\"margin:0 0 4px\"><strong>Ogłoszenie:</strong> " + HtmlUtils.htmlEscape(listingTitle) + "</p>";
|
||||
String body = "<p>Termin oglądania został potwierdzony przez: <strong>"
|
||||
+ HtmlUtils.htmlEscape(otherName) + "</strong>.</p>"
|
||||
+ "<div style=\"margin:0 0 12px;padding:12px 14px;background:#f1f5f9;border-left:4px solid #12a764;border-radius:6px;\">"
|
||||
+ "<p style=\"margin:0 0 4px\"><strong>Termin:</strong> " + HtmlUtils.htmlEscape(when) + "</p>"
|
||||
+ where + about + "</div>"
|
||||
+ "<p style=\"color:#6b7f96;font-size:13px\">Jeśli termin przestanie pasować, odwołaj go w serwisie -"
|
||||
+ " druga strona od razu to zobaczy.</p>";
|
||||
send(email, "Potwierdzone oglądanie: " + when,
|
||||
template("Termin potwierdzony", greeting(name), body, "Zobacz szczegóły", link), "SPOTKANIE", link);
|
||||
}
|
||||
|
||||
private void send(String email, String subject, String html, String label, String link) {
|
||||
SmtpConfig smtp = mailConfigService.getSmtp();
|
||||
boolean configured = smtp.isEnabled()
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package pl.polskalokalnie.meeting;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
|
||||
/**
|
||||
* Spotkanie (ogladanie nieruchomosci) albo wlasny wpis w kalendarzu konta. Wczesniej kalendarz
|
||||
* zyl w localStorage jednej przegladarki - druga strona nic o nim nie wiedziala.
|
||||
*
|
||||
* <p>Wpis prywatny ma puste conversationId i counterpartId oraz status CONFIRMED - nikt poza
|
||||
* autorem go nie widzi. Spotkanie umowione z druga strona powstaje z rozmowy o ogloszeniu
|
||||
* i jest widoczne dla obu uczestnikow.</p>
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "meetings")
|
||||
public class Meeting {
|
||||
|
||||
public enum Status {
|
||||
/** Termin zaproponowany, czeka na odpowiedz drugiej strony. */
|
||||
PROPOSED,
|
||||
/** Termin potwierdzony przez druga strone. */
|
||||
CONFIRMED,
|
||||
/** Druga strona odrzucila termin. */
|
||||
DECLINED,
|
||||
/** Odwolane - moze to zrobic kazda ze stron, do konca spotkania. */
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "conversation_id")
|
||||
private Long conversationId;
|
||||
|
||||
@Column(name = "listing_id")
|
||||
private Long listingId;
|
||||
|
||||
@Column(name = "listing_title", length = 200)
|
||||
private String listingTitle;
|
||||
|
||||
/** Kto zaproponowal termin (albo autor wpisu prywatnego). */
|
||||
@Column(name = "proposer_id", nullable = false)
|
||||
private Long proposerId;
|
||||
|
||||
/** Druga strona spotkania; null dla wpisu prywatnego. */
|
||||
@Column(name = "counterpart_id")
|
||||
private Long counterpartId;
|
||||
|
||||
@Column(nullable = false, length = 160)
|
||||
private String title;
|
||||
|
||||
@Column(length = 200)
|
||||
private String note;
|
||||
|
||||
@Column(length = 200)
|
||||
private String location;
|
||||
|
||||
@Column(name = "meeting_date", nullable = false)
|
||||
private LocalDate meetingDate;
|
||||
|
||||
@Column(name = "start_time", nullable = false)
|
||||
private LocalTime startTime;
|
||||
|
||||
@Column(name = "end_time", nullable = false)
|
||||
private LocalTime endTime;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 20)
|
||||
private Status status = Status.PROPOSED;
|
||||
|
||||
/** Kto odwolal spotkanie - druga strona widzi, po czyjej stronie byla zmiana. */
|
||||
@Column(name = "cancelled_by_id")
|
||||
private Long cancelledById;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Instant createdAt = Instant.now();
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Instant updatedAt = Instant.now();
|
||||
|
||||
public boolean isPrivateNote() {
|
||||
return counterpartId == null;
|
||||
}
|
||||
|
||||
public boolean isParticipant(Long userId) {
|
||||
return proposerId.equals(userId) || (counterpartId != null && counterpartId.equals(userId));
|
||||
}
|
||||
|
||||
public Long otherParticipant(Long userId) {
|
||||
return proposerId.equals(userId) ? counterpartId : proposerId;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Long getConversationId() {
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
public void setConversationId(Long 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 Long getProposerId() {
|
||||
return proposerId;
|
||||
}
|
||||
|
||||
public void setProposerId(Long proposerId) {
|
||||
this.proposerId = proposerId;
|
||||
}
|
||||
|
||||
public Long getCounterpartId() {
|
||||
return counterpartId;
|
||||
}
|
||||
|
||||
public void setCounterpartId(Long counterpartId) {
|
||||
this.counterpartId = counterpartId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public void setNote(String note) {
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public LocalDate getMeetingDate() {
|
||||
return meetingDate;
|
||||
}
|
||||
|
||||
public void setMeetingDate(LocalDate meetingDate) {
|
||||
this.meetingDate = meetingDate;
|
||||
}
|
||||
|
||||
public LocalTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalTime getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(LocalTime endTime) {
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Status status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getCancelledById() {
|
||||
return cancelledById;
|
||||
}
|
||||
|
||||
public void setCancelledById(Long cancelledById) {
|
||||
this.cancelledById = cancelledById;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Instant getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(Instant updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package pl.polskalokalnie.meeting;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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.PutMapping;
|
||||
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;
|
||||
import pl.polskalokalnie.meeting.MeetingDtos.MeetingRequest;
|
||||
import pl.polskalokalnie.meeting.MeetingDtos.MeetingResponse;
|
||||
import pl.polskalokalnie.user.AppUser;
|
||||
import pl.polskalokalnie.user.UserRepository;
|
||||
|
||||
/** Kalendarz spotkan zalogowanego uzytkownika - wlasne wpisy i ogladania umowione z druga strona. */
|
||||
@RestController
|
||||
@RequestMapping("/api/meetings")
|
||||
public class MeetingController {
|
||||
|
||||
private final MeetingService meetingService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public MeetingController(MeetingService meetingService, UserRepository userRepository) {
|
||||
this.meetingService = meetingService;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<MeetingResponse> list(Authentication authentication) {
|
||||
return meetingService.list(me(authentication));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public MeetingResponse create(@RequestBody MeetingRequest request, Authentication authentication) {
|
||||
return meetingService.create(me(authentication), request);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public MeetingResponse update(@PathVariable Long id, @RequestBody MeetingRequest request, Authentication authentication) {
|
||||
return meetingService.updatePrivate(me(authentication), id, request);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/confirm")
|
||||
public MeetingResponse confirm(@PathVariable Long id, Authentication authentication) {
|
||||
return meetingService.respond(me(authentication), id, true);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/decline")
|
||||
public MeetingResponse decline(@PathVariable Long id, Authentication authentication) {
|
||||
return meetingService.respond(me(authentication), id, false);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> cancel(@PathVariable Long id, Authentication authentication) {
|
||||
meetingService.cancel(me(authentication), id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
private AppUser me(Authentication authentication) {
|
||||
return userRepository.findByEmailIgnoreCase(authentication.getName())
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Zaloguj się ponownie"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package pl.polskalokalnie.meeting;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
|
||||
public final class MeetingDtos {
|
||||
|
||||
private MeetingDtos() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Propozycja terminu albo wlasny wpis w kalendarzu. Z conversationId powstaje spotkanie
|
||||
* z druga strona rozmowy; bez niego - wpis prywatny, widoczny tylko dla autora.
|
||||
*/
|
||||
public record MeetingRequest(Long conversationId, String title, String note, String location,
|
||||
LocalDate date, LocalTime start, LocalTime end) {
|
||||
}
|
||||
|
||||
/**
|
||||
* status: PROPOSED / CONFIRMED / DECLINED / CANCELLED.
|
||||
* proposedByMe - czy to ja zaproponowalem termin (tylko druga strona moze go potwierdzic).
|
||||
* privateNote - wpis wlasny, bez drugiej strony.
|
||||
*/
|
||||
public record MeetingResponse(Long id, Long conversationId, Long listingId, String listingTitle,
|
||||
String title, String note, String location, LocalDate date, LocalTime start,
|
||||
LocalTime end, String status, boolean proposedByMe, boolean privateNote,
|
||||
String otherName, boolean cancelledByMe) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package pl.polskalokalnie.meeting;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface MeetingRepository extends JpaRepository<Meeting, Long> {
|
||||
|
||||
@Query("select m from Meeting m where m.proposerId = :userId or m.counterpartId = :userId"
|
||||
+ " order by m.meetingDate asc, m.startTime asc")
|
||||
List<Meeting> findForUser(@Param("userId") Long userId);
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package pl.polskalokalnie.meeting;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
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.auth.AccountEmailService;
|
||||
import pl.polskalokalnie.meeting.MeetingDtos.MeetingRequest;
|
||||
import pl.polskalokalnie.meeting.MeetingDtos.MeetingResponse;
|
||||
import pl.polskalokalnie.message.Conversation;
|
||||
import pl.polskalokalnie.message.ConversationRepository;
|
||||
import pl.polskalokalnie.message.ConversationService;
|
||||
import pl.polskalokalnie.notification.NotificationService;
|
||||
import pl.polskalokalnie.user.AppUser;
|
||||
import pl.polskalokalnie.user.UserRepository;
|
||||
import pl.polskalokalnie.user.UserSettings;
|
||||
import pl.polskalokalnie.user.UserSettingsRepository;
|
||||
|
||||
/**
|
||||
* Umawianie ogladania nieruchomosci. Termin proponuje jedna ze stron rozmowy o ogloszeniu,
|
||||
* druga potwierdza albo odrzuca; odwolac moze kazda ze stron, do konca spotkania.
|
||||
*
|
||||
* <p>Powiadomienia: wpis w watku rozmowy i pod dzwonkiem przy kazdej zmianie, a e-mail
|
||||
* wylacznie przy potwierdzeniu terminu - swiadomie nie zasypujemy skrzynek.</p>
|
||||
*/
|
||||
@Service
|
||||
public class MeetingService {
|
||||
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
||||
private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("HH:mm");
|
||||
|
||||
private final MeetingRepository meetingRepository;
|
||||
private final ConversationRepository conversationRepository;
|
||||
private final ConversationService conversationService;
|
||||
private final UserRepository userRepository;
|
||||
private final UserSettingsRepository userSettingsRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final AccountEmailService accountEmailService;
|
||||
|
||||
public MeetingService(MeetingRepository meetingRepository,
|
||||
ConversationRepository conversationRepository,
|
||||
ConversationService conversationService,
|
||||
UserRepository userRepository,
|
||||
UserSettingsRepository userSettingsRepository,
|
||||
NotificationService notificationService,
|
||||
AccountEmailService accountEmailService) {
|
||||
this.meetingRepository = meetingRepository;
|
||||
this.conversationRepository = conversationRepository;
|
||||
this.conversationService = conversationService;
|
||||
this.userRepository = userRepository;
|
||||
this.userSettingsRepository = userSettingsRepository;
|
||||
this.notificationService = notificationService;
|
||||
this.accountEmailService = accountEmailService;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<MeetingResponse> list(AppUser me) {
|
||||
List<Meeting> meetings = meetingRepository.findForUser(me.getId());
|
||||
Map<Long, AppUser> others = userRepository.findAllById(meetings.stream()
|
||||
.map(meeting -> meeting.otherParticipant(me.getId()))
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.collect(Collectors.toSet()))
|
||||
.stream().collect(Collectors.toMap(AppUser::getId, Function.identity()));
|
||||
return meetings.stream().map(meeting -> toResponse(meeting, me, others.get(meeting.otherParticipant(me.getId())))).toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public MeetingResponse create(AppUser me, MeetingRequest request) {
|
||||
validate(request);
|
||||
Meeting meeting = new Meeting();
|
||||
meeting.setProposerId(me.getId());
|
||||
meeting.setTitle(request.title().trim());
|
||||
meeting.setNote(trimOrNull(request.note()));
|
||||
meeting.setLocation(trimOrNull(request.location()));
|
||||
meeting.setMeetingDate(request.date());
|
||||
meeting.setStartTime(request.start());
|
||||
meeting.setEndTime(request.end());
|
||||
|
||||
if (request.conversationId() == null) {
|
||||
// Wpis wlasny - nie ma z kim uzgadniac terminu, wiec od razu jest potwierdzony.
|
||||
meeting.setStatus(Meeting.Status.CONFIRMED);
|
||||
return toResponse(meetingRepository.save(meeting), me, null);
|
||||
}
|
||||
|
||||
Conversation conversation = requireConversation(me, request.conversationId());
|
||||
AppUser other = requireUser(conversation.otherParticipant(me.getId()));
|
||||
meeting.setConversationId(conversation.getId());
|
||||
meeting.setListingId(conversation.getListingId());
|
||||
meeting.setListingTitle(conversation.getListingTitle());
|
||||
meeting.setCounterpartId(other.getId());
|
||||
meeting.setStatus(Meeting.Status.PROPOSED);
|
||||
Meeting saved = meetingRepository.save(meeting);
|
||||
|
||||
String when = when(saved);
|
||||
conversationService.appendMeetingNote(conversation, me,
|
||||
"Propozycja oglądania: " + when + (saved.getLocation() == null ? "" : ", " + saved.getLocation()));
|
||||
notify(other, "Propozycja oglądania", displayName(me) + " proponuje termin: " + when, conversation.getId());
|
||||
return toResponse(saved, me, other);
|
||||
}
|
||||
|
||||
/** Zmiana wlasnego wpisu w kalendarzu. Terminu umowionego z druga strona sie nie edytuje - odwoluje sie go i proponuje nowy. */
|
||||
@Transactional
|
||||
public MeetingResponse updatePrivate(AppUser me, Long meetingId, MeetingRequest request) {
|
||||
validate(request);
|
||||
Meeting meeting = requireParticipant(me, meetingId);
|
||||
if (!meeting.isPrivateNote()) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Umówionego terminu nie da się zmienić - odwołaj go i zaproponuj nowy");
|
||||
}
|
||||
meeting.setTitle(request.title().trim());
|
||||
meeting.setNote(trimOrNull(request.note()));
|
||||
meeting.setLocation(trimOrNull(request.location()));
|
||||
meeting.setMeetingDate(request.date());
|
||||
meeting.setStartTime(request.start());
|
||||
meeting.setEndTime(request.end());
|
||||
meeting.setUpdatedAt(Instant.now());
|
||||
return toResponse(meeting, me, null);
|
||||
}
|
||||
|
||||
/** Potwierdzenie albo odrzucenie terminu - tylko przez osobe, ktora go dostala. */
|
||||
@Transactional
|
||||
public MeetingResponse respond(AppUser me, Long meetingId, boolean accept) {
|
||||
Meeting meeting = requireParticipant(me, meetingId);
|
||||
if (meeting.isPrivateNote() || meeting.getProposerId().equals(me.getId())) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Termin potwierdza osoba, która go otrzymała");
|
||||
}
|
||||
if (meeting.getStatus() != Meeting.Status.PROPOSED) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Ten termin został już rozpatrzony");
|
||||
}
|
||||
meeting.setStatus(accept ? Meeting.Status.CONFIRMED : Meeting.Status.DECLINED);
|
||||
meeting.setUpdatedAt(Instant.now());
|
||||
|
||||
AppUser other = requireUser(meeting.getProposerId());
|
||||
String when = when(meeting);
|
||||
Conversation conversation = conversationRepository.findById(meeting.getConversationId()).orElse(null);
|
||||
if (conversation != null) {
|
||||
conversationService.appendMeetingNote(conversation, me,
|
||||
(accept ? "Potwierdzam oglądanie: " : "Nie pasuje mi termin: ") + when);
|
||||
}
|
||||
notify(other, accept ? "Termin potwierdzony" : "Termin odrzucony",
|
||||
displayName(me) + (accept ? " potwierdza oglądanie: " : " nie może w terminie: ") + when,
|
||||
meeting.getConversationId());
|
||||
if (accept) {
|
||||
sendConfirmationEmail(other, me, meeting);
|
||||
sendConfirmationEmail(me, other, meeting);
|
||||
}
|
||||
return toResponse(meeting, me, other);
|
||||
}
|
||||
|
||||
/** Odwolac moze kazda ze stron, do konca spotkania. Wpis wlasny po prostu znika. */
|
||||
@Transactional
|
||||
public void cancel(AppUser me, Long meetingId) {
|
||||
Meeting meeting = requireParticipant(me, meetingId);
|
||||
if (meeting.isPrivateNote()) {
|
||||
meetingRepository.delete(meeting);
|
||||
return;
|
||||
}
|
||||
if (meeting.getStatus() == Meeting.Status.CANCELLED) {
|
||||
return;
|
||||
}
|
||||
meeting.setStatus(Meeting.Status.CANCELLED);
|
||||
meeting.setCancelledById(me.getId());
|
||||
meeting.setUpdatedAt(Instant.now());
|
||||
|
||||
AppUser other = requireUser(meeting.otherParticipant(me.getId()));
|
||||
String when = when(meeting);
|
||||
Conversation conversation = conversationRepository.findById(meeting.getConversationId()).orElse(null);
|
||||
if (conversation != null) {
|
||||
conversationService.appendMeetingNote(conversation, me, "Odwołanie oglądania: " + when);
|
||||
}
|
||||
notify(other, "Oglądanie odwołane", displayName(me) + " odwołuje termin: " + when, meeting.getConversationId());
|
||||
}
|
||||
|
||||
private void sendConfirmationEmail(AppUser recipient, AppUser other, Meeting meeting) {
|
||||
boolean wantsEmail = userSettingsRepository.findByUserId(recipient.getId())
|
||||
.map(UserSettings::isNotifyMessages)
|
||||
.orElse(true);
|
||||
if (!wantsEmail) {
|
||||
return;
|
||||
}
|
||||
accountEmailService.sendMeetingConfirmed(recipient.getEmail(), recipient.getFullName(), displayName(other),
|
||||
meeting.getListingTitle(), when(meeting), meeting.getLocation(), meeting.getConversationId());
|
||||
}
|
||||
|
||||
private void notify(AppUser recipient, String title, String body, Long conversationId) {
|
||||
notificationService.create(recipient.getEmail(), "messages", "calendar", title, body,
|
||||
conversationId == null ? "meetings" : "messages:conversation:" + conversationId, null);
|
||||
}
|
||||
|
||||
private String when(Meeting meeting) {
|
||||
return meeting.getMeetingDate().format(DATE) + ", " + meeting.getStartTime().format(TIME)
|
||||
+ "-" + meeting.getEndTime().format(TIME);
|
||||
}
|
||||
|
||||
private void validate(MeetingRequest request) {
|
||||
if (request == null || request.title() == null || request.title().isBlank()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Podaj nazwę spotkania");
|
||||
}
|
||||
if (request.date() == null || request.start() == null || request.end() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Podaj datę i godziny spotkania");
|
||||
}
|
||||
if (!request.end().isAfter(request.start())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Godzina zakończenia musi być późniejsza niż rozpoczęcia");
|
||||
}
|
||||
if (request.date().isBefore(LocalDate.now())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Nie można umówić spotkania w przeszłości");
|
||||
}
|
||||
}
|
||||
|
||||
private Conversation requireConversation(AppUser me, Long conversationId) {
|
||||
Conversation conversation = conversationRepository.findById(conversationId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Nie znaleziono rozmowy"));
|
||||
if (!conversation.isParticipant(me.getId())) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Brak dostępu do tej rozmowy");
|
||||
}
|
||||
return conversation;
|
||||
}
|
||||
|
||||
private Meeting requireParticipant(AppUser me, Long meetingId) {
|
||||
Meeting meeting = meetingRepository.findById(meetingId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Nie znaleziono spotkania"));
|
||||
if (!meeting.isParticipant(me.getId())) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Brak dostępu do tego spotkania");
|
||||
}
|
||||
return meeting;
|
||||
}
|
||||
|
||||
private AppUser requireUser(Long userId) {
|
||||
return userRepository.findById(userId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Konto drugiej strony zostało usunięte"));
|
||||
}
|
||||
|
||||
private MeetingResponse toResponse(Meeting meeting, AppUser me, AppUser other) {
|
||||
return new MeetingResponse(
|
||||
meeting.getId(),
|
||||
meeting.getConversationId(),
|
||||
meeting.getListingId(),
|
||||
meeting.getListingTitle(),
|
||||
meeting.getTitle(),
|
||||
meeting.getNote(),
|
||||
meeting.getLocation(),
|
||||
meeting.getMeetingDate(),
|
||||
meeting.getStartTime(),
|
||||
meeting.getEndTime(),
|
||||
meeting.getStatus().name(),
|
||||
meeting.getProposerId().equals(me.getId()),
|
||||
meeting.isPrivateNote(),
|
||||
meeting.isPrivateNote() ? null : displayName(other),
|
||||
meeting.getCancelledById() != null && meeting.getCancelledById().equals(me.getId()));
|
||||
}
|
||||
|
||||
private static String displayName(AppUser user) {
|
||||
if (user == null) {
|
||||
return "Konto usunięte";
|
||||
}
|
||||
String name = user.getFullName();
|
||||
return name == null || name.isBlank() ? user.getEmail() : name;
|
||||
}
|
||||
|
||||
private static String trimOrNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,24 @@ public class ConversationService {
|
||||
return messageRepository.countByRecipientIdAndReadFalseAndConversationIdIsNotNull(me.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wpis o spotkaniu w watku rozmowy. Swiadomie NIE publikuje zdarzenia o nowej wiadomosci:
|
||||
* o terminach powiadamia modul spotkan (dzwonek zawsze, e-mail tylko przy potwierdzeniu),
|
||||
* wiec kazda propozycja nie ma prawa wygenerowac osobnego maila.
|
||||
*/
|
||||
@Transactional
|
||||
public void appendMeetingNote(Conversation conversation, AppUser sender, String content) {
|
||||
Message message = new Message();
|
||||
message.setConversationId(conversation.getId());
|
||||
message.setSenderId(sender.getId());
|
||||
message.setRecipientId(conversation.otherParticipant(sender.getId()));
|
||||
message.setContent(content.trim());
|
||||
messageRepository.save(message);
|
||||
|
||||
conversation.setLastMessageAt(Instant.now());
|
||||
conversation.restoreForBoth();
|
||||
}
|
||||
|
||||
private void append(Conversation conversation, AppUser sender, AppUser recipient, String content) {
|
||||
textModerationService.validateOrThrow(content);
|
||||
String trimmed = content.trim();
|
||||
|
||||
Reference in New Issue
Block a user