KONTAKTY
Usuwanie istnialo w API, ale nie mialo przycisku na liscie. Doszedl, wraz
z endpointem `GET /admin/contacts/{id}/deletion-impact`: panel pyta
o potwierdzenie DOPIERO Z LICZBAMI, tak samo jak przy usuwaniu webinaru.
Jest to konieczne, bo usuniecie kontaktu kasuje KASKADOWO zapisy na kursy
i zgody. Przy zwyklym wpisie marketingowym odpowiedz jest zupelnie inna niz
przy kursancie z oplaconym kursem, ktoremu usuniecie odbiera dostep do
materialow. Pytanie mowi to wprost i dodaje, ze zamowienia zostaja: sa
dokumentem finansowym i maja `ON DELETE SET NULL`.
Gdy pobranie skutkow sie nie uda, potwierdzenie NIE JEST pokazywane wcale.
Pytanie o zgode na cos, czego skutkow nie udalo sie ustalic, byloby gorsze
niz brak pytania.
UZYTKOWNICY
Byla sama dezaktywacja. Doszly:
- odblokowanie (`POST /{id}/activate`), bo dezaktywacja jest odwracalna
z zalozenia: konto zablokowane na czas urlopu ma dac sie wlaczyc bez
zakladania nowego i bez tracenia sladu w dzienniku,
- usuniecie trwale (`DELETE /{id}`) dla kont zalozonych przez pomylke.
APLIKACJA NIE MOZE ZOSTAC BEZ ADMINISTRATORA
Usuwanie ma te same dwie bariery co dezaktywacja i uzywa TEJ SAMEJ metody
`assertNotLastActiveAdmin`, a nie drugiej kopii reguly: nie mozna usunac
wlasnego konta ani ostatniego aktywnego administratora. Jedno i drugie
konczyloby sie utrata dostepu do panelu bez mozliwosci odzyskania go
z poziomu aplikacji.
PRACA UZYTKOWNIKA NIE PRZEPADA
Wszystkie trzy odwolania do `users` maja `ON DELETE SET NULL`: pliki
w bibliotece, ogloszone zadania i odpowiedzi na pytania zostaja, tracac
jedynie informacje o autorze. Kasowanie kaskadowe zabieraloby tresc szkolen
razem z kontem, ktore ja wprowadzilo.
DEZAKTYWACJA NIE JEST OPERACJA NISZCZACA
Jej potwierdzenie celowo NIE ma czerwonego ostrzezenia: tresc mowi wprost, ze
konto da sie wlaczyc z powrotem, a od teraz jednym klknieciem obok. Czerwien
przeczylaby wlasnemu komunikatowi i rozmydlala sygnal tam, gdzie strata jest
prawdziwa, czyli przy usuwaniu konta i kontaktu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
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;
|
||||
@@ -60,6 +61,26 @@ public class UserAdminController {
|
||||
return userService.deactivate(id, principal);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/activate")
|
||||
@Operation(summary = "Przywrocenie dostepu zablokowanemu kontu")
|
||||
public UserView activate(@PathVariable Long id) {
|
||||
return userService.activate(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trwale usuniecie konta.
|
||||
*
|
||||
* Osobne od dezaktywacji, bo to inna decyzja: dezaktywacja odbiera dostep
|
||||
* i zostawia slad, usuniecie zabiera konto z listy. Praca uzytkownika
|
||||
* zostaje w obu przypadkach.
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "Trwale usuniecie konta")
|
||||
public void delete(@PathVariable Long id,
|
||||
@AuthenticationPrincipal AuthenticatedUser principal) {
|
||||
userService.delete(id, principal);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reset-password")
|
||||
@Operation(summary = "Reset hasla. Nowe haslo zwracane jeden raz.")
|
||||
public ResetPasswordResponse resetPassword(@PathVariable Long id) {
|
||||
|
||||
@@ -116,6 +116,59 @@ public class UserService {
|
||||
return AuthService.toView(userRepository.save(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Przywrocenie dostepu wczesniej zablokowanemu kontu.
|
||||
*
|
||||
* Dezaktywacja jest odwracalna wlasnie po to: konto zablokowane na czas
|
||||
* urlopu albo do wyjasnienia ma dac sie wlaczyc z powrotem, bez zakladania
|
||||
* nowego i bez tracenia sladu w dzienniku.
|
||||
*/
|
||||
@Transactional
|
||||
public UserView activate(Long id) {
|
||||
User user = require(id);
|
||||
user.setActive(true);
|
||||
log.info("Aktywowano konto {}", user.getEmail());
|
||||
return AuthService.toView(userRepository.save(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trwale usuniecie konta.
|
||||
*
|
||||
* KIEDY ZAMIAST DEZAKTYWACJI
|
||||
* -----------------------------------------------------------------------
|
||||
* Dezaktywacja odbiera dostep, ale zostawia konto na liscie i w dzienniku.
|
||||
* Usuniecie ma sens przy koncie zalozonym przez pomylke albo dla osoby,
|
||||
* ktora nigdy nie powinna go dostac. Przy zwyklym odejsciu pracownika
|
||||
* lepsza jest dezaktywacja: zostaje wtedy widoczne, kto co zrobil.
|
||||
*
|
||||
* CO SIE DZIEJE Z JEGO PRACA
|
||||
* -----------------------------------------------------------------------
|
||||
* Nic nie przepada. Pliki w bibliotece i ogloszone zadania maja
|
||||
* `ON DELETE SET NULL` na kolumnie autora, wiec zostaja na miejscu,
|
||||
* tracac jedynie informacje o tym, kto je dodal. Kasowanie kaskadowe
|
||||
* zabieraloby tresc szkolen razem z kontem, ktore ja wprowadzilo.
|
||||
*
|
||||
* TE SAME DWIE BARIERY CO PRZY DEZAKTYWACJI
|
||||
* -----------------------------------------------------------------------
|
||||
* Nie mozna usunac wlasnego konta ani ostatniego aktywnego administratora:
|
||||
* jedno i drugie konczyloby sie utrata dostepu do panelu bez mozliwosci
|
||||
* odzyskania go z poziomu aplikacji.
|
||||
*/
|
||||
@Transactional
|
||||
public void delete(Long id, AuthenticatedUser actor) {
|
||||
User user = require(id);
|
||||
|
||||
if (user.getId().equals(actor.id())) {
|
||||
throw new ValidationException("Nie można usunąć własnego konta");
|
||||
}
|
||||
if (user.getRole() == UserRole.ADMIN && user.isActive()) {
|
||||
assertNotLastActiveAdmin(user, "To ostatnie aktywne konto administratora, nie można go usunąć");
|
||||
}
|
||||
|
||||
userRepository.delete(user);
|
||||
log.info("Usunieto konto {}", user.getEmail());
|
||||
}
|
||||
|
||||
/** Reset hasla przez administratora. Nowe haslo pokazywane jeden raz. */
|
||||
@Transactional
|
||||
public ResetPasswordResponse resetPassword(Long id) {
|
||||
|
||||
@@ -86,6 +86,19 @@ public class ContactAdminController {
|
||||
return contacts.update(id, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Co przepadnie razem z kontaktem.
|
||||
*
|
||||
* Panel pobiera to PRZED pytaniem o potwierdzenie, tak samo jak przy
|
||||
* usuwaniu webinaru: liczby zamieniaja "czy na pewno?" w decyzje opartą
|
||||
* na tym, co faktycznie zniknie.
|
||||
*/
|
||||
@GetMapping("/{id}/deletion-impact")
|
||||
@Operation(summary = "Co przepadnie razem z kontaktem")
|
||||
public ContactService.DeletionImpact deletionImpact(@PathVariable Long id) {
|
||||
return contacts.deletionImpact(id);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "Usuniecie kontaktu")
|
||||
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||
|
||||
@@ -56,17 +56,20 @@ public class ContactService {
|
||||
private final ContactGroupMemberRepository memberRepository;
|
||||
private final ConsentRepository consentRepository;
|
||||
private final SettingsService settings;
|
||||
private final com.softspm.webinar.repository.EnrollmentRepository enrollmentRepository;
|
||||
|
||||
public ContactService(ContactRepository contactRepository,
|
||||
ContactGroupRepository groupRepository,
|
||||
ContactGroupMemberRepository memberRepository,
|
||||
ConsentRepository consentRepository,
|
||||
SettingsService settings) {
|
||||
SettingsService settings,
|
||||
com.softspm.webinar.repository.EnrollmentRepository enrollmentRepository) {
|
||||
this.contactRepository = contactRepository;
|
||||
this.groupRepository = groupRepository;
|
||||
this.memberRepository = memberRepository;
|
||||
this.consentRepository = consentRepository;
|
||||
this.settings = settings;
|
||||
this.enrollmentRepository = enrollmentRepository;
|
||||
}
|
||||
|
||||
// --- odczyt ---
|
||||
@@ -171,6 +174,32 @@ public class ContactService {
|
||||
return get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Co przepadnie razem z kontaktem.
|
||||
*
|
||||
* Panel pyta o potwierdzenie DOPIERO Z TYMI LICZBAMI, tak samo jak przy
|
||||
* usuwaniu webinaru. "Czy na pewno?" bez nich nie daje administratorowi
|
||||
* niczego, na czym moglby oprzec decyzje: przy zwyklym wpisie
|
||||
* marketingowym odpowiedz jest zupelnie inna niz przy kursancie
|
||||
* z oplaconym kursem.
|
||||
*
|
||||
* `student` rozstrzyga, czy kontakt ma konto w Akademii. Usuniecie takiego
|
||||
* kontaktu **odbiera dostep do kursow**, bo zapisy wisza na nim kaskadowo.
|
||||
* Zamowienia zostaja: sa dokumentem finansowym i maja `ON DELETE SET NULL`.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public DeletionImpact deletionImpact(Long id) {
|
||||
Contact contact = require(id);
|
||||
return new DeletionImpact(
|
||||
enrollmentRepository.countByContactId(id),
|
||||
consentRepository.countByContactId(id),
|
||||
contact.hasAccount());
|
||||
}
|
||||
|
||||
/** @param student czy kontakt ma konto kursanta, czyli dostep do Akademii */
|
||||
public record DeletionImpact(long enrollments, long consents, boolean student) {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long id) {
|
||||
Contact contact = require(id);
|
||||
|
||||
@@ -12,6 +12,14 @@ public interface ConsentRepository extends JpaRepository<Consent, Long> {
|
||||
|
||||
List<Consent> findByRegistrationId(Long registrationId);
|
||||
|
||||
/**
|
||||
* Ile zgod ma kontakt.
|
||||
*
|
||||
* Uzywane przy ostrzezeniu przed usunieciem: zgody sa sladem rozliczalnym,
|
||||
* czyli dowodem na to, na jakiej podstawie wolno bylo cokolwiek wyslac.
|
||||
*/
|
||||
long countByContactId(Long contactId);
|
||||
|
||||
/** Zgody dla wielu zapisow naraz, do listy i eksportu. */
|
||||
@Query("select c from Consent c where c.registration.id in :registrationIds")
|
||||
List<Consent> findByRegistrationIdIn(@Param("registrationIds") Collection<Long> registrationIds);
|
||||
|
||||
@@ -43,6 +43,9 @@ public interface EnrollmentRepository extends JpaRepository<Enrollment, Long> {
|
||||
/** Wszystkie zapisy kursu, bez wzgledu na stan. Do podsumowania skutkow skasowania. */
|
||||
long countByCourseId(Long courseId);
|
||||
|
||||
/** Ile zapisow na kursy ma kontakt. Uzywane przy ostrzezeniu przed usunieciem. */
|
||||
long countByContactId(Long contactId);
|
||||
|
||||
/** Zapisani wraz z kontaktem, do listy w panelu i eksportu CSV. */
|
||||
@Query("""
|
||||
select e from Enrollment e
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { client, errorMessage } from '../../api/client'
|
||||
import type { Contact, ContactGroup, Page } from '../../api/types'
|
||||
import { useConfirm } from '../../components/Confirm'
|
||||
import { Icon } from '../../components/Icon'
|
||||
import { PAGE_SIZE, Pagination } from '../../components/Pagination'
|
||||
import { useToasts } from '../../components/Toasts'
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
Card,
|
||||
EmptyState,
|
||||
ErrorState,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
Spinner,
|
||||
@@ -33,8 +35,22 @@ const SOURCE_LABEL: Record<string, string> = {
|
||||
REGISTRATION: 'Z zapisu na webinar',
|
||||
}
|
||||
|
||||
/**
|
||||
* Co przepadnie razem z kontaktem.
|
||||
*
|
||||
* Pytanie o usuniecie ma nazywac konkretne liczby, a nie samo "czy na pewno":
|
||||
* kontakt marketingowy bez sladu to zupelnie inna decyzja niz konto kursanta
|
||||
* z oplaconymi kursami.
|
||||
*/
|
||||
type ContactDeletionImpact = {
|
||||
enrollments: number
|
||||
consents: number
|
||||
student: boolean
|
||||
}
|
||||
|
||||
export function ContactListPage() {
|
||||
const queryClient = useQueryClient()
|
||||
const confirm = useConfirm()
|
||||
const toasts = useToasts()
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
@@ -87,6 +103,85 @@ export function ContactListPage() {
|
||||
onError: (error) => toasts.error(errorMessage(error, 'Nie udało się przypisać do grupy')),
|
||||
})
|
||||
|
||||
const remove = useMutation({
|
||||
mutationFn: (id: number) => client.delete(`/admin/contacts/${id}`),
|
||||
onSuccess: (_data, id) => {
|
||||
toasts.success('Kontakt został usunięty')
|
||||
// zaznaczenie nie moze przezyc usunietego wiersza: masowe przypisanie
|
||||
// do grupy poszloby wtedy z identyfikatorem, ktorego juz nie ma
|
||||
setSelected((current) => current.filter((x) => x !== id))
|
||||
queryClient.invalidateQueries({ queryKey: ['contacts'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['contact-groups'] })
|
||||
},
|
||||
onError: (error) => toasts.error(errorMessage(error, 'Nie udało się usunąć kontaktu')),
|
||||
})
|
||||
|
||||
/**
|
||||
* Pytanie o usuniecie zlozone z tego, co naprawde przepadnie.
|
||||
*
|
||||
* Impact pobieramy przed pokazaniem okna, bo dopiero on rozstrzyga, czy
|
||||
* chodzi o zwykly wpis marketingowy, czy o konto kursanta z dostepem do
|
||||
* oplaconych materialow.
|
||||
*/
|
||||
async function handleDelete(contact: Contact) {
|
||||
let impact: ContactDeletionImpact
|
||||
try {
|
||||
const response = await client.get<ContactDeletionImpact>(
|
||||
`/admin/contacts/${contact.id}/deletion-impact`,
|
||||
)
|
||||
impact = response.data
|
||||
} catch (error) {
|
||||
toasts.error(errorMessage(error, 'Nie udało się sprawdzić, co zniknie razem z kontaktem'))
|
||||
return
|
||||
}
|
||||
|
||||
const fullName = `${contact.firstName} ${contact.lastName}`.trim()
|
||||
|
||||
const ok = await confirm({
|
||||
title: impact.student ? 'Usunąć konto kursanta?' : 'Usunąć kontakt?',
|
||||
message: (
|
||||
<>
|
||||
<p>
|
||||
Kontakt <strong>{fullName || contact.email}</strong> (
|
||||
<span className="hx-data">{contact.email}</span>) zostanie usunięty bezpowrotnie.
|
||||
</p>
|
||||
|
||||
{impact.student && (
|
||||
<p className="mt-2">
|
||||
To <strong>konto kursanta</strong>. Usunięcie odbiera dostęp do kursów: osoba nie
|
||||
zaloguje się do Akademii i straci wgląd w materiały oraz certyfikaty.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{impact.enrollments > 0 && (
|
||||
<p className="mt-2">
|
||||
Przepadnie <strong>{impact.enrollments}</strong>{' '}
|
||||
{plural(impact.enrollments, 'zapis na kurs', 'zapisy na kursy', 'zapisów na kursy')}{' '}
|
||||
wraz z postępem w nauce.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{impact.consents > 0 && (
|
||||
<p className="mt-2">
|
||||
Zniknie także historia zgód (<strong>{impact.consents}</strong>{' '}
|
||||
{plural(impact.consents, 'wpis', 'wpisy', 'wpisów')}). To ślad rozliczalny, czyli
|
||||
dowód podstawy wysyłki.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{impact.student && (
|
||||
<p className="mt-2 text-slate-500">
|
||||
Zamówienia zostają: to dokumenty finansowe i przeżywają usunięcie konta.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
confirmLabel: impact.student ? 'Usuń konto kursanta' : 'Usuń kontakt',
|
||||
destructive: true,
|
||||
})
|
||||
if (ok) remove.mutate(contact.id)
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
setExporting(true)
|
||||
try {
|
||||
@@ -262,6 +357,20 @@ export function ContactListPage() {
|
||||
{contact.company && (
|
||||
<p className="text-sm text-slate-500">{contact.company}</p>
|
||||
)}
|
||||
<div className="flex justify-end gap-1">
|
||||
<IconButton
|
||||
icon="pencil"
|
||||
label={`Edytuj kontakt ${contact.email}`}
|
||||
onClick={() => setEditing(contact)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="trash"
|
||||
label={`Usuń kontakt ${contact.email}`}
|
||||
onClick={() => handleDelete(contact)}
|
||||
disabled={remove.isPending}
|
||||
className="text-alarm-600 hover:bg-alarm-50"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -306,7 +415,7 @@ export function ContactListPage() {
|
||||
<ConsentBadge contact={contact} />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex justify-end">
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@@ -315,6 +424,17 @@ export function ContactListPage() {
|
||||
>
|
||||
Edytuj
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon="trash"
|
||||
onClick={() => handleDelete(contact)}
|
||||
disabled={remove.isPending}
|
||||
title={`Usuń kontakt ${contact.email}`}
|
||||
className="text-alarm-600 hover:bg-alarm-50"
|
||||
>
|
||||
<span className="sr-only">Usuń kontakt {contact.email}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -358,6 +478,21 @@ export function ContactListPage() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Polska odmiana rzeczownika po liczbie.
|
||||
*
|
||||
* "1 zapis", "2 zapisy", "5 zapisow": bez tego komunikat o skutkach usuniecia
|
||||
* czytalby sie jak tekst maszyny, a ma byc zdaniem, na ktorym uzytkownik
|
||||
* opiera decyzje nieodwracalna.
|
||||
*/
|
||||
function plural(count: number, one: string, few: string, many: string): string {
|
||||
if (count === 1) return one
|
||||
const lastTwo = count % 100
|
||||
const last = count % 10
|
||||
if (lastTwo >= 12 && lastTwo <= 14) return many
|
||||
return last >= 2 && last <= 4 ? few : many
|
||||
}
|
||||
|
||||
/**
|
||||
* Stan zgody jednym spojrzeniem.
|
||||
*
|
||||
|
||||
@@ -88,6 +88,26 @@ export function UserListPage() {
|
||||
onError: (error) => toasts.error(errorMessage(error, 'Nie udało się dezaktywować konta')),
|
||||
})
|
||||
|
||||
const activate = useMutation({
|
||||
mutationFn: (id: number) => client.post(`/admin/users/${id}/activate`),
|
||||
onSuccess: () => {
|
||||
toasts.success('Konto zostało odblokowane')
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||
},
|
||||
onError: (error) => toasts.error(errorMessage(error, 'Nie udało się odblokować konta')),
|
||||
})
|
||||
|
||||
const remove = useMutation({
|
||||
mutationFn: (id: number) => client.delete(`/admin/users/${id}`),
|
||||
onSuccess: () => {
|
||||
toasts.success('Konto zostało usunięte')
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||
},
|
||||
// backend odrzuca usuniecie wlasnego konta i ostatniego aktywnego
|
||||
// administratora, wiec jego komunikat jest tu wazniejszy niz nasz zapasowy
|
||||
onError: (error) => toasts.error(errorMessage(error, 'Nie udało się usunąć konta')),
|
||||
})
|
||||
|
||||
const reset = useMutation({
|
||||
mutationFn: async (user: User) => {
|
||||
const response = await client.post<{ password: string; message: string }>(
|
||||
@@ -102,6 +122,83 @@ export function UserListPage() {
|
||||
onError: (error) => toasts.error(errorMessage(error, 'Nie udało się zresetować hasła')),
|
||||
})
|
||||
|
||||
/**
|
||||
* Pytania o operacje na koncie w jednym miejscu.
|
||||
*
|
||||
* Widok ma dwa warianty, karty na telefonie i tabele od duzego ekranu.
|
||||
* Wpisanie tresci osobno w kazdym oznaczaloby, ze poprawka w jednym omija
|
||||
* drugi i to samo pytanie brzmi inaczej zaleznie od szerokosci okna.
|
||||
*/
|
||||
async function handleActivate(user: User) {
|
||||
const ok = await confirm({
|
||||
title: 'Odblokować konto?',
|
||||
message: (
|
||||
<>
|
||||
Konto <strong>{user.email}</strong> odzyska dostęp do panelu w zakresie roli{' '}
|
||||
{ROLE_LABEL[user.role]}. Hasło zostaje bez zmian.
|
||||
</>
|
||||
),
|
||||
confirmLabel: 'Odblokuj konto',
|
||||
})
|
||||
if (ok) activate.mutate(user.id)
|
||||
}
|
||||
|
||||
async function handleDelete(user: User) {
|
||||
const ok = await confirm({
|
||||
title: 'Usunąć konto?',
|
||||
message: (
|
||||
<>
|
||||
<p>
|
||||
Konto <strong>{user.email}</strong> zostanie usunięte trwale, bez możliwości
|
||||
przywrócenia.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Jeśli chodzi tylko o odebranie dostępu, lepszym wyborem jest dezaktywacja: konto
|
||||
zostaje wraz z historią i można je później włączyć z powrotem.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
confirmLabel: 'Usuń konto',
|
||||
destructive: true,
|
||||
})
|
||||
if (ok) remove.mutate(user.id)
|
||||
}
|
||||
|
||||
async function handleDeactivate(user: User) {
|
||||
const ok = await confirm({
|
||||
title: 'Dezaktywować konto?',
|
||||
message: (
|
||||
<>
|
||||
Konto <strong>{user.email}</strong> straci dostęp do panelu natychmiast, także
|
||||
w otwartych już sesjach. Można je później włączyć z powrotem.
|
||||
</>
|
||||
),
|
||||
confirmLabel: 'Dezaktywuj konto',
|
||||
/*
|
||||
* Bez `destructive`: dezaktywacja jest ODWRACALNA, a od teraz jednym
|
||||
* kliknieciem, bo obok stoi przycisk odblokowania. Czerwone ostrzezenie
|
||||
* "tej operacji nie da sie cofnac" przeczyloby zdaniu wyzej, ktore mowi
|
||||
* wprost, ze konto da sie wlaczyc z powrotem. Ostrzezenie uzyte wszedzie
|
||||
* przestaje cokolwiek znaczyc tam, gdzie strata jest prawdziwa.
|
||||
*/
|
||||
})
|
||||
if (ok) deactivate.mutate(user.id)
|
||||
}
|
||||
|
||||
async function handleReset(user: User) {
|
||||
const ok = await confirm({
|
||||
title: 'Zresetować hasło?',
|
||||
message: (
|
||||
<>
|
||||
Konto <strong>{user.email}</strong> dostanie nowe hasło jednorazowe i będzie musiało je
|
||||
zmienić przy najbliższym logowaniu.
|
||||
</>
|
||||
),
|
||||
confirmLabel: 'Zresetuj hasło',
|
||||
})
|
||||
if (ok) reset.mutate(user)
|
||||
}
|
||||
|
||||
function startEdit(user: User) {
|
||||
setEditing(user)
|
||||
setCreating(false)
|
||||
@@ -275,43 +372,36 @@ export function UserListPage() {
|
||||
<IconButton
|
||||
icon="key"
|
||||
label="Resetuj hasło"
|
||||
onClick={async () => {
|
||||
const ok = await confirm({
|
||||
title: 'Zresetować hasło?',
|
||||
message: (
|
||||
<>
|
||||
Konto <strong>{user.email}</strong> dostanie nowe hasło jednorazowe
|
||||
i będzie musiało je zmienić przy najbliższym logowaniu.
|
||||
</>
|
||||
),
|
||||
confirmLabel: 'Zresetuj hasło',
|
||||
})
|
||||
if (ok) reset.mutate(user)
|
||||
}}
|
||||
onClick={() => handleReset(user)}
|
||||
disabled={reset.isPending}
|
||||
/>
|
||||
{!user.active && (
|
||||
<IconButton
|
||||
icon="check"
|
||||
label="Odblokuj"
|
||||
onClick={() => handleActivate(user)}
|
||||
disabled={activate.isPending}
|
||||
className="text-ok-600 hover:bg-ok-50"
|
||||
/>
|
||||
)}
|
||||
{user.active && !isSelf && (
|
||||
<IconButton
|
||||
icon="user-off"
|
||||
label="Dezaktywuj"
|
||||
onClick={async () => {
|
||||
const ok = await confirm({
|
||||
title: 'Dezaktywować konto?',
|
||||
message: (
|
||||
<>
|
||||
Konto <strong>{user.email}</strong> straci dostęp do panelu
|
||||
natychmiast, także w otwartych już sesjach. Można je później
|
||||
włączyć z powrotem.
|
||||
</>
|
||||
),
|
||||
confirmLabel: 'Dezaktywuj konto',
|
||||
})
|
||||
if (ok) deactivate.mutate(user.id)
|
||||
}}
|
||||
onClick={() => handleDeactivate(user)}
|
||||
disabled={deactivate.isPending}
|
||||
className="text-alarm-600 hover:bg-alarm-50"
|
||||
/>
|
||||
)}
|
||||
{!isSelf && (
|
||||
<IconButton
|
||||
icon="trash"
|
||||
label="Usuń konto"
|
||||
onClick={() => handleDelete(user)}
|
||||
disabled={remove.isPending}
|
||||
className="text-alarm-600 hover:bg-alarm-50"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -374,43 +464,29 @@ export function UserListPage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="key"
|
||||
onClick={async () => {
|
||||
const ok = await confirm({
|
||||
title: 'Zresetować hasło?',
|
||||
message: (
|
||||
<>
|
||||
Konto <strong>{user.email}</strong> dostanie nowe hasło
|
||||
jednorazowe i będzie musiało je zmienić przy najbliższym
|
||||
logowaniu.
|
||||
</>
|
||||
),
|
||||
confirmLabel: 'Zresetuj hasło',
|
||||
})
|
||||
if (ok) reset.mutate(user)
|
||||
}}
|
||||
onClick={() => handleReset(user)}
|
||||
disabled={reset.isPending}
|
||||
title="Resetuj hasło"
|
||||
>
|
||||
<span className="sr-only">Resetuj hasło</span>
|
||||
</Button>
|
||||
{!user.active && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="check"
|
||||
onClick={() => handleActivate(user)}
|
||||
disabled={activate.isPending}
|
||||
title="Odblokuj"
|
||||
className="text-ok-600 hover:bg-ok-50"
|
||||
>
|
||||
<span className="sr-only">Odblokuj</span>
|
||||
</Button>
|
||||
)}
|
||||
{user.active && !isSelf && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="user-off"
|
||||
onClick={async () => {
|
||||
const ok = await confirm({
|
||||
title: 'Dezaktywować konto?',
|
||||
message: (
|
||||
<>
|
||||
Konto <strong>{user.email}</strong> straci dostęp do panelu
|
||||
natychmiast, także w otwartych już sesjach. Można je później
|
||||
włączyć z powrotem.
|
||||
</>
|
||||
),
|
||||
confirmLabel: 'Dezaktywuj konto',
|
||||
})
|
||||
if (ok) deactivate.mutate(user.id)
|
||||
}}
|
||||
onClick={() => handleDeactivate(user)}
|
||||
disabled={deactivate.isPending}
|
||||
title="Dezaktywuj"
|
||||
className="text-alarm-600 hover:bg-alarm-50"
|
||||
@@ -418,6 +494,18 @@ export function UserListPage() {
|
||||
<span className="sr-only">Dezaktywuj</span>
|
||||
</Button>
|
||||
)}
|
||||
{!isSelf && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="trash"
|
||||
onClick={() => handleDelete(user)}
|
||||
disabled={remove.isPending}
|
||||
title="Usuń konto"
|
||||
className="text-alarm-600 hover:bg-alarm-50"
|
||||
>
|
||||
<span className="sr-only">Usuń konto</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user