From aa5e90aa9c34f04f36d3c04d206470f1cf52e80d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 00:58:24 +0000 Subject: [PATCH] feat: integrate V2 frontend with real backend endpoints - Created CouponService for user coupons management. - Updated FlashcardService and AIService for creation and AI generation. - Updated UserService with admin statistics functionality. - Transitioned V2CouponCenter, V2FlashcardCreator, V2AIFlashcardGenerator, V2AdminDashboard, and V2AdminUsers from mock data to real service integration. - Added comprehensive unit tests for all integrated V2 pages. - Improved error handling and loading states across integrated components. Co-authored-by: godie <227743+godie@users.noreply.github.com> --- src/services/AIService.js | 5 ++ src/services/CouponService.js | 11 +++ src/services/FlashcardService.js | 5 ++ src/services/UserService.js | 7 +- src/v2/pages/V2AIFlashcardGenerator.jsx | 87 ++++++++++++++++---- src/v2/pages/V2AIFlashcardGenerator.test.jsx | 56 +++++++++++-- src/v2/pages/V2AdminDashboard.jsx | 60 ++++++++++---- src/v2/pages/V2AdminDashboard.test.jsx | 31 +++++-- src/v2/pages/V2AdminUsers.jsx | 62 ++++++++++---- src/v2/pages/V2AdminUsers.test.jsx | 37 +++++++-- src/v2/pages/V2CouponCenter.jsx | 36 +++++--- src/v2/pages/V2CouponCenter.test.jsx | 42 +++++----- src/v2/pages/V2FlashcardCreator.jsx | 70 +++++++++++++--- src/v2/pages/V2FlashcardCreator.test.jsx | 39 ++++++--- 14 files changed, 421 insertions(+), 127 deletions(-) create mode 100644 src/services/CouponService.js diff --git a/src/services/AIService.js b/src/services/AIService.js index 8ddc41e..bcaee82 100644 --- a/src/services/AIService.js +++ b/src/services/AIService.js @@ -22,4 +22,9 @@ export default class AIService extends BaseService { }, }); } + + static generateFlashcards(payload) { + const headers = this.getHeaders(); + return axios.post(BaseService.getURL("v2/ai/generate-flashcards"), payload, headers); + } } diff --git a/src/services/CouponService.js b/src/services/CouponService.js new file mode 100644 index 0000000..3ad28bd --- /dev/null +++ b/src/services/CouponService.js @@ -0,0 +1,11 @@ +import axios from "axios"; +import BaseService from "./BaseService"; + +class CouponService extends BaseService { + static getCoupons() { + const headers = this.getHeaders(); + return axios.get(BaseService.getURL("v2/coupons/me"), headers); + } +} + +export default CouponService; diff --git a/src/services/FlashcardService.js b/src/services/FlashcardService.js index a263102..01e0d4b 100644 --- a/src/services/FlashcardService.js +++ b/src/services/FlashcardService.js @@ -22,4 +22,9 @@ export default class FlashcardService extends BaseService { const headers = this.getHeaders(); return axios.post(BaseService.getURL(`flashcards/${id}/review`), { quality }, headers); } + + static createFlashcard(payload) { + const headers = this.getHeaders(); + return axios.post(BaseService.getURL("v2/flashcards"), payload, headers); + } } diff --git a/src/services/UserService.js b/src/services/UserService.js index 02ba5a8..1501c22 100644 --- a/src/services/UserService.js +++ b/src/services/UserService.js @@ -36,7 +36,6 @@ class UserService extends BaseService { static getPublicProfile(userId) { const headers = this.getHeaders(); return axios.get(BaseService.getURL(`users/${userId}/public-profile`), headers); - return axios.get(BaseService.getURL(`users/${userId}/public-profile`), headers); } // Admin: Listar usuarios @@ -57,6 +56,12 @@ class UserService extends BaseService { return axios.delete(BaseService.getURL(`users/${id}`), headers); } + // Admin V2: Estadísticas globales + static getAdminStats() { + const headers = this.getHeaders(); + return axios.get(BaseService.getURL("v2/admin/stats"), headers); + } + // Aliases para compatibilidad durante la transición si es necesario static createPlayer(params) { return this.createUser(params); diff --git a/src/v2/pages/V2AIFlashcardGenerator.jsx b/src/v2/pages/V2AIFlashcardGenerator.jsx index a5c24c7..d346d41 100644 --- a/src/v2/pages/V2AIFlashcardGenerator.jsx +++ b/src/v2/pages/V2AIFlashcardGenerator.jsx @@ -1,5 +1,7 @@ import { useState } from 'react'; import { useHistory } from 'react-router-dom'; +import AIService from '../../services/AIService'; +import FlashcardService from '../../services/FlashcardService'; import '../styles/v2-theme.css'; const V2AIFlashcardGenerator = () => { @@ -8,19 +10,56 @@ const V2AIFlashcardGenerator = () => { const [count, setCount] = useState(5); const [generating, setGenerating] = useState(false); const [suggestions, setSuggestions] = useState([]); + const [savingAll, setSavingAll] = useState(false); - const handleGenerate = (e) => { + const handleGenerate = async (e) => { e.preventDefault(); setGenerating(true); - // Mocking API call to POST /v2/ai/generate-flashcards - setTimeout(() => { - setSuggestions([ - { id: 's1', front: 'Tratamiento inicial de la cetoacidosis diabética', back: 'Reposición hídrica con solución salina al 0.9%' }, - { id: 's2', front: 'Criterios diagnósticos de preeclampsia', back: 'TA > 140/90 mmHg y proteinuria > 300mg/24h después de la semana 20' }, - { id: 's3', front: 'Signo de Murphy positivo indica:', back: 'Colecistitis aguda' } - ]); + try { + const response = await AIService.generateFlashcards({ topic, count }); + setSuggestions(response.data || []); + } catch (err) { + console.error("Error generating flashcards:", err); + alert("Error al generar sugerencias. Por favor, intenta de nuevo."); + } finally { setGenerating(false); - }, 1500); + } + }; + + const handleSaveOne = async (suggestion, index) => { + try { + await FlashcardService.createFlashcard({ + front: suggestion.front, + back: suggestion.back, + specialty_id: suggestion.specialty_id || 1 // Fallback or derived specialty + }); + // Mark as saved in UI + const newSuggestions = [...suggestions]; + newSuggestions[index].saved = true; + setSuggestions(newSuggestions); + } catch (err) { + console.error("Error saving individual flashcard:", err); + } + }; + + const handleSaveAll = async () => { + setSavingAll(true); + try { + const unsaved = suggestions.filter(s => !s.saved); + for (const s of unsaved) { + await FlashcardService.createFlashcard({ + front: s.front, + back: s.back, + specialty_id: s.specialty_id || 1 + }); + } + history.push('/v2/flashcards/repaso'); + } catch (err) { + console.error("Error saving all flashcards:", err); + alert("Ocurrió un error al guardar todas las flashcards."); + } finally { + setSavingAll(false); + } }; return ( @@ -32,7 +71,7 @@ const V2AIFlashcardGenerator = () => {

Generador de Flashcards con IA

-
0 ? '1fr 1fr' : '1fr', gap: '32px' }}> +
0 ? '1fr 1.5fr' : '1fr', gap: '32px' }}>

Configuración

@@ -46,6 +85,7 @@ const V2AIFlashcardGenerator = () => { value={topic} onChange={(e) => setTopic(e.target.value)} required + style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid var(--md-sys-color-outline-variant)', backgroundColor: 'transparent', color: 'inherit' }} />
@@ -57,7 +97,8 @@ const V2AIFlashcardGenerator = () => { min="1" max="20" value={count} - onChange={(e) => setCount(e.target.value)} + onChange={(e) => setCount(parseInt(e.target.value))} + style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid var(--md-sys-color-outline-variant)', backgroundColor: 'transparent', color: 'inherit' }} />
))} - )} diff --git a/src/v2/pages/V2AIFlashcardGenerator.test.jsx b/src/v2/pages/V2AIFlashcardGenerator.test.jsx index aede297..d209b00 100644 --- a/src/v2/pages/V2AIFlashcardGenerator.test.jsx +++ b/src/v2/pages/V2AIFlashcardGenerator.test.jsx @@ -2,34 +2,72 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import V2AIFlashcardGenerator from './V2AIFlashcardGenerator'; +import AIService from '../../services/AIService'; +import FlashcardService from '../../services/FlashcardService'; + +const mockPush = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useHistory: () => ({ + push: mockPush, + goBack: vi.fn() + }) + }; +}); + +vi.mock('../../services/AIService'); +vi.mock('../../services/FlashcardService'); describe('V2AIFlashcardGenerator', () => { - it('renders generator form', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('generates suggestions on form submit', async () => { + AIService.generateFlashcards.mockResolvedValue({ + data: [ + { front: 'AI Question', back: 'AI Answer' } + ] + }); + render( ); - expect(screen.getByPlaceholderText(/Ej: Diabetes Mellitus Tipo 2/i)).toBeTruthy(); - expect(screen.getByText('Generar Sugerencias')).toBeTruthy(); + fireEvent.change(screen.getByPlaceholderText(/Diabetes Mellitus/i), { target: { value: 'Diabetes' } }); + fireEvent.click(screen.getByText('Generar Sugerencias')); + + await waitFor(() => { + expect(screen.getByText('Q: AI Question')).toBeTruthy(); + expect(screen.getByText('A: AI Answer')).toBeTruthy(); + }); }); - it('generates suggestions on form submission', async () => { + it('saves all suggestions', async () => { + AIService.generateFlashcards.mockResolvedValue({ + data: [{ front: 'Q1', back: 'A1' }, { front: 'Q2', back: 'A2' }] + }); + FlashcardService.createFlashcard.mockResolvedValue({}); + render( ); - fireEvent.change(screen.getByPlaceholderText(/Ej: Diabetes Mellitus Tipo 2/i), { target: { value: 'HTA' } }); + fireEvent.change(screen.getByPlaceholderText(/Diabetes Mellitus/i), { target: { value: 'Diabetes' } }); fireEvent.click(screen.getByText('Generar Sugerencias')); - expect(screen.getByText('Generando...')).toBeTruthy(); + await waitFor(() => screen.getByText('Guardar Todas')); + fireEvent.click(screen.getByText('Guardar Todas')); await waitFor(() => { - expect(screen.getByText('Sugerencias Generadas')).toBeTruthy(); - expect(screen.getByText(/Q: Signo de Murphy positivo indica:/i)).toBeTruthy(); - }, { timeout: 2000 }); + expect(FlashcardService.createFlashcard).toHaveBeenCalledTimes(2); + expect(mockPush).toHaveBeenCalledWith('/v2/flashcards/repaso'); + }); }); }); diff --git a/src/v2/pages/V2AdminDashboard.jsx b/src/v2/pages/V2AdminDashboard.jsx index c5bd87d..de7b011 100644 --- a/src/v2/pages/V2AdminDashboard.jsx +++ b/src/v2/pages/V2AdminDashboard.jsx @@ -1,26 +1,52 @@ import { useState, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; +import UserService from '../../services/UserService'; import '../styles/v2-theme.css'; const V2AdminDashboard = () => { const history = useHistory(); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { - // Mocking API call for admin stats - setTimeout(() => { - setStats({ - totalUsers: 15420, - activeUsersToday: 1205, - newSubscriptions: 45, - revenueToday: ',250.00' - }); - setLoading(false); - }, 800); + const fetchStats = async () => { + try { + const response = await UserService.getAdminStats(); + setStats(response.data); + } catch (err) { + console.error("Error fetching admin stats:", err); + setError("Error al cargar estadísticas generales."); + } finally { + setLoading(false); + } + }; + + fetchStats(); }, []); - if (loading) return
; + if (loading) return ( +
+
+
+
+
+
+
+
+
+ ); + + if (error || !stats) return ( +
+
+ error +

Error

+

{error || "No se pudieron obtener los datos."}

+ +
+
+ ); return (
@@ -29,26 +55,26 @@ const V2AdminDashboard = () => {

Resumen general de la plataforma

-
+
Usuarios Totales
-
{stats.totalUsers}
+
{stats.totalUsers || 0}
Activos Hoy
-
{stats.activeUsersToday}
+
{stats.activeUsersToday || 0}
Nuevas Suscripciones
-
+{stats.newSubscriptions}
+
+{stats.newSubscriptions || 0}
Ingresos Hoy
-
{stats.revenueToday}
+
{stats.revenueToday || '$0.00'}
-
+

Acciones Rápidas

diff --git a/src/v2/pages/V2AdminDashboard.test.jsx b/src/v2/pages/V2AdminDashboard.test.jsx index 46ac1fe..946d26a 100644 --- a/src/v2/pages/V2AdminDashboard.test.jsx +++ b/src/v2/pages/V2AdminDashboard.test.jsx @@ -2,9 +2,25 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import V2AdminDashboard from './V2AdminDashboard'; +import UserService from '../../services/UserService'; + +vi.mock('../../services/UserService'); describe('V2AdminDashboard', () => { - it('renders stats after loading', async () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders admin stats correctly', async () => { + UserService.getAdminStats.mockResolvedValue({ + data: { + totalUsers: 100, + activeUsersToday: 10, + newSubscriptions: 2, + revenueToday: '$200.00' + } + }); + render( @@ -12,12 +28,14 @@ describe('V2AdminDashboard', () => { ); await waitFor(() => { - expect(screen.getByText('15420')).toBeTruthy(); - expect(screen.getByText(',250.00')).toBeTruthy(); - }, { timeout: 2000 }); + expect(screen.getByText('100')).toBeTruthy(); + expect(screen.getByText('$200.00')).toBeTruthy(); + }); }); - it('renders quick actions', async () => { + it('renders error state when API fails', async () => { + UserService.getAdminStats.mockRejectedValue(new Error('API Error')); + render( @@ -25,8 +43,7 @@ describe('V2AdminDashboard', () => { ); await waitFor(() => { - expect(screen.getByText('Gestionar Usuarios')).toBeTruthy(); - expect(screen.getByText('Logs de Actividad')).toBeTruthy(); + expect(screen.getByText(/Error al cargar estadísticas/i)).toBeTruthy(); }); }); }); diff --git a/src/v2/pages/V2AdminUsers.jsx b/src/v2/pages/V2AdminUsers.jsx index f3d83fe..6cbb06f 100644 --- a/src/v2/pages/V2AdminUsers.jsx +++ b/src/v2/pages/V2AdminUsers.jsx @@ -1,23 +1,29 @@ import { useState, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; +import UserService from '../../services/UserService'; import '../styles/v2-theme.css'; const V2AdminUsers = () => { const history = useHistory(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { - // Mocking API call for user list - setTimeout(() => { - setUsers([ - { id: 1, name: 'Juan Pérez', email: 'juan@example.com', role: 'Premium', status: 'active' }, - { id: 2, name: 'María García', email: 'maria@example.com', role: 'Free', status: 'active' }, - { id: 3, name: 'Carlos López', email: 'carlos@example.com', role: 'Premium', status: 'inactive' }, - { id: 4, name: 'Ana Martínez', email: 'ana@example.com', role: 'Admin', status: 'active' } - ]); - setLoading(false); - }, 800); + const fetchUsers = async () => { + try { + const response = await UserService.getUsers(); + // Ensure response data format is handled correctly (array or nested object) + setUsers(Array.isArray(response.data) ? response.data : (response.data.users || [])); + } catch (err) { + console.error("Error fetching users:", err); + setError("No se pudo cargar la lista de usuarios."); + } finally { + setLoading(false); + } + }; + + fetchUsers(); }, []); return ( @@ -29,6 +35,12 @@ const V2AdminUsers = () => {

Gestión de Usuarios

+ {error && ( +
+

{error}

+
+ )} +
@@ -42,21 +54,41 @@ const V2AdminUsers = () => { {loading ? ( - + + + + ) : users.length === 0 ? ( + + + ) : ( users.map(user => ( - +
+
+
+
+
+
+
+

No hay usuarios registrados.

+
{user.name}{user.name || (user.first_name ? `${user.first_name} ${user.last_name || ''}` : 'Sin nombre')} {user.email} - + {user.role}
-
- {user.status === 'active' ? 'Activo' : 'Inactivo'} +
+ {(user.status === 'active' || user.active) ? 'Activo' : 'Inactivo'}
diff --git a/src/v2/pages/V2AdminUsers.test.jsx b/src/v2/pages/V2AdminUsers.test.jsx index 07d6e21..8a73d01 100644 --- a/src/v2/pages/V2AdminUsers.test.jsx +++ b/src/v2/pages/V2AdminUsers.test.jsx @@ -2,9 +2,38 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import V2AdminUsers from './V2AdminUsers'; +import UserService from '../../services/UserService'; + +vi.mock('../../services/UserService'); describe('V2AdminUsers', () => { - it('renders user list after loading', async () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders user list correctly', async () => { + UserService.getUsers.mockResolvedValue({ + data: [ + { id: 1, name: 'Admin User', email: 'admin@test.com', role: 'Admin', status: 'active' }, + { id: 2, name: 'Premium User', email: 'premium@test.com', role: 'Premium', status: 'active' } + ] + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Admin User')).toBeTruthy(); + expect(screen.getByText('premium@test.com')).toBeTruthy(); + }); + }); + + it('handles empty user list', async () => { + UserService.getUsers.mockResolvedValue({ data: [] }); + render( @@ -12,9 +41,7 @@ describe('V2AdminUsers', () => { ); await waitFor(() => { - expect(screen.getByText('Juan Pérez')).toBeTruthy(); - expect(screen.getByText('maria@example.com')).toBeTruthy(); - expect(screen.getByText('Admin')).toBeTruthy(); - }, { timeout: 2000 }); + expect(screen.getByText(/No hay usuarios registrados/i)).toBeTruthy(); + }); }); }); diff --git a/src/v2/pages/V2CouponCenter.jsx b/src/v2/pages/V2CouponCenter.jsx index 79d2035..10fe313 100644 --- a/src/v2/pages/V2CouponCenter.jsx +++ b/src/v2/pages/V2CouponCenter.jsx @@ -1,27 +1,33 @@ import { useState, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; +import CouponService from '../../services/CouponService'; import '../styles/v2-theme.css'; const V2CouponCenter = () => { const history = useHistory(); const [coupons, setCoupons] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { - // Mocking API call to /v2/coupons/me - setTimeout(() => { - setCoupons([ - { id: 'c1', code: 'BIENVENIDO25', discount: '25%', description: 'Descuento de bienvenida', expires: '2025-12-31', status: 'active' }, - { id: 'c2', code: 'STUDENTLIFE', discount: '50%', description: 'Descuento para estudiantes', expires: '2025-06-30', status: 'active' }, - { id: 'c3', code: 'EXPIRED10', discount: '10%', description: 'Promoción antigua', expires: '2024-01-01', status: 'expired' } - ]); - setLoading(false); - }, 800); + const fetchCoupons = async () => { + try { + const response = await CouponService.getCoupons(); + setCoupons(response.data || []); + } catch (err) { + console.error("Error fetching coupons:", err); + setError("No se pudieron cargar los cupones. Por favor, intenta de nuevo más tarde."); + } finally { + setLoading(false); + } + }; + + fetchCoupons(); }, []); const copyToClipboard = (code) => { navigator.clipboard.writeText(code); - // Show some feedback here if needed, like a toast + // Toast logic could be added here if a global toast exists }; return ( @@ -43,6 +49,16 @@ const V2CouponCenter = () => { + ) : error ? ( +
+ error_outline +

{error}

+
+ ) : coupons.length === 0 ? ( +
+ confirmation_number +

No tienes cupones disponibles en este momento.

+
) : (
{coupons.map(coupon => ( diff --git a/src/v2/pages/V2CouponCenter.test.jsx b/src/v2/pages/V2CouponCenter.test.jsx index 992f4b1..713c6ed 100644 --- a/src/v2/pages/V2CouponCenter.test.jsx +++ b/src/v2/pages/V2CouponCenter.test.jsx @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import V2CouponCenter from './V2CouponCenter'; +import CouponService from '../../services/CouponService'; const mockGoBack = vi.fn(); vi.mock('react-router-dom', async () => { @@ -14,22 +15,30 @@ vi.mock('react-router-dom', async () => { }; }); +vi.mock('../../services/CouponService'); + describe('V2CouponCenter', () => { beforeEach(() => { vi.clearAllMocks(); }); it('renders loading state initially', () => { + CouponService.getCoupons.mockReturnValue(new Promise(() => {})); render( ); - // Find by class instead of role since materialize preloader might not have role progressbar by default expect(document.querySelector('.preloader-wrapper')).toBeTruthy(); }); it('renders coupons after loading', async () => { + CouponService.getCoupons.mockResolvedValue({ + data: [ + { id: 'c1', code: 'PROMO2025', discount: '20%', description: 'Test Coupon', expires: '2025-12-31', status: 'active' } + ] + }); + render( @@ -37,9 +46,9 @@ describe('V2CouponCenter', () => { ); await waitFor(() => { - expect(screen.getByText('BIENVENIDO25')).toBeTruthy(); - expect(screen.getByText('STUDENTLIFE')).toBeTruthy(); - }, { timeout: 2000 }); + expect(screen.getByText('PROMO2025')).toBeTruthy(); + expect(screen.getByText('20% OFF')).toBeTruthy(); + }); }); it('handles copy to clipboard', async () => { @@ -48,30 +57,21 @@ describe('V2CouponCenter', () => { }; Object.assign(navigator, { clipboard: mockClipboard }); - render( - - - - ); - - await waitFor(() => screen.getByText('BIENVENIDO25')); - - const copyButtons = screen.getAllByText('Copiar Código'); - fireEvent.click(copyButtons[0]); + CouponService.getCoupons.mockResolvedValue({ + data: [{ id: 'c1', code: 'PROMO2025', discount: '20%', description: 'Test Coupon', expires: '2025-12-31', status: 'active' }] + }); - expect(mockClipboard.writeText).toHaveBeenCalledWith('BIENVENIDO25'); - }); - - it('disables copy button for expired coupons', async () => { render( ); - await waitFor(() => screen.getByText('EXPIRED10')); + await waitFor(() => screen.getByText('PROMO2025')); + + const copyButton = screen.getByText('Copiar Código'); + fireEvent.click(copyButton); - const expiredButton = screen.getAllByRole('button', { name: /Copiar Código/i }).find(btn => btn.disabled); - expect(expiredButton).toBeTruthy(); + expect(mockClipboard.writeText).toHaveBeenCalledWith('PROMO2025'); }); }); diff --git a/src/v2/pages/V2FlashcardCreator.jsx b/src/v2/pages/V2FlashcardCreator.jsx index 2f66a64..f3250e3 100644 --- a/src/v2/pages/V2FlashcardCreator.jsx +++ b/src/v2/pages/V2FlashcardCreator.jsx @@ -1,24 +1,67 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; +import FlashcardService from '../../services/FlashcardService'; +import ExamService from '../../services/ExamService'; import '../styles/v2-theme.css'; const V2FlashcardCreator = () => { const history = useHistory(); const [front, setFront] = useState(''); const [back, setBack] = useState(''); - const [specialty, setSpecialty] = useState(''); + const [specialtyId, setSpecialtyId] = useState(''); + const [specialties, setSpecialties] = useState([]); const [saving, setSaving] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchSpecialties = async () => { + try { + const response = await ExamService.loadCategories(); + setSpecialties(response.data || []); + } catch (err) { + console.error("Error fetching specialties:", err); + } finally { + setLoading(false); + } + }; + + fetchSpecialties(); + }, []); const handleSave = async (e) => { e.preventDefault(); + if (!specialtyId || !front || !back) return; + setSaving(true); - // Mocking API call to POST /v2/flashcards - setTimeout(() => { + try { + await FlashcardService.createFlashcard({ + front, + back, + specialty_id: specialtyId + }); + history.push('/v2/flashcards/repaso'); + } catch (err) { + console.error("Error saving flashcard:", err); + alert("Ocurrió un error al guardar la flashcard. Por favor, intenta de nuevo."); + } finally { setSaving(false); - history.push('/v2/repaso'); - }, 1000); + } }; + if (loading) { + return ( +
+
+
+
+
+
+
+
+
+ ); + } + return (
@@ -34,16 +77,15 @@ const V2FlashcardCreator = () => {
@@ -57,6 +99,7 @@ const V2FlashcardCreator = () => { value={front} onChange={(e) => setFront(e.target.value)} required + style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid var(--md-sys-color-outline-variant)', backgroundColor: 'transparent', color: 'inherit' }} />
@@ -70,6 +113,7 @@ const V2FlashcardCreator = () => { value={back} onChange={(e) => setBack(e.target.value)} required + style={{ width: '100%', padding: '12px', borderRadius: '8px', border: '1px solid var(--md-sys-color-outline-variant)', backgroundColor: 'transparent', color: 'inherit' }} /> diff --git a/src/v2/pages/V2FlashcardCreator.test.jsx b/src/v2/pages/V2FlashcardCreator.test.jsx index b64735b..feb55b1 100644 --- a/src/v2/pages/V2FlashcardCreator.test.jsx +++ b/src/v2/pages/V2FlashcardCreator.test.jsx @@ -2,9 +2,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import V2FlashcardCreator from './V2FlashcardCreator'; +import FlashcardService from '../../services/FlashcardService'; +import ExamService from '../../services/ExamService'; const mockPush = vi.fn(); const mockGoBack = vi.fn(); + vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { @@ -16,40 +19,54 @@ vi.mock('react-router-dom', async () => { }; }); +vi.mock('../../services/FlashcardService'); +vi.mock('../../services/ExamService'); + describe('V2FlashcardCreator', () => { beforeEach(() => { vi.clearAllMocks(); + ExamService.loadCategories.mockResolvedValue({ + data: [{ id: 1, name: 'Pediatría' }] + }); }); - it('renders form elements correctly', () => { + it('renders correctly after loading specialties', async () => { render( ); - expect(screen.getByLabelText(/Especialidad/i)).toBeTruthy(); - expect(screen.getByLabelText(/Anverso/i)).toBeTruthy(); - expect(screen.getByLabelText(/Reverso/i)).toBeTruthy(); + await waitFor(() => { + expect(screen.getByText('Crear Flashcard')).toBeTruthy(); + expect(screen.getByText('Pediatría')).toBeTruthy(); + }); }); it('handles form submission', async () => { + FlashcardService.createFlashcard.mockResolvedValue({ data: { success: true } }); + render( ); - fireEvent.change(screen.getByLabelText(/Especialidad/i), { target: { value: 'pediatria' } }); - fireEvent.change(screen.getByLabelText(/Anverso/i), { target: { value: 'Pregunta' } }); - fireEvent.change(screen.getByLabelText(/Reverso/i), { target: { value: 'Respuesta' } }); + await waitFor(() => screen.getByText('Pediatría')); - fireEvent.click(screen.getByText('Guardar Flashcard')); + fireEvent.change(screen.getByLabelText(/Especialidad/i), { target: { value: '1' } }); + fireEvent.change(screen.getByPlaceholderText(/Escribe la pregunta/i), { target: { value: 'Pregunta de prueba' } }); + fireEvent.change(screen.getByPlaceholderText(/Escribe la respuesta/i), { target: { value: 'Respuesta de prueba' } }); - expect(screen.getByText('Guardando...')).toBeTruthy(); + fireEvent.click(screen.getByText('Guardar Flashcard')); await waitFor(() => { - expect(mockPush).toHaveBeenCalledWith('/v2/repaso'); - }, { timeout: 2000 }); + expect(FlashcardService.createFlashcard).toHaveBeenCalledWith({ + front: 'Pregunta de prueba', + back: 'Respuesta de prueba', + specialty_id: '1' + }); + expect(mockPush).toHaveBeenCalledWith('/v2/flashcards/repaso'); + }); }); });