Prelegent widzi zadania i banery na sali

`PublicTaskOfferController` szukal tokenu wylacznie w `registrations`,
a prelegent tam nie istnieje: nie przechodzi przez formularz zapisu i ma
wlasny token w `webinar_speakers`. Kazdy endpoint sali odpowiadal mu 404.

Objawialo sie to tym, ze prowadzacy **nie widzial wlasnych zadan**: panel
pokazywal "Nie znaleziono zapisu" przy zadaniu ogloszonym minute wczesniej
i widocznym w panelu prowadzenia. Tak samo znikaly banery i nie liczyly sie
ich wyswietlenia. Zdarzylo sie to na zywo, w trakcie spotkania.

Nowa metoda `TaskService.viewer` obsluguje oba rodzaje tokenu, wzorowana na
`PublicInteractionController`, ktory robil to juz przy historii czatu.
Przelaczone sa WYLACZNIE odczyty: lista zadan, banery i ich zdarzenia.
Zapisy (zgloszenie do zadania, wyslanie oferty) zostaja przy `participant`,
bo prelegent nie zapisuje sie na wlasne zadanie.

`registrationId` rowne null jest tu poprawna wartoscia, nie brakiem danych:
`listViews` przewidywalo to od poczatku, a `banner_events.registration_id`
jest nullowalne wlasnie dla zdarzen od osob bez zapisu.

Front: `TasksTab` dostaje tryb `readOnly`, wiec prowadzacy widzi zadania
i liczbe zapisanych, ale bez przycisku zgloszenia.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 09:44:28 +02:00
parent 5138268b6d
commit 2bc2e6525c
4 changed files with 79 additions and 17 deletions
@@ -67,7 +67,9 @@ public class PublicTaskOfferController {
@GetMapping("/tasks") @GetMapping("/tasks")
@Operation(summary = "Zadania webinaru i wlasne zgloszenia") @Operation(summary = "Zadania webinaru i wlasne zgloszenia")
public TasksView taskList(@PathVariable String token) { public TasksView taskList(@PathVariable String token) {
TaskService.ParticipantDetails participant = participant(token); // odczyt, wiec takze dla prelegenta: prowadzacy ma widziec zadania,
// ktore sam oglosil
TaskService.ParticipantDetails participant = tasks.viewer(token);
return new TasksView(tasks.listViews(participant.webinarId(), participant.registrationId())); return new TasksView(tasks.listViews(participant.webinarId(), participant.registrationId()));
} }
@@ -153,7 +155,7 @@ public class PublicTaskOfferController {
@GetMapping("/banners") @GetMapping("/banners")
@Operation(summary = "Banery obowiazujace w tej chwili dla tego webinaru") @Operation(summary = "Banery obowiazujace w tej chwili dla tego webinaru")
public BannersView activeBanners(@PathVariable String token) { public BannersView activeBanners(@PathVariable String token) {
TaskService.ParticipantDetails participant = participant(token); TaskService.ParticipantDetails participant = tasks.viewer(token);
return new BannersView( return new BannersView(
banners.activeFor(participant.webinarId()), banners.activeFor(participant.webinarId()),
banners.rotationSeconds()); banners.rotationSeconds());
@@ -169,7 +171,9 @@ public class PublicTaskOfferController {
@PostMapping("/banners/{bannerId}/impression") @PostMapping("/banners/{bannerId}/impression")
@Operation(summary = "Zapis wyswietlenia banera, raz na uczestnika") @Operation(summary = "Zapis wyswietlenia banera, raz na uczestnika")
public void bannerImpression(@PathVariable String token, @PathVariable Long bannerId) { public void bannerImpression(@PathVariable String token, @PathVariable Long bannerId) {
TaskService.ParticipantDetails participant = participant(token); // takze prelegent: `banner_events.registration_id` jest nullowalne
// wlasnie po to, bo zdarzenie moze pochodzic od osoby bez zapisu
TaskService.ParticipantDetails participant = tasks.viewer(token);
banners.recordImpression(bannerId, participant.registrationId()); banners.recordImpression(bannerId, participant.registrationId());
} }
@@ -177,7 +181,7 @@ public class PublicTaskOfferController {
@PostMapping("/banners/{bannerId}/click") @PostMapping("/banners/{bannerId}/click")
@Operation(summary = "Zapis klikniecia w baner") @Operation(summary = "Zapis klikniecia w baner")
public void bannerClick(@PathVariable String token, @PathVariable Long bannerId) { public void bannerClick(@PathVariable String token, @PathVariable Long bannerId) {
TaskService.ParticipantDetails participant = participant(token); TaskService.ParticipantDetails participant = tasks.viewer(token);
banners.recordClick(bannerId, participant.registrationId()); banners.recordClick(bannerId, participant.registrationId());
} }
@@ -40,19 +40,22 @@ public class TaskService {
private final WebinarRepository webinarRepository; private final WebinarRepository webinarRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final PlainTextSanitizer sanitizer; private final PlainTextSanitizer sanitizer;
private final com.softspm.webinar.repository.WebinarSpeakerRepository speakerRepository;
public TaskService(TaskRepository taskRepository, public TaskService(TaskRepository taskRepository,
InteractionRepository interactionRepository, InteractionRepository interactionRepository,
RegistrationRepository registrationRepository, RegistrationRepository registrationRepository,
WebinarRepository webinarRepository, WebinarRepository webinarRepository,
UserRepository userRepository, UserRepository userRepository,
PlainTextSanitizer sanitizer) { PlainTextSanitizer sanitizer,
com.softspm.webinar.repository.WebinarSpeakerRepository speakerRepository) {
this.taskRepository = taskRepository; this.taskRepository = taskRepository;
this.interactionRepository = interactionRepository; this.interactionRepository = interactionRepository;
this.registrationRepository = registrationRepository; this.registrationRepository = registrationRepository;
this.webinarRepository = webinarRepository; this.webinarRepository = webinarRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.sanitizer = sanitizer; this.sanitizer = sanitizer;
this.speakerRepository = speakerRepository;
} }
@Transactional @Transactional
@@ -265,6 +268,43 @@ public class TaskService {
* Odczyt jest w serwisie, a nie w kontrolerze: @Transactional na metodzie * Odczyt jest w serwisie, a nie w kontrolerze: @Transactional na metodzie
* wolanej z tej samej klasy nie dziala, bo samowywolanie omija proxy Springa. * wolanej z tej samej klasy nie dziala, bo samowywolanie omija proxy Springa.
*/ */
/**
* Osoba na sali: uczestnik ALBO prelegent.
*
* DWA RODZAJE TOKENU, JEDNO WEJSCIE
* -----------------------------------------------------------------------
* Prelegent nie jest wierszem w `registrations`: nie przechodzi przez
* formularz zapisu i ma wlasny token w `webinar_speakers`. Metoda
* `participant` szuka wylacznie zapisu, wiec kazdy endpoint sali oparty
* na niej odpowiadal prelegentowi bledem 404. Objawialo sie to tym, ze
* prowadzacy **nie widzial wlasnych zadan** ani banerow, a panel pokazywal
* "Nie znaleziono zapisu" przy poprawnie ogloszonym zadaniu.
*
* `registrationId` rowne null oznacza prelegenta i jest **poprawna
* wartoscia**, nie brakiem danych: `listViews` traktuje go jako osobe bez
* wlasnych zgloszen, bo prelegent nie zapisuje sie na wlasne zadanie.
*/
@Transactional(readOnly = true)
public ParticipantDetails viewer(String token) {
return registrationRepository.findByConfirmationToken(token)
.map(registration -> new ParticipantDetails(
registration.getId(),
registration.getWebinar().getId(),
(registration.getFirstName() + " " + registration.getLastName()).trim(),
registration.getEmail(),
registration.getPhone(),
registration.getCompany()))
.orElseGet(() -> speakerRepository.findByAccessToken(token)
.map(speaker -> new ParticipantDetails(
null,
speaker.getWebinar().getId(),
speaker.getFullName(),
speaker.getEmail(),
null,
null))
.orElseThrow(() -> new NotFoundException("Nie znaleziono zapisu")));
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ParticipantDetails participant(String confirmationToken) { public ParticipantDetails participant(String confirmationToken) {
Registration registration = registrationRepository.findByConfirmationToken(confirmationToken) Registration registration = registrationRepository.findByConfirmationToken(confirmationToken)
@@ -161,7 +161,9 @@ export function InteractionPanel({
</div> </div>
)} )}
{tab === 'tasks' && <TasksTab token={token} webinarId={history.webinarId} />} {tab === 'tasks' && (
<TasksTab token={token} webinarId={history.webinarId} readOnly={history.speaker} />
)}
{tab === 'offer' && !history.speaker && <OfferTab token={token} />} {tab === 'offer' && !history.speaker && <OfferTab token={token} />}
{tab === 'chat' ? ( {tab === 'chat' ? (
+27 -11
View File
@@ -25,7 +25,21 @@ export type TaskView = {
createdAt: string createdAt: string
} }
export function TasksTab({ token, webinarId }: { token: string; webinarId: number }) { export function TasksTab({
token,
webinarId,
readOnly = false,
}: {
token: string
webinarId: number
/**
* Podglad bez mozliwosci zapisu.
*
* Prowadzacy widzi zadania, ktore sam oglosil, ale nie zapisuje sie na nie:
* nie ma wiersza w `registrations`, wiec backend i tak odrzucilby zgloszenie.
*/
readOnly?: boolean
}) {
const [tasks, setTasks] = useState<TaskView[]>([]) const [tasks, setTasks] = useState<TaskView[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@@ -129,16 +143,18 @@ export function TasksTab({ token, webinarId }: { token: string; webinarId: numbe
)} )}
{!task.open && <span className="text-xs text-warn-600">zapisy zamknięte</span>} {!task.open && <span className="text-xs text-warn-600">zapisy zamknięte</span>}
<Button {!readOnly && (
type="button" <Button
size="sm" type="button"
variant={task.signedUp ? 'secondary' : 'primary'} size="sm"
className="ml-auto" variant={task.signedUp ? 'secondary' : 'primary'}
disabled={busy === task.id || (blocked && !task.signedUp)} className="ml-auto"
onClick={() => toggle(task)} disabled={busy === task.id || (blocked && !task.signedUp)}
> onClick={() => toggle(task)}
{task.signedUp ? 'Wycofaj się' : task.full ? 'Brak miejsc' : 'Zgłaszam się'} >
</Button> {task.signedUp ? 'Wycofaj się' : task.full ? 'Brak miejsc' : 'Zgłaszam się'}
</Button>
)}
</div> </div>
{task.signedUp && ( {task.signedUp && (