# Frontend Integration Example - Achievement Toggle **Endpoint:** `PATCH /api/v1/gamification/achievements/:id` **Componente:** `AdminGamificationPage` (Tab de Logros) **Fecha:** 2025-11-25 --- ## 馃幆 Objetivo Integrar el endpoint de toggle de achievements en el componente `AdminGamificationPage` para persistir el cambio de estado activo/inactivo. --- ## 馃摝 API Service Implementation ### Opci贸n 1: Usando `apiClient` existente ```typescript // apps/frontend/src/services/api/gamificationAPI.ts import { apiClient } from './apiClient'; import { API_ROUTES } from '@shared/constants'; /** * Actualiza el estado activo/inactivo de un achievement * @param achievementId - UUID del achievement * @param isActive - Nuevo estado (true: activo, false: inactivo) * @returns Achievement actualizado */ export const updateAchievementStatus = async ( achievementId: string, isActive: boolean ): Promise<{ success: boolean; achievement: Achievement; }> => { const response = await apiClient.patch( `${API_ROUTES.GAMIFICATION.BASE}/achievements/${achievementId}`, { is_active: isActive } ); return response.data; }; ``` ### Opci贸n 2: Usando `fetch` directo ```typescript // apps/frontend/src/features/admin/api/adminAPI.ts const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1'; export const toggleAchievementStatus = async ( achievementId: string, isActive: boolean, token: string ): Promise<{ success: boolean; achievement: Achievement; }> => { const response = await fetch( `${API_BASE_URL}/gamification/achievements/${achievementId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ is_active: isActive }), } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); }; ``` --- ## 馃帹 Component Integration ### AdminGamificationPage - Tab de Logros ```typescript // apps/frontend/src/apps/admin/pages/AdminGamificationPage.tsx import React, { useState } from 'react'; import { Switch } from '@/components/ui/switch'; import { useToast } from '@/components/ui/use-toast'; import { updateAchievementStatus } from '@/services/api/gamificationAPI'; import { useAuth } from '@/app/providers/AuthContext'; interface Achievement { id: string; name: string; description: string; is_active: boolean; // ... otros campos } const AchievementsTab: React.FC = () => { const [achievements, setAchievements] = useState([]); const [loading, setLoading] = useState>({}); const { toast } = useToast(); const { token } = useAuth(); /** * Maneja el toggle del estado activo/inactivo de un achievement */ const handleToggleAchievement = async ( achievementId: string, currentStatus: boolean ) => { const newStatus = !currentStatus; // Optimistic update setAchievements(prev => prev.map(ach => ach.id === achievementId ? { ...ach, is_active: newStatus } : ach ) ); // Set loading state setLoading(prev => ({ ...prev, [achievementId]: true })); try { const result = await updateAchievementStatus(achievementId, newStatus); if (result.success) { // Update with server response setAchievements(prev => prev.map(ach => ach.id === achievementId ? result.achievement : ach ) ); toast({ title: '脡xito', description: `Achievement ${newStatus ? 'activado' : 'desactivado'} correctamente`, variant: 'success', }); } } catch (error) { // Rollback on error setAchievements(prev => prev.map(ach => ach.id === achievementId ? { ...ach, is_active: currentStatus } : ach ) ); toast({ title: 'Error', description: 'No se pudo actualizar el achievement. Intenta nuevamente.', variant: 'destructive', }); console.error('Error updating achievement:', error); } finally { setLoading(prev => ({ ...prev, [achievementId]: false })); } }; return (
{achievements.map(achievement => (

{achievement.name}

{achievement.description}

{achievement.is_active ? 'Activo' : 'Inactivo'} handleToggleAchievement(achievement.id, achievement.is_active) } disabled={loading[achievement.id]} />
))}
); }; export default AchievementsTab; ``` --- ## 馃獫 Custom Hook Approach ### useAchievements Hook ```typescript // apps/frontend/src/apps/admin/hooks/useAchievements.ts import { useState, useCallback } from 'react'; import { updateAchievementStatus } from '@/services/api/gamificationAPI'; import { useToast } from '@/components/ui/use-toast'; interface Achievement { id: string; name: string; description: string; is_active: boolean; } export const useAchievements = () => { const [achievements, setAchievements] = useState([]); const [loading, setLoading] = useState>({}); const { toast } = useToast(); /** * Toggle achievement status */ const toggleAchievement = useCallback( async (achievementId: string, currentStatus: boolean) => { const newStatus = !currentStatus; // Optimistic update setAchievements(prev => prev.map(ach => ach.id === achievementId ? { ...ach, is_active: newStatus } : ach ) ); setLoading(prev => ({ ...prev, [achievementId]: true })); try { const result = await updateAchievementStatus(achievementId, newStatus); if (result.success) { setAchievements(prev => prev.map(ach => ach.id === achievementId ? result.achievement : ach ) ); toast({ title: '脡xito', description: `Achievement ${newStatus ? 'activado' : 'desactivado'}`, variant: 'success', }); return result.achievement; } } catch (error) { // Rollback setAchievements(prev => prev.map(ach => ach.id === achievementId ? { ...ach, is_active: currentStatus } : ach ) ); toast({ title: 'Error', description: 'No se pudo actualizar el achievement', variant: 'destructive', }); throw error; } finally { setLoading(prev => ({ ...prev, [achievementId]: false })); } }, [toast] ); return { achievements, setAchievements, loading, toggleAchievement, }; }; ``` ### Uso del Hook ```typescript // apps/frontend/src/apps/admin/pages/AdminGamificationPage.tsx import { useAchievements } from '@/apps/admin/hooks/useAchievements'; const AchievementsTab: React.FC = () => { const { achievements, loading, toggleAchievement } = useAchievements(); return (
{achievements.map(achievement => ( toggleAchievement(achievement.id, achievement.is_active)} /> ))}
); }; ``` --- ## 馃З Reusable Component ### AchievementToggleSwitch Component ```typescript // apps/frontend/src/apps/admin/components/achievements/AchievementToggleSwitch.tsx import React from 'react'; import { Switch } from '@/components/ui/switch'; import { Loader2 } from 'lucide-react'; interface AchievementToggleSwitchProps { isActive: boolean; loading?: boolean; onToggle: (newStatus: boolean) => void; disabled?: boolean; } export const AchievementToggleSwitch: React.FC = ({ isActive, loading = false, onToggle, disabled = false, }) => { return (
{loading && } {isActive ? 'Activo' : 'Inactivo'}
); }; ``` ### Uso del Componente ```typescript handleToggleAchievement(achievement.id, achievement.is_active)} /> ``` --- ## 馃攧 Zustand Store Integration (Opcional) ### Achievement Store ```typescript // apps/frontend/src/stores/achievementStore.ts import { create } from 'zustand'; import { updateAchievementStatus } from '@/services/api/gamificationAPI'; interface Achievement { id: string; name: string; is_active: boolean; } interface AchievementStore { achievements: Achievement[]; loading: Record; setAchievements: (achievements: Achievement[]) => void; toggleAchievement: (id: string, currentStatus: boolean) => Promise; } export const useAchievementStore = create((set, get) => ({ achievements: [], loading: {}, setAchievements: (achievements) => set({ achievements }), toggleAchievement: async (id, currentStatus) => { const newStatus = !currentStatus; // Optimistic update set((state) => ({ achievements: state.achievements.map((ach) => ach.id === id ? { ...ach, is_active: newStatus } : ach ), loading: { ...state.loading, [id]: true }, })); try { const result = await updateAchievementStatus(id, newStatus); if (result.success) { set((state) => ({ achievements: state.achievements.map((ach) => ach.id === id ? result.achievement : ach ), loading: { ...state.loading, [id]: false }, })); } } catch (error) { // Rollback set((state) => ({ achievements: state.achievements.map((ach) => ach.id === id ? { ...ach, is_active: currentStatus } : ach ), loading: { ...state.loading, [id]: false }, })); throw error; } }, })); ``` --- ## 馃И Testing Example ### Component Test ```typescript // apps/frontend/src/apps/admin/components/__tests__/AchievementsTab.test.tsx import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { AchievementsTab } from '../AchievementsTab'; import { updateAchievementStatus } from '@/services/api/gamificationAPI'; jest.mock('@/services/api/gamificationAPI'); describe('AchievementsTab', () => { it('should toggle achievement status', async () => { const mockAchievement = { id: '123', name: 'Test Achievement', description: 'Test', is_active: true, }; (updateAchievementStatus as jest.Mock).mockResolvedValue({ success: true, achievement: { ...mockAchievement, is_active: false }, }); render(); const toggle = screen.getByRole('switch'); fireEvent.click(toggle); await waitFor(() => { expect(updateAchievementStatus).toHaveBeenCalledWith('123', false); }); expect(screen.getByText('Inactivo')).toBeInTheDocument(); }); }); ``` --- ## 馃摑 TypeScript Types ```typescript // apps/frontend/src/shared/types/gamification.types.ts export interface Achievement { id: string; tenant_id?: string; name: string; description?: string; icon: string; category: AchievementCategoryEnum; rarity: string; difficulty_level: DifficultyLevelEnum; conditions: Record; rewards: Record; ml_coins_reward: number; is_secret: boolean; is_active: boolean; is_repeatable: boolean; order_index: number; points_value: number; unlock_message?: string; instructions?: string; tips?: string[]; metadata: Record; created_by?: string; created_at: Date; updated_at: Date; } export interface UpdateAchievementStatusResponse { success: boolean; achievement: Achievement; } ``` --- ## 馃幆 Best Practices Implementadas 1. **Optimistic Updates:** UI responde inmediatamente antes de la respuesta del servidor 2. **Error Handling:** Rollback en caso de error 3. **Loading States:** Indicadores visuales durante la operaci贸n 4. **Toast Notifications:** Feedback visual al usuario 5. **Type Safety:** TypeScript en toda la implementaci贸n 6. **Reusability:** Componentes y hooks reutilizables 7. **Testing:** Ejemplos de tests unitarios --- ## 馃殌 Deployment Checklist - [ ] Agregar el m茅todo `updateAchievementStatus` en `gamificationAPI.ts` - [ ] Crear el hook `useAchievements` (opcional) - [ ] Actualizar `AdminGamificationPage` con el toggle handler - [ ] Agregar tipos TypeScript en `gamification.types.ts` - [ ] Probar en desarrollo - [ ] Agregar tests unitarios - [ ] Documentar en Storybook (opcional) - [ ] Code review - [ ] Deploy a staging - [ ] QA testing - [ ] Deploy a production --- **Desarrollado por:** Claude Code **Fecha:** 2025-11-25 **Versi贸n:** 1.0