Appearance
Arquitectura Frontend - Sistema Bautista
Esta guía describe la arquitectura del frontend del sistema Bautista, que está en proceso de migración de tecnología Legacy (PHP SSR) hacia React moderno.
⚠️ Arquitectura Híbrida en Coexistencia
El sistema mantiene tres tipos de arquitectura frontend funcionando simultáneamente:
1. Legacy PHP SSR (⚠️ Solo Mantenimiento)
Ver documentación completa: Arquitectura Legacy
- Views:
.phpenphp/components/mod-{modulo}/ - JavaScript: Vanilla JS en
js/view/{modulo}/ - Modales:
.html/.phpcon.jspropio, fetch dinámico - Comunicación: Fetch directo al backend API
- Proxy:
php/backend/DEPRECADO (usar fetch directo) - Estado: ⚠️ DEPRECADO - Solo mantenimiento
Ejemplo:
php/components/mod-venta/forms/tarjeta.php (View)
js/view/venta/tarjeta.js (Interactividad)2. React Montable (Migración Gradual)
Componentes React montados en contexto legacy PHP.
A. Full View React
- Archivo
.tsxcompleto - Montaje directo en
.phpshell - Ejemplo: Conceptos de retenciones de ganancia
B. Component Mount Only
- Componente embebido en view legacy
- API expuesta para interacción con
.jslegacy - Ejemplo: Campo carteras de cliente
3. React Full Module (Arquitectura Moderna)
Ver: Estructura de Módulos React
- App completa con React Router
- Sidebar propio configurable
- Contextos propios
- Ejemplo en producción: Módulo Membresías (
ts/mod-membresias/)
Decisión: ¿Qué Arquitectura Usar?
| Tipo Feature | Arquitectura | Referencia |
|---|---|---|
| Mantenimiento código existente | ⚠️ Legacy PHP SSR | legacy-architecture.md |
| Campo/modal pequeño | ✅ React Component Mount | Este doc - Component Mount Only |
| Vista completa | ✅ React Full View Mount | Este doc - Full View React |
| Módulo completo | ✅ React Full Module | estructura-modulos-react.md |
Estado Actual del Frontend
El sistema tiene una arquitectura híbrida con dos stacks coexistiendo:
1. Legacy Platform (PHP SSR)
Usada en módulos antiguos:
- Rendering: Server-Side Rendering (PHP)
- Templates: PHP templates
- JavaScript: Vanilla JS / jQuery
- Estilos: CSS personalizado
- Comunicación: Formularios tradicionales + AJAX
2. React SPA (Moderna)
En producción en módulos nuevos y migrados:
- Framework: React
- Módulos migrados: Membresía (ver rama
feature/modulo-membresia) - Comunicación: API REST JSON
- Autenticación: JWT via headers
Ejemplo de módulo migrado: El módulo de Membresía está completamente implementado en React con:
- Gestión de estado moderno
- Componentes reutilizables
- Integración con API REST
- UI moderna y responsive
Stack Tecnológico React (bautista-app)
Tecnologías Principales
Core:
- React: 19+ - Framework UI
- TypeScript: 5.8+ - Type safety con modo strict
- Vite: 7+ - Build tool y dev server
UI Framework:
- Material UI (MUI): 7+ - Componentes UI
- MUI X Data Grid: Para tablas complejas con server-side rendering
- MUI Icons: Iconos prediseñados
State & Data:
- TanStack Query (React Query): 6+ - Server state management y caching
- Axios: HTTP client con interceptores
- Zustand: Client state management (opcional para estado UI local)
Forms & Validation:
- React Hook Form: 7+ - Form management con validación integrada
- Zod: 3+ - Schema validation runtime
Routing:
- React Router DOM: 7+ - Client-side routing
Estructura del Proyecto React
bautista-app/
├── public/ # Archivos estáticos
├── ts/ # Código TypeScript
│ ├── api/ # Configuración API
│ │ ├── api.ts # Instancia Axios configurada
│ │ └── endpoints/ # Endpoints por módulo
│ ├── core/ # Código compartido
│ │ ├── components/ # Componentes reutilizables
│ │ ├── hooks/ # Custom hooks
│ │ ├── types/ # Tipos TypeScript
│ │ └── utils/ # Utilidades
│ ├── config/ # Configuración
│ │ └── theme.ts # Tema Material UI
│ └── modules/ # Módulos por dominio
│ ├── membresia/
│ │ ├── components/ # Componentes del módulo
│ │ ├── hooks/ # Hooks del módulo
│ │ ├── schemas/ # Schemas Zod
│ │ ├── services/ # Servicios API
│ │ └── types/ # Tipos del módulo
│ ├── auth/
│ ├── ventas/
│ └── ...
├── tsconfig.json # Configuración TypeScript
├── vite.config.ts # Configuración Vite
└── package.jsonConfiguración TypeScript
El proyecto utiliza TypeScript en modo strict con las siguientes características:
- Target ES2022 con soporte para módulos modernos
- Path aliases configurados (
@/apunta a./ts/) - React JSX con transformación moderna
- Validaciones estrictas habilitadas (noUnusedLocals, noUnusedParameters, etc.)
- Resolución de módulos tipo bundler para compatibilidad con Vite
Configuración de API y Autenticación
Cliente HTTP (Axios)
El sistema utiliza una instancia centralizada de Axios configurada en:
- Ubicación:
ts/api/api.ts - Configuración base:
ts/config/app.ts(URL del API hardcodeada, no variables de entorno) - Timeout: 10 segundos
- withCredentials: Habilitado para enviar cookies en requests
Sistema de Autenticación
Autenticación basada en cookies:
- Token de acceso almacenado en cookie
ACCESS_TOKEN - Refresh token también manejado vía cookies
- NO se usa localStorage para tokens
Interceptores de Request:
- Lee el token de la cookie
ACCESS_TOKEN - Agrega header
Authorization: Bearer {token}automáticamente - Agrega header
X-Requested-With: Axios
Interceptores de Response:
- 401 (Unauthorized):
- Detecta
INVALID_ACCESS_TOKENe intenta refresh automático - Si falla el refresh o es
INVALID_REFRESH_TOKEN, redirige a logout - Deshabilita toda la UI antes de redirigir (botones, forms, links)
- Detecta
- 422 (Validation Error): Lanza
ValidationErrorcon mensaje del backend - 400 (Bad Request): Lanza
BadRequestError - 409 (Conflict): Lanza
ConflictError
Manejo de errores tipados: Todos los errores tienen propiedades name y message para manejo específico en componentes.
Patrón de Módulos
Cada módulo sigue una estructura consistente demostrada en el módulo de membresías en producción:
ts/mod-membresias/
├── components/ # Componentes React del módulo
├── config/ # Configuración (routes, queryKeys)
├── hooks/ # React Query hooks
├── mappers/ # Transformación Backend ↔ Frontend
├── schemas/ # Validación con Zod
├── services/ # Llamadas API
├── types/ # TypeScript types
└── views/ # Páginas/Vistas principales1. Schemas (Zod) - Validación Frontend
Ejemplo real: ts/mod-membresias/schemas/miembro.schema.ts
typescript
import { z } from 'zod';
import { isValidFrontendDate } from '../../core/utils/dateConversion.js';
/**
* Schema para validar datos de membresía (extData)
*/
const extDataSchema = z.object({
jubilado: z.boolean(),
nacimiento: z
.string()
.min(1, 'La fecha de nacimiento es requerida')
.refine((val) => isValidFrontendDate(val), {
message: 'Fecha de nacimiento inválida (formato: dd/mm/yyyy)',
}),
sexo: z.nativeEnum(Sexo),
fichaMedicaCompleta: z.boolean(),
fichaMedica: z.string().nullable().optional(),
});
/**
* Schema para crear un nuevo miembro
*/
export const miembroCreateSchema = z.object({
// Datos básicos
nombre: z
.string()
.min(3, 'El nombre debe tener al menos 3 caracteres')
.max(55, 'El nombre no puede exceder 55 caracteres'),
identificacion: z.coerce.number().nullable().optional(),
email: z
.preprocess((val) => {
if (typeof val === 'string') {
const trimmed = val.trim();
return trimmed === '' ? null : trimmed;
}
return val;
}, z.email('Email inválido').max(50).nullable().optional())
.nullable()
.optional(),
// Relaciones (objetos anidados para localidad, números para selects)
localidad: z.object({
id: z.number(),
nombre: z.string().optional(),
}),
condicionIva: z.number().nullable(),
vendedor: z.number().nullable(),
// Datos de membresía
idCategoria: z.number().nullable(),
extData: extDataSchema,
});
export type MiembroCreateSchemaType = z.infer<typeof miembroCreateSchema>;
/**
* Schema para actualizar un miembro existente
*/
export const miembroUpdateSchema = miembroCreateSchema.extend({
id: z.number().optional(),
});
export type MiembroUpdateSchemaType = z.infer<typeof miembroUpdateSchema>;Características importantes:
- Validaciones personalizadas con
.refine()para fechas, lógica compleja - Transformaciones con
.preprocess()para normalizar datos (ej: email vacío → null) - Tipos inferidos con
z.infer<>para type-safety - Validación de enums con
z.nativeEnum() - Objetos anidados para relaciones complejas
- Reutilización de schemas (extDataSchema dentro de miembroCreateSchema)
2. Mappers - Transformación Backend ↔ Frontend
¿Por qué usamos mappers?
- Backend usa
snake_case(PHP convention) - Frontend usa
camelCase(JavaScript convention) - Separación clara de responsabilidades
- Transformaciones de tipos (fechas, estructuras)
Ejemplo real: ts/mod-membresias/mappers/miembro.mapper.ts
typescript
import { backendDateToFrontend, frontendDateToBackend } from '../../core/utils/dateConversion.js';
// Estructura del backend (snake_case)
export interface MiembroBackend {
id: number;
nombre: string;
identificacion?: number | null;
email?: string | null;
condicion_iva?: { id: number; nombre: string } | null;
nro_iibb?: string | null;
localidad?: { id: number; nombre: string; cod_post?: string } | null;
ext_data?: {
id_ordcon: number;
jubilado: boolean;
nacimiento: string; // Backend: "2000-12-31"
sexo: Sexo;
ficha_medica_completa: boolean;
fecha_baja?: string | null;
motivo_baja?: string | null;
} | null;
categoria?: { id: number; nombre: string } | null;
}
/**
* Mapper: Backend (snake_case) → Frontend (camelCase)
*/
export const mapMiembroToFrontend = (item: MiembroBackend): Miembro => ({
id: item.id,
nombre: item.nombre,
identificacion: item.identificacion,
email: item.email,
condicionIva: item.condicion_iva ? {
id: item.condicion_iva.id,
nombre: item.condicion_iva.nombre,
} : null,
nroIibb: item.nro_iibb,
localidad: item.localidad ? {
id: item.localidad.id,
nombre: item.localidad.nombre,
...(item.localidad.cod_post !== undefined && { codPost: item.localidad.cod_post }),
} : null,
extData: item.ext_data ? {
idOrdcon: item.ext_data.id_ordcon,
jubilado: item.ext_data.jubilado,
// Transformación de fechas: "2000-12-31" → "31/12/2000"
nacimiento: backendDateToFrontend(item.ext_data.nacimiento),
sexo: item.ext_data.sexo,
fichaMedicaCompleta: item.ext_data.ficha_medica_completa,
fechaBaja: item.ext_data.fecha_baja ? backendDateToFrontend(item.ext_data.fecha_baja) : null,
motivoBaja: item.ext_data.motivo_baja ?? null,
} : null,
categoria: item.categoria ? {
id: item.categoria.id,
nombre: item.categoria.nombre,
} : null,
});
/**
* Mapper: Frontend (camelCase) → Backend (snake_case)
*/
export const mapMiembroToBackend = (
data: MiembroCreateSchemaType | MiembroUpdateSchemaType
): Record<string, unknown> => {
return {
nombre: data.nombre,
identificacion: data.identificacion ?? null,
email: data.email ?? null,
localidad: data.localidad?.id ? { id: data.localidad.id } : null,
condicion_iva: data.condicionIva ? { id: data.condicionIva } : null,
nro_iibb: data.nroIibb ?? null,
id_categoria: data.idCategoria ?? null,
ext_data: {
jubilado: data.extData.jubilado,
// Transformación de fechas: "31/12/2000" → "2000-12-31"
nacimiento: frontendDateToBackend(data.extData.nacimiento),
sexo: data.extData.sexo,
ficha_medica_completa: data.extData.fichaMedicaCompleta,
},
};
};Ventajas del patrón Mapper:
- Independencia de convenciones de nomenclatura backend/frontend
- Transformaciones centralizadas (fechas, estructuras)
- Type-safety en ambas direcciones
- Fácil mantenimiento cuando cambia el backend
3. Services (API calls)
Ejemplo real: ts/mod-membresias/services/miembro.service.ts
typescript
import api from '../../api/api.js';
import type { ApiResponse, QueryOptions, PaginatedResponse } from '../../api/api.types.js';
import { mapMiembroToFrontend, mapMiembroToBackend, type MiembroBackend } from '../mappers/miembro.mapper.js';
const basePath = 'mod-membresia/';
export type MemberStatus = 'all' | 'active' | 'inactive' | 'non_member';
export interface MiembroQueryOptions extends QueryOptions {
categoriaId?: number;
memberStatus?: MemberStatus;
}
export const MiembroService = {
/**
* Get all miembros with server-side pagination, filtering, and sorting
* Por defecto trae sólo miembros activos (memberStatus = 'active')
*/
getAll: async (options: MiembroQueryOptions = {}) => {
const result = await api.get<ApiResponse<PaginatedResponse<MiembroBackend>>>(
`${basePath}miembros`,
{
params: {
scope: options.scope ?? 'max',
include: options.include ?? null,
filter: options.filter,
pageIndex: options.pageIndex,
pageSize: options.pageSize,
order: options.order,
columnFilter: options.columnFilter,
...(options.categoriaId != null ? { categoriaId: options.categoriaId } : {}),
memberStatus: options.memberStatus ?? 'active', // Por defecto solo activos
},
}
);
const paginatedResponse = result.data.data;
// Validación defensiva
if (!paginatedResponse?.data || !Array.isArray(paginatedResponse.data)) {
console.error('Invalid paginated response structure:', paginatedResponse);
throw new Error('Invalid API response structure for miembros');
}
// Mapear cada item del backend al frontend
return {
data: paginatedResponse.data.map(mapMiembroToFrontend),
meta: paginatedResponse.meta,
};
},
/**
* Get miembro by ID
*/
getById: async (id: number) => {
const result = await api.get<ApiResponse<MiembroBackend>>(
`${basePath}miembros/${id}`,
{
params: {
scope: 'max',
include: ['categoria', 'disciplinas', 'productos'],
},
}
);
return mapMiembroToFrontend(result.data.data);
},
/**
* Dar de baja un miembro (DELETE con payload)
*/
darBaja: async (id: number, data: BajaMiembroData) => {
const payload = mapBajaMiembroToBackend(data);
const result = await api.delete<ApiResponse<void>>(
`${basePath}miembros/${id}`,
{ data: payload }
);
return result.data;
},
/**
* Create new miembro
*/
create: async (data: MiembroCreateSchemaType) => {
const payload = mapMiembroToBackend(data);
const result = await api.post<ApiResponse<MiembroBackend>>(
`${basePath}miembros`,
payload
);
return mapMiembroToFrontend(result.data.data);
},
/**
* Update existing miembro
*/
update: async (id: number, data: MiembroUpdateSchemaType) => {
const payload = mapMiembroToBackend(data);
const result = await api.put<ApiResponse<MiembroBackend>>(
`${basePath}miembros/${id}`,
payload
);
return mapMiembroToFrontend(result.data.data);
},
/**
* Reactivate a member using POST endpoint
*/
reactivar: async (id: number) => {
const result = await api.post<ApiResponse<MiembroBackend>>(
`${basePath}miembros/${id}/reactivar`
);
return mapMiembroToFrontend(result.data.data);
},
};Características importantes:
- Tipado estricto con genéricos de TypeScript
- Server-side pagination con QueryOptions
- Mappers para transformar datos en ambas direcciones
- Validación defensiva de respuestas del API
- Parámetros opcionales con defaults (memberStatus = 'active')
- Documentación JSDoc en cada método
4. Hooks (React Query)
Ejemplo real: ts/mod-membresias/hooks/useMiembro.ts
typescript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { MiembroService } from '../services/miembro.service.js';
import { membresiaQueryKeys } from '../config/queryKeys.js';
interface UseMiembroListOptions {
queryOptions?: MiembroQueryOptions;
enabled?: boolean;
}
/**
* Hook para obtener una lista de miembros con paginación y filtros
*/
export function useMiembroList(options: UseMiembroListOptions = {}) {
const { queryOptions = {}, enabled = true } = options;
return useQuery({
queryKey: membresiaQueryKeys.miembros.list(queryOptions),
queryFn: () => MiembroService.getAll(queryOptions),
staleTime: 5 * 60 * 1000, // 5 minutos - datos frescos por 5 min
enabled,
});
}
/**
* Hook para obtener un miembro por ID
*/
export function useMiembroById(id: number, enabled = true) {
return useQuery({
queryKey: membresiaQueryKeys.miembros.detail(id),
queryFn: () => MiembroService.getById(id),
staleTime: 5 * 60 * 1000,
enabled,
});
}
/**
* Hook para crear un miembro
*/
export function useCreateMiembro() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: MiembroCreateSchemaType) => MiembroService.create(data),
onSuccess: (newMiembro) => {
// Invalidar todas las listas de miembros
queryClient.invalidateQueries({ queryKey: membresiaQueryKeys.miembros.lists() });
// Optimistic update: agregar el nuevo miembro al cache de detalle
queryClient.setQueryData(
membresiaQueryKeys.miembros.detail(newMiembro.id),
newMiembro
);
},
});
}
/**
* Hook para actualizar un miembro
*/
export function useUpdateMiembro() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: MiembroUpdateSchemaType }) =>
MiembroService.update(id, data),
onMutate: async ({ id, data }) => {
// Cancelar queries en curso para evitar race conditions
await queryClient.cancelQueries({
queryKey: membresiaQueryKeys.miembros.detail(id)
});
// Snapshot del valor anterior
const previousMiembro = queryClient.getQueryData<Miembro>(
membresiaQueryKeys.miembros.detail(id)
);
// Optimistic update - actualizar UI inmediatamente
if (previousMiembro) {
queryClient.setQueryData(
membresiaQueryKeys.miembros.detail(id),
{ ...previousMiembro, ...data }
);
}
// Retornar contexto para rollback si falla
return { previousMiembro };
},
onError: (_err, { id }, context) => {
// Revertir en caso de error
if (context?.previousMiembro) {
queryClient.setQueryData(
membresiaQueryKeys.miembros.detail(id),
context.previousMiembro
);
}
},
onSuccess: (updatedMiembro, { id }) => {
// Actualizar cache con datos reales del servidor
queryClient.setQueryData(
membresiaQueryKeys.miembros.detail(id),
updatedMiembro
);
// Invalidar listas para que se actualicen
queryClient.invalidateQueries({
queryKey: membresiaQueryKeys.miembros.lists()
});
},
});
}
/**
* Hook para dar de baja un miembro
*/
export function useDarBajaMiembro() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: BajaMiembroData }) =>
MiembroService.darBaja(id, data),
onSuccess: (_, { id }) => {
// Invalidar el detalle del miembro
queryClient.invalidateQueries({
queryKey: membresiaQueryKeys.miembros.detail(id)
});
// Invalidar todas las listas de miembros
queryClient.invalidateQueries({
queryKey: membresiaQueryKeys.miembros.lists()
});
},
});
}
/**
* Hook para reactivar un miembro
*/
export function useReactivarMiembro() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => MiembroService.reactivar(id),
onSuccess: (reactivatedMiembro) => {
// Actualizar el cache del detalle
queryClient.setQueryData(
membresiaQueryKeys.miembros.detail(reactivatedMiembro.id),
reactivatedMiembro
);
// Invalidar todas las listas de miembros
queryClient.invalidateQueries({
queryKey: membresiaQueryKeys.miembros.lists()
});
},
});
}Características importantes:
- Query Keys Factory (
membresiaQueryKeys) para organizar keys de cache - Optimistic Updates en mutaciones para UX instantánea
- Rollback automático si la mutación falla
- Cache invalidation selectiva (solo lo necesario)
- staleTime configurado para evitar refetches innecesarios
- enabled flag para deshabilitar queries condicionales
- Tipos inferidos de schemas y services
5. Componentes - Tabla con Server-Side Rendering
Ejemplo real: ts/mod-membresias/components/MiembroTable.tsx
Este componente demuestra una tabla con paginación, filtrado y ordenamiento del lado del servidor (Server-Side Rendering), un patrón muy usado:
typescript
import { useMemo } from 'react';
import type { MRT_ColumnDef } from 'material-react-table';
import Box from '@mui/material/Box';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Radio from '@mui/material/Radio';
import RestoreIcon from '@mui/icons-material/Restore';
import { DataTable } from '../../core/components/DataTable.js';
import type { DataTableAction } from '../../core/components/DataTable.js';
import type { Miembro } from '../types/miembro.types.js';
import type { MemberStatus } from '../services/miembro.service.js';
import { useMiembroTableSSR } from '../hooks/useMiembroTableSSR.js';
interface MiembroTableProps {
reloadSignal?: number;
onDelete?: (miembro: Miembro) => void;
onEdit?: (miembro: Miembro) => void;
onReactivar?: (miembro: Miembro) => void;
categoriaId?: number | null;
enableStatusFilter?: boolean;
defaultMemberStatus?: MemberStatus;
}
/**
* Tabla de miembros con paginación, filtrado y ordenamiento del lado del servidor (SSR)
*/
export function MiembroTable({
reloadSignal,
onDelete,
onEdit,
onReactivar,
categoriaId,
enableStatusFilter = true,
defaultMemberStatus = 'active',
}: MiembroTableProps) {
// Hook personalizado que encapsula toda la lógica SSR
const {
data,
meta,
isLoading,
error,
pagination,
sorting,
columnFilters,
memberStatus,
handlePaginationChange,
handleSortingChange,
handleColumnFiltersChange,
setMemberStatus,
} = useMiembroTableSSR({ reloadSignal, categoriaId, defaultMemberStatus });
// Definición de columnas con useMemo para evitar re-renders
const columns: MRT_ColumnDef<Miembro>[] = useMemo(() => {
const baseColumns: MRT_ColumnDef<Miembro>[] = [
{ accessorKey: 'id', header: 'Código', size: 100, enableColumnFilter: true },
{ accessorKey: 'nombre', header: 'Nombre', size: 300, enableColumnFilter: true },
];
// Columnas condicionales según el estado
if (memberStatus !== 'non_member') {
baseColumns.push({
accessorKey: 'categoria.nombre',
header: 'Categoría',
size: 200,
enableSorting: false,
Cell: ({ row }) => row.original.categoria?.nombre ?? '-',
});
}
// Columnas adicionales para inactivos
if (memberStatus === 'inactive') {
baseColumns.push(
{
accessorKey: 'extData.fechaBaja',
header: 'Fecha de Baja',
Cell: ({ row }) => row.original.extData?.fechaBaja ?? '-',
},
{
accessorKey: 'extData.motivoBaja',
header: 'Motivo de Baja',
Cell: ({ row }) => row.original.extData?.motivoBaja ?? '-',
}
);
}
return baseColumns;
}, [memberStatus]);
// Acciones dinámicas según el estado del miembro
const actions: DataTableAction<Miembro>[] = useMemo(() => {
if (memberStatus === 'active') {
return [
...(onEdit ? [{ type: 'edit' as const, onClick: onEdit }] : []),
...(onDelete ? [{ type: 'delete' as const, onClick: onDelete }] : []),
];
} else if (memberStatus === 'inactive') {
return onReactivar
? [{ type: 'custom' as const, icon: <RestoreIcon />, onClick: onReactivar }]
: [];
}
return onEdit ? [{ type: 'edit' as const, onClick: onEdit }] : [];
}, [memberStatus, onEdit, onDelete, onReactivar]);
if (error) {
return <div>Error al cargar miembros: {error.message}</div>;
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Filtro de estado de miembro */}
{enableStatusFilter && (
<Box sx={{ pl: 2, pt: 2 }}>
<FormControl component="fieldset">
<FormLabel>Filtrar por estado</FormLabel>
<RadioGroup
row
value={memberStatus}
onChange={(e) => setMemberStatus(e.target.value as MemberStatus)}
>
<FormControlLabel value="active" control={<Radio />} label="Activos" />
<FormControlLabel value="inactive" control={<Radio />} label="Inactivos" />
<FormControlLabel value="non_member" control={<Radio />} label="No Miembros" />
</RadioGroup>
</FormControl>
</Box>
)}
{/* Tabla */}
<DataTable<Miembro>
resourceName="miembros"
data={data}
columns={columns}
actions={actions}
manualPagination
rowCount={meta?.totalRowCount ?? 0}
onPaginationChange={handlePaginationChange}
manualSorting
onSortingChange={handleSortingChange}
manualFiltering
onColumnFiltersChange={handleColumnFiltersChange}
state={{ isLoading, pagination, sorting, columnFilters }}
initialState={{ sorting: [{ id: 'id', desc: false }] }}
/>
</Box>
);
}Características del patrón de componentes:
- Server-Side Rendering: Paginación/ordenamiento/filtrado en servidor
- useMemo: Optimización de re-renders
- Columnas dinámicas: Cambian según el estado
- Acciones condicionales: Diferentes botones según estado
- Props opcionales con defaults
- Componentes reutilizables (
DataTabledel core) - Manejo de errores con UI feedback
- TypeScript strict
Configuración de Material UI
Ubicación: ts/config/theme.ts
El proyecto utiliza un tema personalizado de Material UI con:
- Paleta de colores personalizada (primary, secondary)
- Tipografía configurada (Roboto como fuente principal)
- Sobrescrituras de componentes: Por ejemplo, botones sin transformación automática a mayúsculas
Providers y Setup Principal
Ubicación: ts/main.tsx
La aplicación se configura con múltiples providers:
- React.StrictMode: Modo estricto de React para desarrollo
- QueryClientProvider: Proveedor de TanStack Query con configuración:
refetchOnWindowFocus: false- No refetch automático al cambiar de pestañaretry: 1- Solo un reintento en caso de error
- ThemeProvider: Proveedor del tema de Material UI
- CssBaseline: Normalización de estilos CSS de MUI
Sistema de Componentes Montables
El proyecto React utiliza un patrón de componentes montables que permite integrar componentes React dentro de páginas PHP Legacy:
Características:
- Componentes independientes que se pueden montar en cualquier elemento DOM
- Comunicación mediante props y eventos personalizados
- Coexistencia con código Legacy
- Migración incremental sin reescribir todo el sistema
Ejemplo de uso: Ver CLAUDE.md en bautista-app para más detalles sobre esta arquitectura híbrida.
Convención para Nuevas Vistas React (Contexto Legacy)
Toda nueva vista implementada en React y destinada a ser utilizada dentro de una página PHP Legacy (es decir, montada en un contexto no React completo) debe seguir la siguiente convención:
- El componente principal de la vista debe terminar con el sufijo
App(ej:MiNuevaFuncionalidadApp.tsx). - Este componente
*Appdebe ser el punto de montaje directo. No debe existir un archivo intermedio de montaje que a su vez renderice el*App. - La invocación para el montaje desde el contexto Legacy (PHP/JS) debe apuntar directamente al componente
*App.
Justificación: Esta convención simplifica la identificación de los puntos de entrada para las vistas React en entornos híbridos y asegura un montaje consistente y directo, evitando capas de abstracción innecesarias para la integración Legacy.
API REST (Backend separation)
- Protocolo: HTTP/REST
- Formato: JSON
- Autenticación: JWT via headers
Arquitectura Frontend
Modelo Legacy (Actual)
El sistema utiliza rendering en servidor con PHP:
┌─────────────────────────────────────┐
│ Browser │
├─────────────────────────────────────┤
│ HTML/CSS/JS (Legacy) │ ← Rendered por PHP
├─────────────────────────────────────┤
│ AJAX Calls │ ← Comunicación con API
├─────────────────────────────────────┤
│ Backend API (REST) │ ← Endpoints JSON
└─────────────────────────────────────┘Estructura de Vistas
Las vistas se organizan por módulo:
views/ (ubicación por confirmar)
├── auth/
│ ├── login.php
│ └── register.php
├── ventas/
│ ├── comprobantes.php
│ ├── facturacion.php
│ └── informes/
│ └── ventas-provincias.php
├── tesoreria/
│ ├── recibos.php
│ └── retenciones.php
└── ...Tipos de Componentes Frontend
1. Listados (Grillas/Tablas)
Propósito: Mostrar colecciones de datos con filtros, búsqueda y paginación.
Características:
- Tabla con columnas configurables
- Filtros por campos
- Búsqueda global
- Ordenamiento por columnas
- Paginación
- Acciones por fila (editar, eliminar, ver detalle)
Permisos típicos:
MODULO_RECURSO_VIEW: Ver listado
Ejemplo: Listado de conceptos de retenciones
- Columnas: Nombre, Cuenta, Tipo, Valor, Impuesto/Servicio, Acumula
- Filtros: Por tipo (P/F), por impser (Sí/No)
- Acciones: Editar, Eliminar
2. Formularios (Alta/Edición)
Propósito: Crear o modificar registros.
Características:
- Campos de entrada tipados
- Validación client-side
- Validación server-side
- Mensajes de error inline
- Botones de guardar/cancelar
- Confirmaciones para cambios
Permisos requeridos: Los permisos en el sistema son jerárquicos y por funcionalidad completa, no granulares por acción:
- Formato:
MODULO_SECCION_FUNCIONALIDAD - Niveles: 1 (Módulo) → 2 (Sección) → 3 (Funcionalidad) → 4 (Sub-opción)
- Control: Acceso completo a la funcionalidad (no hay VIEW/WRITE/DELETE separados)
Ejemplos:
VENTAS_BASES_CLIENTES: Acceso completo al ABM de clientesVENTAS_MOV_FACT: Acceso a facturación electrónicaCONFIG_PERMISOS_GRUPOS: Acceso a gestión de grupos
Tipos de inputs:
- Text inputs
- Number inputs
- Selects/Dropdowns
- Checkboxes
- Date pickers
- Autocomplete
Ejemplo: Formulario de retención
┌─────────────────────────────────────┐
│ Alta/Edición de Retención │
├─────────────────────────────────────┤
│ Nombre: [_______________] │
│ Cuenta: [_______________] │
│ Tipo: (•) Porcentaje ( ) Fijo │
│ Valor: [_______________] │
│ ☑ Es Impuesto/Servicio │
│ ☑ Acumula con anteriores │
│ │
│ [Guardar] [Cancelar] │
└─────────────────────────────────────┘3. Vistas de Detalle
Propósito: Mostrar información completa de un registro.
Características:
- Solo lectura (usualmente)
- Información organizada en secciones
- Datos relacionados
- Historial/auditoría
- Acciones disponibles
Permisos típicos:
MODULO_RECURSO_VIEW: Ver detalle
4. Modales/Dialogs
Propósito: Mostrar información o formularios sin cambiar de página.
Tipos:
- Confirmación: Confirmar acciones destructivas
- Información: Mostrar detalles rápidos
- Formularios: Alta/edición rápida
- Selección: Elegir entre opciones
Ejemplo: Modal de confirmación de eliminación
┌─────────────────────────────────────┐
│ ⚠️ Confirmar eliminación │
├─────────────────────────────────────┤
│ ¿Está seguro que desea eliminar │
│ el concepto de retención "IIBB"? │
│ │
│ Esta acción no se puede deshacer. │
│ │
│ [Cancelar] [Eliminar] │
└─────────────────────────────────────┘5. Reportes/Informes
Propósito: Mostrar información agregada y analítica.
Características:
- Formulario de parámetros (fechas, filtros)
- Visualización tabular o gráfica
- Exportación (PDF, Excel)
- Totales y subtotales
- Agrupaciones
Permisos típicos:
MODULO_INFORMES_VIEW: Ver informes
Ejemplo: Ventas por Provincias por Rubros
- Parámetros: Rango de fechas, rango de rubros
- Vista: Modal Legacy PHP SSR
- Formato: Tabla agrupada por provincia
- Exportación: PDF
Navegación y Routing
Estructura de Menú
Sistema
├── Ventas
│ ├── Comprobantes
│ ├── Facturación
│ └── Informes
│ ├── Ventas por cliente
│ ├── Ventas por producto
│ └── Ventas por provincias
│ └── Por rubros (modal)
├── Tesorería
│ ├── Recibos
│ ├── Conceptos de retenciones
│ └── Movimientos de caja
├── Cuenta Corriente
├── Compras
├── Contabilidad
├── Stock
└── CRMURLs (Legacy)
El sistema legacy probablemente usa URLs como:
/index.php?modulo=ventas&accion=listar
/index.php?modulo=ventas&accion=formulario&id=123
/index.php?modulo=tesoreria&accion=retencionesSistema de Permisos
Estructura de Permisos
El sistema utiliza permisos jerárquicos por funcionalidad completa, no granulares por operación.
Formato: MODULO_SECCION_FUNCIONALIDAD
Niveles jerárquicos:
- Nivel 1 - Módulo: Acceso al módulo principal (ej:
VENTAS,CONFIG,TESORERIA) - Nivel 2 - Sección: Acceso a secciones dentro del módulo (ej:
VENTAS_BASES,VENTAS_MOV,VENTAS_INF) - Nivel 3 - Funcionalidad: Acceso a funcionalidad específica (ej:
VENTAS_BASES_CLIENTES,VENTAS_MOV_FACT) - Nivel 4 - Sub-opción: Opciones dentro de funcionalidades (ej:
VENTAS_BASES_LISTA-PRECIO_RANGO)
Ejemplos reales:
VENTAS (Nivel 1: Módulo)
├── VENTAS_BASES (Nivel 2: Sección)
│ ├── VENTAS_BASES_CLIENTES (Nivel 3: ABM Clientes)
│ ├── VENTAS_BASES_ARTICULOS (Nivel 3: ABM Artículos)
│ └── VENTAS_BASES_LISTA-PRECIO (Nivel 3: Listas)
│ ├── VENTAS_BASES_LISTA-PRECIO_RANGO (Nivel 4: Por rango)
│ └── VENTAS_BASES_LISTA-PRECIO_COSTO (Nivel 4: Por costo)
├── VENTAS_MOV (Nivel 2: Sección)
│ ├── VENTAS_MOV_FACT (Nivel 3: Facturación)
│ └── VENTAS_MOV_PEDIDOS (Nivel 3: Pedidos)
└── VENTAS_INF (Nivel 2: Sección)
├── VENTAS_INF_CLIENTES (Nivel 3: Informe clientes)
└── VENTAS_INF_SUBDIARIO (Nivel 3: Subdiario)Importante: Cada permiso otorga acceso completo a la funcionalidad (listar, crear, editar, eliminar). No hay permisos separados por operación como VIEW/WRITE/DELETE.
Control de Acceso en UI
A nivel de menú: No mostrar opciones sin permiso A nivel de vista: Verificar permisos antes de renderizar A nivel de funcionalidad: Mostrar/ocultar toda la funcionalidad según permiso
Ejemplo Legacy PHP:
php
<?php if (hasPermission('VENTAS_BASES_CLIENTES')): ?>
<!-- Mostrar toda la funcionalidad de ABM clientes -->
<div class="clientes-section">
<button onclick="nuevoCliente()">Nuevo</button>
<button onclick="editarCliente(<?= $id ?>)">Editar</button>
<button onclick="eliminarCliente(<?= $id ?>)">Eliminar</button>
</div>
<?php endif; ?>Ejemplo React:
jsx
{hasPermission('VENTAS_BASES_CLIENTES') && (
<ClientesModule>
<Button onClick={handleNuevo}>Nuevo</Button>
<Button onClick={handleEditar}>Editar</Button>
<Button onClick={handleEliminar}>Eliminar</Button>
</ClientesModule>
)}Comunicación con Backend
El sistema tiene dos enfoques diferentes de comunicación según la tecnología:
1. Legacy (PHP SSR) - Peticiones a backend/
⚠️ Enfoque Legacy - No usar en código nuevo
El frontend Legacy hace peticiones a archivos PHP dentro de la carpeta backend/ del mismo proyecto frontend:
javascript
// Petición FETCH mediante API Class
ApiRequest.post('/backend/tesoreria/procesar_retencion',{
nombre: 'IIBB',
cuenta: 1234567890,
tipo: 'P',
valor: 2.5
});Estructura Legacy:
frontend/
├── index.php
├── views/
└── backend/ ← Archivos PHP que procesan peticiones
├── tesoreria/
│ └── procesar_retencion.php
└── ventas/Problemas de este enfoque:
- Lógica de negocio mezclada con frontend
- Difícil de mantener y testear
- No reutilizable por otros clientes
- Acoplamiento fuerte
2. Moderno (React) - Axios a API REST
✅ Enfoque Recomendado - Usar en todo código nuevo
El frontend React hace peticiones directas al backend API REST usando Axios:
javascript
// GET - Listar retenciones
const getRetenciones = async () => {
try {
const response = await api.get('/tesoreria/boniret');
return response.data;
} catch (error) {
console.error('Error:', error);
throw error;
}
};Estructura Moderna:
bautista-app/ (Frontend React)
├── src/
│ ├── api/
│ │ ├── axios.js ← Configuración de Axios
│ │ └── tesoreria.js
│ └── components/
bautista-backend/ (Backend API REST)
├── Routes/
├── controller/
├── service/
└── models/Ventajas de este enfoque:
- Separación clara frontend/backend
- Reutilizable por múltiples clientes (web, mobile)
- Fácil de testear
- Escalable y mantenible
3. Comparación de Enfoques
| Aspecto | Legacy (backend/) | Moderno (Axios + API REST) |
|---|---|---|
| Ubicación | /frontend/backend/*.php | Backend separado /api/* |
| Protocolo | Peticiones a archivos PHP | HTTP REST JSON |
| Cliente HTTP | jQuery AJAX / fetch | Axios |
| Autenticación | Sesiones PHP | JWT tokens |
| Reutilización | Solo web | Web, mobile, integraciones |
| Testing | Difícil | Fácil (unitario + integración) |
| Estado | ⚠️ Legacy - deprecar | ✅ Actual - usar siempre |
Manejo de Respuestas
Success Response:
json
{
"status": 200,
"message": "Datos recibidos correctamente.",
"data": { ... }
}Error Response:
json
{
"error": "Mensaje de error descriptivo"
}Estado de Carga
Mostrar indicadores visuales durante operaciones asíncronas:
javascript
function cargarDatos() {
mostrarSpinner();
fetch('/api/...')
.then(response => response.json())
.then(data => {
ocultarSpinner();
renderizarDatos(data);
})
.catch(error => {
ocultarSpinner();
mostrarError(error.message);
});
}Validación de Datos
Client-Side Validation
Validaciones básicas en el cliente para UX:
javascript
function validarFormulario() {
let errores = [];
// Campo requerido
if (!nombre.value) {
errores.push('El nombre es requerido');
}
// Longitud máxima
if (nombre.value.length > 15) {
errores.push('El nombre no puede exceder 15 caracteres');
}
// Valor numérico
if (isNaN(parseFloat(valor.value))) {
errores.push('El valor debe ser numérico');
}
// Mostrar errores
if (errores.length > 0) {
mostrarErrores(errores);
return false;
}
return true;
}Importante: Las validaciones client-side son solo para UX. El backend SIEMPRE debe validar.
Mostrar Errores
Errores inline junto a campos:
html
<div class="form-group">
<label>Nombre:</label>
<input type="text" id="nombre" name="nombre" />
<span class="error-message" id="nombre-error"></span>
</div>Patrones de UI
Loading States
Mostrar estado de carga:
html
<div class="spinner" id="spinner" style="display: none;">
<i class="fa fa-spinner fa-spin"></i> Cargando...
</div>Empty States
Mostrar cuando no hay datos:
html
<div class="empty-state">
<p>No se encontraron resultados</p>
<button onclick="nuevaRetencion()">Crear primera retención</button>
</div>Error States
Mostrar errores de forma clara:
html
<div class="alert alert-error">
<strong>Error:</strong> No se pudo guardar el registro.
<button onclick="reintentar()">Reintentar</button>
</div>Success Messages
Confirmar operaciones exitosas:
html
<div class="alert alert-success">
<strong>Éxito:</strong> El registro se guardó correctamente.
</div>Estilos y Temas
CSS Structure
Organización de estilos:
css/
├── base/
│ ├── reset.css
│ ├── typography.css
│ └── variables.css
├── components/
│ ├── buttons.css
│ ├── forms.css
│ ├── tables.css
│ └── modals.css
├── layouts/
│ ├── grid.css
│ └── navigation.css
└── modules/
├── ventas.css
└── tesoreria.cssResponsive Design
Asegurar que las vistas funcionen en diferentes dispositivos:
- Desktop (1024px+)
- Tablet (768px - 1023px)
- Mobile (< 768px)
Migración a SPA (Futuro)
Consideraciones para modernización
Si se decide migrar a un SPA moderno (React, Vue, Angular):
Ventajas:
- Mejor experiencia de usuario
- Componentes reutilizables
- Estado de aplicación centralizado
- Testing más robusto
- Desarrollo más ágil
Pasos sugeridos:
- Migrar módulo por módulo
- Mantener backend API intacto
- Coexistencia temporal legacy + SPA
- Migración incremental
Stack sugerido:
- React/Vue/Angular
- TypeScript
- State management (Redux/Vuex/NgRx)
- Routing (React Router/Vue Router)
- UI Library (Material UI/Vuetify)
Documentar Vistas
Template para vista
Al documentar una vista frontend, incluir:
- Ubicación: Ruta en el menú
- Propósito: Qué hace la vista
- Permisos: Permisos necesarios
- Elementos UI: Qué muestra
- Interacciones: Qué puede hacer el usuario
- API calls: Qué endpoints consume
- Validaciones: Qué se valida
- Estados: Loading, error, empty, success
Ver: Template de funcionalidad sección Frontend
Referencias
- Arquitectura Legacy - PHP SSR y Vanilla JS (deprecado)
- Arquitectura Backend
- Guía de funcionalidades
- Ejemplo: Boniret Resource
- Ejemplo: Ventas por Provincias
Última actualización: 2025-12-09