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:
@@ -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