# ET-FRONTEND-MGN-004-002: Gestión de Journals Contables **RF Asociado:** [RF-MGN-004-002](../../requerimientos-funcionales/mgn-004/RF-MGN-004-002-gestión-de-journals-contables.md) **ET Backend:** [ET-BACKEND-MGN-004-002](../backend/mgn-004/ET-BACKEND-MGN-004-002-gestión-de-journals-contables.md) **Módulo:** MGN-004 **Complejidad:** Baja **Story Points:** 2 SP (Frontend) **Estado:** Diseñado **Fecha:** 2025-11-24 ## Resumen Técnico Implementación frontend para gestión de journals contables. Incluye componentes React con TypeScript, formularios con validación, integración con API backend, y arquitectura Feature-Sliced Design (FSD). ## Stack Tecnológico - **Framework:** React 18.x + TypeScript 5.x - **Build Tool:** Vite 5.x - **UI Library:** Ant Design 5.x (AntD) - **State Management:** Zustand + React Query (TanStack Query) - **Routing:** React Router 6.x - **Forms:** React Hook Form + Zod validation - **HTTP Client:** Axios (con interceptors para auth) - **Testing:** Vitest + React Testing Library + Playwright ## Arquitectura Frontend (FSD) Estructura basada en Feature-Sliced Design: ``` src/ ├── app/ # App-level config │ ├── providers/ │ │ └── router.tsx │ └── styles/ │ └── theme.ts ├── pages/ # Route pages │ └── GestiónJournalsContablesPage/ │ ├── index.tsx │ └── GestiónJournalsContablesPage.tsx ├── widgets/ # Complex UI blocks │ └── GestiónJournalsContablesTable/ │ ├── ui/ │ │ └── GestiónJournalsContablesTable.tsx │ └── index.ts ├── features/ # User interactions │ ├── createGestiónJournalsContables/ │ │ ├── ui/ │ │ │ └── CreateGestiónJournalsContablesForm.tsx │ │ ├── model/ │ │ │ └── useGestiónJournalsContablesActions.ts │ │ └── index.ts │ ├── updateGestiónJournalsContables/ │ └── deleteGestiónJournalsContables/ ├── entities/ # Business entities │ └── gestiónJournalsContables/ │ ├── model/ │ │ ├── types.ts │ │ ├── schemas.ts │ │ └── gestiónJournalsContables.store.ts │ ├── api/ │ │ ├── gestiónJournalsContables.api.ts │ │ └── gestiónJournalsContables.queries.ts │ ├── ui/ │ │ └── GestiónJournalsContablesCard.tsx │ └── index.ts └── shared/ # Shared code ├── ui/ # UI kit │ ├── Button/ │ ├── Modal/ │ └── Table/ ├── api/ │ └── client.ts └── lib/ └── utils.ts ``` ## Rutas ```typescript // src/app/routes/mgn-004.routes.tsx export const GestiónJournalsContablesRoutes = { list: '/mgn-004/gestiónJournalsContables', create: '/mgn-004/gestiónJournalsContables/create', edit: '/mgn-004/gestiónJournalsContables/:id/edit', view: '/mgn-004/gestiónJournalsContables/:id', }; // Integración en Router } /> } /> } /> } /> ``` ## Types / Interfaces ```typescript // src/entities/gestiónJournalsContables/model/types.ts export interface GestiónJournalsContables { id: string; tenantId: string; name: string; code?: string; createdAt: string; updatedAt?: string; deletedAt?: string; } export interface CreateGestiónJournalsContablesDto { name: string; code?: string; } export type UpdateGestiónJournalsContablesDto = Partial; export interface GestiónJournalsContablesFilters { search?: string; page?: number; limit?: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; } export interface GestiónJournalsContablesListResponse { data: GestiónJournalsContables[]; meta: { page: number; limit: number; total: number; totalPages: number; }; } ``` ## Schemas de Validación (Zod) ```typescript // src/entities/gestiónJournalsContables/model/schemas.ts import { z } from 'zod'; export const createGestiónJournalsContablesSchema = z.object({ name: z.string() .min(3, 'El nombre debe tener al menos 3 caracteres') .max(255, 'El nombre no puede exceder 255 caracteres'), code: z.string() .min(2, 'El código debe tener al menos 2 caracteres') .max(50, 'El código no puede exceder 50 caracteres') .optional(), }); export const updateGestiónJournalsContablesSchema = createGestiónJournalsContablesSchema.partial(); export type CreateGestiónJournalsContablesFormData = z.infer; export type UpdateGestiónJournalsContablesFormData = z.infer; // Validación personalizada (ejemplo) export const validateGestiónJournalsContablesCode = (code: string): boolean => { return /^[A-Z0-9-]+$/.test(code); }; ``` ## API Client ```typescript // src/entities/gestiónJournalsContables/api/gestiónJournalsContables.api.ts import { apiClient } from '@shared/api/client'; import type { GestiónJournalsContables, CreateGestiónJournalsContablesDto, UpdateGestiónJournalsContablesDto, GestiónJournalsContablesFilters, GestiónJournalsContablesListResponse } from '../model/types'; const BASE_URL = '/api/v1/journals'; export const gestiónJournalsContablesApi = { getAll: async (filters?: GestiónJournalsContablesFilters): Promise => { const { data } = await apiClient.get(BASE_URL, { params: filters, }); return data; }, getById: async (id: string): Promise => { const { data } = await apiClient.get<{ data: GestiónJournalsContables }>(`${BASE_URL}/${id}`); return data.data; }, create: async (dto: CreateGestiónJournalsContablesDto): Promise => { const { data } = await apiClient.post<{ data: GestiónJournalsContables }>(BASE_URL, dto); return data.data; }, update: async (id: string, dto: UpdateGestiónJournalsContablesDto): Promise => { const { data } = await apiClient.put<{ data: GestiónJournalsContables }>(`${BASE_URL}/${id}`, dto); return data.data; }, delete: async (id: string): Promise => { await apiClient.delete(`${BASE_URL}/${id}`); }, }; // Configuración de Axios client // src/shared/api/client.ts import axios from 'axios'; import { message } from 'antd'; export const apiClient = axios.create({ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000', timeout: 30000, }); // Request interceptor: agregar auth token apiClient.interceptors.request.use( (config) => { const token = localStorage.getItem('accessToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // Response interceptor: manejar errores globales apiClient.interceptors.response.use( (response) => response, (error) => { const { response } = error; if (response?.status === 401) { // Token expirado: redirigir a login localStorage.removeItem('accessToken'); window.location.href = '/login'; } else if (response?.status === 403) { message.error('No tienes permisos para realizar esta acción'); } else if (response?.status === 500) { message.error('Error interno del servidor'); } return Promise.reject(error); } ); ``` ## State Management (Zustand + React Query) ### Zustand Store (estado local UI) ```typescript // src/entities/gestiónJournalsContables/model/gestiónJournalsContables.store.ts import { create } from 'zustand'; import { GestiónJournalsContables } from './types'; interface GestiónJournalsContablesStore { selectedGestiónJournalsContables: GestiónJournalsContables | null; isModalOpen: boolean; modalMode: 'create' | 'edit' | 'view' | null; setSelectedGestiónJournalsContables: (entity: GestiónJournalsContables | null) => void; openModal: (mode: 'create' | 'edit' | 'view', entity?: GestiónJournalsContables) => void; closeModal: () => void; } export const useGestiónJournalsContablesStore = create((set) => ({ selectedGestiónJournalsContables: null, isModalOpen: false, modalMode: null, setSelectedGestiónJournalsContables: (entity) => set({ selectedGestiónJournalsContables: entity }), openModal: (mode, entity) => set({ isModalOpen: true, modalMode: mode, selectedGestiónJournalsContables: entity || null, }), closeModal: () => set({ isModalOpen: false, modalMode: null, selectedGestiónJournalsContables: null, }), })); ``` ### React Query Hooks (servidor state) ```typescript // src/entities/gestiónJournalsContables/api/gestiónJournalsContables.queries.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { message } from 'antd'; import { gestiónJournalsContablesApi } from './gestiónJournalsContables.api'; import type { CreateGestiónJournalsContablesDto, UpdateGestiónJournalsContablesDto, GestiónJournalsContablesFilters } from '../model/types'; const QUERY_KEY = 'gestiónJournalsContables'; // Query: obtener lista export const useGestiónJournalsContabless = (filters?: GestiónJournalsContablesFilters) => { return useQuery({ queryKey: [QUERY_KEY, filters], queryFn: () => gestiónJournalsContablesApi.getAll(filters), staleTime: 5 * 60 * 1000, // 5 minutos }); }; // Query: obtener por ID export const useGestiónJournalsContables = (id: string) => { return useQuery({ queryKey: [QUERY_KEY, id], queryFn: () => gestiónJournalsContablesApi.getById(id), enabled: !!id, }); }; // Mutation: crear export const useCreateGestiónJournalsContables = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: (dto: CreateGestiónJournalsContablesDto) => gestiónJournalsContablesApi.create(dto), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY] }); message.success('GestiónJournalsContables creado exitosamente'); }, onError: (error: any) => { const errorMsg = error.response?.data?.message || 'Error al crear gestiónjournalscontables'; message.error(errorMsg); }, }); }; // Mutation: actualizar export const useUpdateGestiónJournalsContables = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ id, dto }: { id: string; dto: UpdateGestiónJournalsContablesDto }) => gestiónJournalsContablesApi.update(id, dto), onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY] }); queryClient.setQueryData([QUERY_KEY, data.id], data); message.success('GestiónJournalsContables actualizado exitosamente'); }, onError: (error: any) => { const errorMsg = error.response?.data?.message || 'Error al actualizar gestiónjournalscontables'; message.error(errorMsg); }, }); }; // Mutation: eliminar export const useDeleteGestiónJournalsContables = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: (id: string) => gestiónJournalsContablesApi.delete(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY] }); message.success('GestiónJournalsContables eliminado exitosamente'); }, onError: (error: any) => { const errorMsg = error.response?.data?.message || 'Error al eliminar gestiónjournalscontables'; message.error(errorMsg); }, }); }; ``` ## Components UI ### Tabla de Listado ```typescript // src/widgets/GestiónJournalsContablesTable/ui/GestiónJournalsContablesTable.tsx import React, { useState } from 'react'; import { Table, Button, Space, Input, Modal } from 'antd'; import { EditOutlined, DeleteOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { useGestiónJournalsContabless, useDeleteGestiónJournalsContables } from '@entities/gestiónJournalsContables'; import { useGestiónJournalsContablesStore } from '@entities/gestiónJournalsContables'; import type { GestiónJournalsContables, GestiónJournalsContablesFilters } from '@entities/gestiónJournalsContables'; const { Search } = Input; export const GestiónJournalsContablesTable: React.FC = () => { const [filters, setFilters] = useState({ page: 1, limit: 20, sortBy: 'createdAt', sortOrder: 'desc', }); const { data, isLoading } = useGestiónJournalsContabless(filters); const deleteMutation = useDeleteGestiónJournalsContables(); const { openModal } = useGestiónJournalsContablesStore(); const handleSearch = (value: string) => { setFilters((prev) => ({ ...prev, search: value, page: 1 })); }; const handleTableChange = (pagination: any, filters: any, sorter: any) => { setFilters((prev) => ({ ...prev, page: pagination.current, limit: pagination.pageSize, sortBy: sorter.field || 'createdAt', sortOrder: sorter.order === 'ascend' ? 'asc' : 'desc', })); }; const handleDelete = (id: string, name: string) => { Modal.confirm({ title: '¿Confirmar eliminación?', content: `¿Está seguro de eliminar "${name}"? Esta acción no se puede deshacer.`, okText: 'Eliminar', okType: 'danger', cancelText: 'Cancelar', onOk: () => deleteMutation.mutate(id), }); }; const columns: ColumnsType = [ { title: 'Nombre', dataIndex: 'name', key: 'name', sorter: true, width: '40%', }, { title: 'Código', dataIndex: 'code', key: 'code', width: '20%', }, { title: 'Fecha Creación', dataIndex: 'createdAt', key: 'createdAt', sorter: true, width: '20%', render: (date: string) => new Date(date).toLocaleDateString('es-ES'), }, { title: 'Acciones', key: 'actions', width: '20%', render: (_, record) => ( `Total: ${total} registros`, }} onChange={handleTableChange} /> ); }; ``` ### Formulario de Creación/Edición ```typescript // src/features/createGestiónJournalsContables/ui/CreateGestiónJournalsContablesForm.tsx import React, { useEffect } from 'react'; import { Form, Input, Button, Space } from 'antd'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { createGestiónJournalsContablesSchema, updateGestiónJournalsContablesSchema, type CreateGestiónJournalsContablesFormData } from '@entities/gestiónJournalsContables'; import { useCreateGestiónJournalsContables, useUpdateGestiónJournalsContables } from '@entities/gestiónJournalsContables'; import type { GestiónJournalsContables } from '@entities/gestiónJournalsContables'; interface GestiónJournalsContablesFormProps { mode: 'create' | 'edit'; initialData?: GestiónJournalsContables; onSuccess?: () => void; } export const GestiónJournalsContablesForm: React.FC = ({ mode, initialData, onSuccess, }) => { const isEditMode = mode === 'edit'; const schema = isEditMode ? updateGestiónJournalsContablesSchema : createGestiónJournalsContablesSchema; const { control, handleSubmit, formState: { errors }, reset } = useForm({ resolver: zodResolver(schema), defaultValues: initialData || { name: '', code: '', }, }); const createMutation = useCreateGestiónJournalsContables(); const updateMutation = useUpdateGestiónJournalsContables(); const mutation = isEditMode ? updateMutation : createMutation; useEffect(() => { if (initialData) { reset(initialData); } }, [initialData, reset]); const onSubmit = (data: CreateGestiónJournalsContablesFormData) => { if (isEditMode && initialData) { updateMutation.mutate( { id: initialData.id, dto: data }, { onSuccess: () => onSuccess?.() } ); } else { createMutation.mutate(data, { onSuccess: () => { reset(); onSuccess?.(); }, }); } }; return (
( )} /> ( )} /> ); }; ``` ### Modal Wrapper ```typescript // src/features/createGestiónJournalsContables/ui/CreateGestiónJournalsContablesModal.tsx import React from 'react'; import { Modal } from 'antd'; import { useGestiónJournalsContablesStore } from '@entities/gestiónJournalsContables'; import { GestiónJournalsContablesForm } from './CreateGestiónJournalsContablesForm'; export const GestiónJournalsContablesModal: React.FC = () => { const { isModalOpen, modalMode, selectedGestiónJournalsContables, closeModal } = useGestiónJournalsContablesStore(); const title = { create: 'Crear GestiónJournalsContables', edit: 'Editar GestiónJournalsContables', view: 'Ver GestiónJournalsContables', }[modalMode || 'create']; return ( {modalMode !== 'view' ? ( ) : (

Nombre: {selectedGestiónJournalsContables?.name}

Código: {selectedGestiónJournalsContables?.code || 'N/A'}

Creado: {new Date(selectedGestiónJournalsContables?.createdAt || '').toLocaleString('es-ES')}

)}
); }; ``` ### Página Principal ```typescript // src/pages/GestiónJournalsContablesPage/GestiónJournalsContablesPage.tsx import React from 'react'; import { Card } from 'antd'; import { GestiónJournalsContablesTable } from '@widgets/GestiónJournalsContablesTable'; import { GestiónJournalsContablesModal } from '@features/createGestiónJournalsContables'; export const GestiónJournalsContablesPage: React.FC = () => { return (
); }; ``` ## Validaciones del Cliente ### Validación en Tiempo Real ```typescript // Las validaciones Zod se ejecutan automáticamente con React Hook Form // Validación custom adicional (ejemplo) const validateUniqueGestiónJournalsContablesCode = async (code: string): Promise => { try { const { data } = await gestiónJournalsContablesApi.getAll({ search: code }); return data.data.length === 0; } catch { return false; } }; // Uso en formulario { if (value && !(await validateUniqueGestiónJournalsContablesCode(value))) { return 'Este código ya existe'; } return true; }, }} render={...} /> ``` ### Mensajes de Error - Español claro y conciso - Sugerencias de corrección cuando sea posible - Highlight visual de campos con error (Ant Design `validateStatus`) ## UX/UI ### Diseño Visual - **Layout:** Ant Design Pro Layout (Header + Sidebar + Content) - **Colores:** - Primary: `#1890ff` (Ant Design default) - Success: `#52c41a` - Warning: `#faad14` - Error: `#f5222d` - **Tipografía:** - Headings: Inter font family - Body: Roboto font family - **Iconos:** Ant Design Icons ### Feedback al Usuario - **Loading States:** Spinners en botones (`Button loading={true}`) y tablas (`Table loading={true}`) - **Success Messages:** `message.success('Operación exitosa')` - **Error Messages:** `message.error('Error: detalles...')` - **Confirmations:** `Modal.confirm()` para acciones destructivas (delete) - **Progress:** `Progress` para operaciones largas ### Responsiveness - **Desktop (1200px+):** Tabla completa con todas las columnas - **Tablet (768-1199px):** Tabla adaptada, algunas columnas ocultas - **Mobile (<768px):** Reemplazar tabla por cards (`` de Ant Design) ```typescript // Ejemplo responsive import { useMediaQuery } from '@shared/hooks/useMediaQuery'; const isMobile = useMediaQuery('(max-width: 768px)'); return isMobile ? ( ( {/* Card layout */} )} /> ) : (
); ``` ## Permisos (RBAC en UI) ```typescript // src/shared/hooks/usePermissions.ts import { useAuth } from '@modules/auth'; export const usePermissions = () => { const { user } = useAuth(); const can = (permission: string): boolean => { return user?.permissions?.includes(permission) ?? false; }; const hasRole = (role: string): boolean => { return user?.roles?.includes(role) ?? false; }; return { can, hasRole }; }; // Uso en componentes import { usePermissions } from '@shared/hooks/usePermissions'; const { can } = usePermissions(); {can('mgn-004.gestiónJournalsContables.create') && ( )} {can('mgn-004.gestiónJournalsContables.delete') && ( )} ``` ## Testing ### Component Tests (Vitest + React Testing Library) ```typescript // src/widgets/GestiónJournalsContablesTable/ui/GestiónJournalsContablesTable.test.tsx import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { GestiónJournalsContablesTable } from './GestiónJournalsContablesTable'; import { gestiónJournalsContablesApi } from '@entities/gestiónJournalsContables'; // Mock API vi.mock('@entities/gestiónJournalsContables', () => ({ gestiónJournalsContablesApi: { getAll: vi.fn(), delete: vi.fn(), }, })); describe('GestiónJournalsContablesTable', () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); const wrapper = ({ children }) => ( {children} ); beforeEach(() => { vi.clearAllMocks(); }); it('should render table with data', async () => { const mockData = { data: [ { id: '1', name: 'Test GestiónJournalsContables 1', code: 'TEST1', createdAt: '2025-11-24' }, { id: '2', name: 'Test GestiónJournalsContables 2', code: 'TEST2', createdAt: '2025-11-24' }, ], meta: { page: 1, limit: 20, total: 2, totalPages: 1 }, }; vi.mocked(gestiónJournalsContablesApi.getAll).mockResolvedValue(mockData); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('Test GestiónJournalsContables 1')).toBeInTheDocument(); expect(screen.getByText('Test GestiónJournalsContables 2')).toBeInTheDocument(); }); }); it('should call delete mutation on delete button click', async () => { const user = userEvent.setup(); const mockData = { data: [{ id: '1', name: 'To Delete', code: 'DEL', createdAt: '2025-11-24' }], meta: { page: 1, limit: 20, total: 1, totalPages: 1 }, }; vi.mocked(gestiónJournalsContablesApi.getAll).mockResolvedValue(mockData); vi.mocked(gestiónJournalsContablesApi.delete).mockResolvedValue(); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('To Delete')).toBeInTheDocument(); }); const deleteBtn = screen.getByTitle('Eliminar'); await user.click(deleteBtn); // Confirmar modal const confirmBtn = screen.getByText('Eliminar'); await user.click(confirmBtn); await waitFor(() => { expect(gestiónJournalsContablesApi.delete).toHaveBeenCalledWith('1'); }); }); it('should filter data on search', async () => { const user = userEvent.setup(); vi.mocked(gestiónJournalsContablesApi.getAll).mockResolvedValue({ data: [], meta: { page: 1, limit: 20, total: 0, totalPages: 0 }, }); render(, { wrapper }); const searchInput = screen.getByPlaceholderText('Buscar por nombre o código'); await user.type(searchInput, 'test'); await user.keyboard('{Enter}'); await waitFor(() => { expect(gestiónJournalsContablesApi.getAll).toHaveBeenCalledWith( expect.objectContaining({ search: 'test' }) ); }); }); }); ``` ### E2E Tests (Playwright) ```typescript // e2e/gestiónJournalsContables.spec.ts import { test, expect } from '@playwright/test'; test.describe('GestiónJournalsContables Management', () => { test.beforeEach(async ({ page }) => { // Login await page.goto('/login'); await page.fill('[name="email"]', 'test@example.com'); await page.fill('[name="password"]', 'Test1234!'); await page.click('button[type="submit"]'); await page.waitForURL('/mgn-004/gestiónJournalsContables'); }); test('should create new gestiónJournalsContables', async ({ page }) => { await page.goto('/mgn-004/gestiónJournalsContables'); // Click create button await page.click('text=Crear GestiónJournalsContables'); // Fill form await page.fill('[name="name"]', 'E2E Test GestiónJournalsContables'); await page.fill('[name="code"]', 'E2E001'); // Submit await page.click('button[type="submit"]'); // Verify success message await expect(page.locator('.ant-message-success')).toContainText('creado exitosamente'); // Verify in table await expect(page.locator('table')).toContainText('E2E Test GestiónJournalsContables'); }); test('should edit existing gestiónJournalsContables', async ({ page }) => { await page.goto('/mgn-004/gestiónJournalsContables'); // Click edit on first row await page.click('table tbody tr:first-child button[title="Editar"]'); // Update name await page.fill('[name="name"]', 'Updated Name'); // Submit await page.click('button[type="submit"]'); // Verify success await expect(page.locator('.ant-message-success')).toContainText('actualizado exitosamente'); }); test('should delete gestiónJournalsContables with confirmation', async ({ page }) => { await page.goto('/mgn-004/gestiónJournalsContables'); // Click delete on first row await page.click('table tbody tr:first-child button[title="Eliminar"]'); // Confirm deletion await page.click('.ant-modal-confirm button.ant-btn-dangerous'); // Verify success await expect(page.locator('.ant-message-success')).toContainText('eliminado exitosamente'); }); test('should validate required fields', async ({ page }) => { await page.goto('/mgn-004/gestiónJournalsContables'); // Click create button await page.click('text=Crear GestiónJournalsContables'); // Try to submit empty form await page.click('button[type="submit"]'); // Verify validation errors await expect(page.locator('.ant-form-item-explain-error')).toContainText('al menos 3 caracteres'); }); test('should search and filter gestiónJournalsContabless', async ({ page }) => { await page.goto('/mgn-004/gestiónJournalsContables'); // Search await page.fill('[placeholder="Buscar por nombre o código"]', 'test'); await page.keyboard.press('Enter'); // Verify API call (check network tab or wait for results) await page.waitForTimeout(500); // Results should be filtered const rows = page.locator('table tbody tr'); await expect(rows.first()).toBeVisible(); }); }); ``` ## Performance ### Optimizaciones React ```typescript // 1. Lazy loading de páginas const GestiónJournalsContablesPage = React.lazy(() => import('@pages/GestiónJournalsContablesPage')); // 2. Memo para componentes pesados export const GestiónJournalsContablesCard = React.memo(({ data }) => { // ... }); // 3. useMemo para cálculos pesados const filteredData = useMemo(() => { return data?.data.filter((item) => item.status === 'active'); }, [data]); // 4. useCallback para funciones pasadas como props const handleEdit = useCallback((id: string) => { openModal('edit', data.find((item) => item.id === id)); }, [data, openModal]); ``` ### Virtualización para Listas Largas ```typescript // Para tablas con >100 rows import { FixedSizeList } from 'react-window'; {({ index, style }) => (
{/* Row content */}
)}
``` ### Debounce en Búsqueda ```typescript import { useDebouncedCallback } from 'use-debounce'; const debouncedSearch = useDebouncedCallback( (value: string) => { setFilters((prev) => ({ ...prev, search: value, page: 1 })); }, 300 ); debouncedSearch(e.target.value)} /> ``` ### Bundle Size - **Code splitting:** Lazy loading de páginas (`React.lazy()`) - **Tree shaking:** Importar solo lo necesario de Ant Design - **Chunk optimization:** Vite automático - **Target bundle size:** <200 KB por chunk ## Referencias - [RF Asociado](../../requerimientos-funcionales/mgn-004/RF-MGN-004-002-gestión-de-journals-contables.md) - [ET Backend](../backend/mgn-004/ET-BACKEND-MGN-004-002-gestión-de-journals-contables.md) - [Gamilit Frontend Patterns](../../00-analisis-referencias/gamilit/frontend-patterns.md) - [ADR-009: Frontend Architecture](../../adr/ADR-009-frontend-architecture.md) ## Dependencias ### Módulos Frontend - `AuthModule` - Autenticación y autorización - `SharedModule` - Componentes y utilities compartidos ### RF Bloqueantes - RF-MGN-001-001 (Autenticación de Usuarios) - RF-MGN-004-002 Backend completado ## Notas de Implementación - [ ] Crear estructura FSD: `entities/gestiónJournalsContables`, `features/createGestiónJournalsContables`, `widgets/GestiónJournalsContablesTable` - [ ] Definir types e interfaces en `entities/gestiónJournalsContables/model/types.ts` - [ ] Crear schemas de validación Zod - [ ] Implementar API client con axios - [ ] Crear React Query hooks (queries + mutations) - [ ] Implementar Zustand store para UI state - [ ] Crear componentes UI (Table, Form, Modal) - [ ] Implementar rutas en React Router - [ ] Crear tests (componentes + e2e) - [ ] Validar responsiveness (desktop + tablet + mobile) - [ ] Validar con criterios de aceptación del RF - [ ] Code review por Tech Lead - [ ] QA testing ## Estimación - **Frontend Development:** 2 SP - **Testing (Unit + e2e):** 1 SP - **Code Review + QA:** 1 SP - **Total:** 5 SP --- **Documento generado:** 2025-11-24 **Versión:** 1.0 **Estado:** Diseñado **Próximo paso:** Implementación