From 68cfe7594bec9283f369277ab936185758911c70 Mon Sep 17 00:00:00 2001 From: adevopg Date: Sun, 12 Jul 2026 22:01:19 +0000 Subject: [PATCH] =?UTF-8?q?Fase=203=20(isla):=20recuperar=20=C3=ADtems=20(?= =?UTF-8?q?restore=5Fitems)=20en=20React/TSX,=20flujo=20de=202=20pasos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit restore_items_view ahora responde JSON cuando la petición es AJAX (X-Requested-With): GET con character_name -> {items}; POST recover_id -> {success, message}. Se conserva el flujo legacy (redirect+messages) para no-AJAX. RestoreItemsForm.tsx: paso 1 elegir personaje y buscar ítems borrados (SOAP), paso 2 elegir ítem y recuperarlo; refresca la lista tras recuperar. La vista pasa character_names por json_script. Verificado: check OK, render de la isla, redirige a login sin sesión. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/components/RestoreItemsForm.tsx | 124 +++++++++++++++++++ frontend/src/entries/restore_items.tsx | 14 +++ frontend/vite.config.ts | 1 + home/templates/partials/restore_items.html | 37 ++---- home/views/characters.py | 28 ++++- 5 files changed, 169 insertions(+), 35 deletions(-) create mode 100644 frontend/src/components/RestoreItemsForm.tsx create mode 100644 frontend/src/entries/restore_items.tsx diff --git a/frontend/src/components/RestoreItemsForm.tsx b/frontend/src/components/RestoreItemsForm.tsx new file mode 100644 index 0000000..94b085c --- /dev/null +++ b/frontend/src/components/RestoreItemsForm.tsx @@ -0,0 +1,124 @@ +import { useState } from 'react' + +interface DeletedItem { + recover_id: string + item_name: string + item_id: string + count: string +} + +interface Props { + url: string + csrfToken: string + characters: string[] +} + +export function RestoreItemsForm({ url, csrfToken, characters }: Props) { + const [character, setCharacter] = useState('') + const [items, setItems] = useState(null) + const [recoverId, setRecoverId] = useState('') + const [busy, setBusy] = useState(false) + const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null) + + async function doSearch(char: string, keepMessage = false) { + if (!char) return + setBusy(true) + if (!keepMessage) setMessage(null) + setItems(null) + setRecoverId('') + try { + const r = await fetch(`${url}?character_name=${encodeURIComponent(char)}`, { + headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json' }, + credentials: 'same-origin', + }) + const d: { items?: DeletedItem[]; error?: string } = await r.json() + if (d.error) setMessage({ ok: false, html: `${d.error}` }) + else setItems(d.items || []) + } catch { + setMessage({ ok: false, html: 'Error al buscar los ítems.' }) + } finally { + setBusy(false) + } + } + + function search(e: React.FormEvent) { + e.preventDefault() + if (busy) return + void doSearch(character) + } + + async function recover(e: React.FormEvent) { + e.preventDefault() + if (!recoverId || busy) return + setBusy(true) + setMessage(null) + const body = new URLSearchParams() + body.set('recover_id', recoverId) + body.set('character_name', character) + body.set('csrfmiddlewaretoken', csrfToken) + try { + const r = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': csrfToken, + Accept: 'application/json', + }, + credentials: 'same-origin', + body: body.toString(), + }) + const d: { success?: boolean; message?: string } = await r.json() + setMessage({ ok: !!d.success, html: d.message || '' }) + if (d.success) { + // refrescar la lista de ítems tras recuperar uno (conservando el mensaje) + void doSearch(character, true) + } + } catch { + setMessage({ ok: false, html: 'Error al recuperar el ítem.' }) + } finally { + setBusy(false) + } + } + + return ( + <> +
+ + +
+ + {items !== null && ( + items.length > 0 ? ( +
+ + + +
+ ) : ( +

No hay ítems eliminados para este personaje.

+ ) + )} + +
+ {message && } +
+ + ) +} diff --git a/frontend/src/entries/restore_items.tsx b/frontend/src/entries/restore_items.tsx new file mode 100644 index 0000000..0285aa0 --- /dev/null +++ b/frontend/src/entries/restore_items.tsx @@ -0,0 +1,14 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { RestoreItemsForm } from '../components/RestoreItemsForm' + +const el = document.getElementById('restore-items-app') +if (el) { + const dEl = document.getElementById('restore-items-data') + const characters: string[] = dEl ? JSON.parse(dEl.textContent || '[]') : [] + createRoot(el).render( + + + , + ) +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 4a2a99b..eaa409e 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -31,6 +31,7 @@ export default defineConfig({ vote_points: resolve(__dirname, 'src/entries/vote_points.tsx'), history_table: resolve(__dirname, 'src/entries/history_table.tsx'), restore_character: resolve(__dirname, 'src/entries/restore_character.tsx'), + restore_items: resolve(__dirname, 'src/entries/restore_items.tsx'), }, }, }, diff --git a/home/templates/partials/restore_items.html b/home/templates/partials/restore_items.html index 479d686..3f8f350 100644 --- a/home/templates/partials/restore_items.html +++ b/home/templates/partials/restore_items.html @@ -17,35 +17,14 @@

Se pueden consultar ítems borrados en un mismo personaje cada 8 horas.

- -
- - - -
- - {% if deleted_items %} -
- {% csrf_token %} - - - - -
- {% elif selected_character %} -

No hay ítems eliminados disponibles para restaurar.

- {% endif %} + {% load django_vite %} + +
+ {{ character_names|json_script:"restore-items-data" }} + {% vite_asset 'src/entries/restore_items.tsx' %} diff --git a/home/views/characters.py b/home/views/characters.py index 9715eca..e8f956e 100644 --- a/home/views/characters.py +++ b/home/views/characters.py @@ -204,6 +204,9 @@ def restore_items_view(request): except Exception as e: messages.error(request, f"Error al obtener los personajes: {str(e)}") + # La isla React usa AJAX (X-Requested-With); en ese caso respondemos JSON. + is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' + if request.method == "GET" and "character_name" in request.GET: selected_character = request.GET.get("character_name") @@ -229,13 +232,20 @@ def restore_items_view(request): }) except Exception as e: + if is_ajax: + return JsonResponse({'error': f'Error al obtener los ítems eliminados: {str(e)}'}) messages.error(request, f"Error al obtener los ítems eliminados: {str(e)}") + if is_ajax: + return JsonResponse({'items': deleted_items}) + if request.method == "POST": recover_id = request.POST.get("recover_id") selected_character = request.POST.get("character_name") if not recover_id or not selected_character: + if is_ajax: + return JsonResponse({'success': False, 'message': 'Selecciona un personaje y un ítem para restaurar.'}) messages.error(request, "Selecciona un personaje y un ítem para restaurar.") return redirect('restore-items') @@ -243,20 +253,26 @@ def restore_items_view(request): # Ejecutar comando SOAP para restaurar el ítem command = f".item restore {recover_id} {selected_character}" response = execute_soap_command(command) - - if "success" in response.lower(): - messages.success(request, "Ítem restaurado exitosamente.") - else: - messages.error(request, "No se pudo restaurar el ítem.") + ok = "success" in response.lower() except Exception as e: + if is_ajax: + return JsonResponse({'success': False, 'message': f'Error al restaurar el ítem: {str(e)}'}) messages.error(request, f"Error al restaurar el ítem: {str(e)}") + return redirect('restore-items') + if is_ajax: + msg = ('Ítem restaurado exitosamente.' if ok + else 'No se pudo restaurar el ítem.') + return JsonResponse({'success': ok, 'message': msg}) + + messages.success(request, "Ítem restaurado exitosamente.") if ok else messages.error(request, "No se pudo restaurar el ítem.") return redirect('restore-items') return render(request, 'account/restore_items.html', { 'characters': characters, 'deleted_items': deleted_items, - 'selected_character': selected_character + 'selected_character': selected_character, + 'character_names': characters, }) def quest_character_view(request): username = request.session.get('username')