Frontend Specifications - ERP Construccion
Fecha: 2025-12-05
Version: 1.0.0
Stack: React 18 / TypeScript 5.3+ / Vite / Zustand / TanStack Query / TailwindCSS
Estructura del Directorio
06-frontend-specs/
├── README.md (este archivo)
├── components/
│ ├── COMP-shared.md [Componentes compartidos]
│ ├── COMP-construction.md [Componentes de construccion]
│ ├── COMP-compliance.md [Componentes de cumplimiento]
│ ├── COMP-finance.md [Componentes financieros]
│ ├── COMP-assets.md [Componentes de activos]
│ └── COMP-documents.md [Componentes DMS]
├── pages/
│ ├── PAGES-construction.md [Paginas de proyectos]
│ ├── PAGES-compliance.md [Paginas de cumplimiento]
│ ├── PAGES-finance.md [Paginas financieras]
│ ├── PAGES-assets.md [Paginas de activos]
│ └── PAGES-documents.md [Paginas de documentos]
└── stores/
└── STORES-spec.md [Especificacion de stores]
Arquitectura Frontend
Estructura de Aplicacion
apps/frontend/src/
├── app/
│ ├── App.tsx
│ ├── router.tsx
│ └── providers.tsx
├── features/
│ ├── construction/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── hooks/
│ │ ├── api/
│ │ └── store/
│ ├── compliance/
│ ├── finance/
│ ├── assets/
│ └── documents/
├── shared/
│ ├── components/
│ │ ├── ui/
│ │ ├── forms/
│ │ ├── tables/
│ │ └── charts/
│ ├── hooks/
│ ├── utils/
│ ├── types/
│ └── api/
├── layouts/
│ ├── MainLayout.tsx
│ ├── AuthLayout.tsx
│ └── components/
└── styles/
Patrones de Desarrollo
1. Feature-Based Structure
Cada modulo sigue una estructura consistente:
features/construction/
├── components/
│ ├── ProjectCard.tsx
│ ├── ProjectList.tsx
│ ├── BudgetForm.tsx
│ └── ProgressChart.tsx
├── pages/
│ ├── ProjectsPage.tsx
│ ├── ProjectDetailPage.tsx
│ ├── BudgetPage.tsx
│ └── ProgressPage.tsx
├── hooks/
│ ├── useProjects.ts
│ ├── useBudget.ts
│ └── useProgress.ts
├── api/
│ ├── projects.api.ts
│ ├── budgets.api.ts
│ └── progress.api.ts
├── store/
│ └── construction.store.ts
├── types/
│ └── construction.types.ts
└── index.ts
2. Component Pattern
// components/ProjectCard.tsx
import { FC, memo } from 'react';
import { Card, Badge, Progress } from '@/shared/components/ui';
import { Project } from '../types/construction.types';
interface ProjectCardProps {
project: Project;
onClick?: (id: string) => void;
showProgress?: boolean;
}
export const ProjectCard: FC<ProjectCardProps> = memo(({
project,
onClick,
showProgress = true
}) => {
return (
<Card
className="p-4 hover:shadow-lg transition-shadow cursor-pointer"
onClick={() => onClick?.(project.id)}
>
<div className="flex justify-between items-start">
<div>
<h3 className="font-semibold text-lg">{project.name}</h3>
<p className="text-sm text-gray-500">{project.code}</p>
</div>
<Badge variant={getStatusVariant(project.status)}>
{project.status}
</Badge>
</div>
{showProgress && (
<div className="mt-4">
<Progress value={project.progressPercentage} />
<span className="text-sm text-gray-600">
{project.progressPercentage}% completado
</span>
</div>
)}
</Card>
);
});
ProjectCard.displayName = 'ProjectCard';
3. Hook Pattern (TanStack Query)
// hooks/useProjects.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { projectsApi } from '../api/projects.api';
import { CreateProjectDto, ProjectQueryDto } from '../types/construction.types';
const PROJECTS_KEY = ['projects'];
export function useProjects(query?: ProjectQueryDto) {
return useQuery({
queryKey: [...PROJECTS_KEY, query],
queryFn: () => projectsApi.findAll(query),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
export function useProject(id: string) {
return useQuery({
queryKey: [...PROJECTS_KEY, id],
queryFn: () => projectsApi.findOne(id),
enabled: !!id,
});
}
export function useCreateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: CreateProjectDto) => projectsApi.create(dto),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
},
});
}
export function useUpdateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, dto }: { id: string; dto: UpdateProjectDto }) =>
projectsApi.update(id, dto),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: [...PROJECTS_KEY, id] });
queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
},
});
}
export function useDeleteProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => projectsApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
},
});
}
4. API Layer Pattern
// api/projects.api.ts
import { apiClient } from '@/shared/api/client';
import { PaginatedResponse } from '@/shared/types';
import {
Project,
ProjectDetail,
CreateProjectDto,
UpdateProjectDto,
ProjectQueryDto
} from '../types/construction.types';
const BASE_URL = '/api/v1/projects';
export const projectsApi = {
findAll: async (query?: ProjectQueryDto): Promise<PaginatedResponse<Project>> => {
const response = await apiClient.get(BASE_URL, { params: query });
return response.data;
},
findOne: async (id: string): Promise<ProjectDetail> => {
const response = await apiClient.get(`${BASE_URL}/${id}`);
return response.data;
},
create: async (dto: CreateProjectDto): Promise<Project> => {
const response = await apiClient.post(BASE_URL, dto);
return response.data;
},
update: async (id: string, dto: UpdateProjectDto): Promise<Project> => {
const response = await apiClient.put(`${BASE_URL}/${id}`, dto);
return response.data;
},
delete: async (id: string): Promise<void> => {
await apiClient.delete(`${BASE_URL}/${id}`);
},
updateStatus: async (id: string, status: ProjectStatus): Promise<Project> => {
const response = await apiClient.put(`${BASE_URL}/${id}/status`, { status });
return response.data;
}
};
5. Store Pattern (Zustand)
// store/construction.store.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { Project, ProjectFilters } from '../types/construction.types';
interface ConstructionState {
// State
selectedProjectId: string | null;
filters: ProjectFilters;
viewMode: 'list' | 'grid' | 'kanban';
// Actions
setSelectedProject: (id: string | null) => void;
setFilters: (filters: Partial<ProjectFilters>) => void;
resetFilters: () => void;
setViewMode: (mode: 'list' | 'grid' | 'kanban') => void;
}
const initialFilters: ProjectFilters = {
status: undefined,
search: '',
dateRange: undefined,
};
export const useConstructionStore = create<ConstructionState>()(
devtools(
persist(
(set) => ({
// Initial State
selectedProjectId: null,
filters: initialFilters,
viewMode: 'list',
// Actions
setSelectedProject: (id) =>
set({ selectedProjectId: id }, false, 'setSelectedProject'),
setFilters: (filters) =>
set(
(state) => ({ filters: { ...state.filters, ...filters } }),
false,
'setFilters'
),
resetFilters: () =>
set({ filters: initialFilters }, false, 'resetFilters'),
setViewMode: (mode) =>
set({ viewMode: mode }, false, 'setViewMode'),
}),
{
name: 'construction-store',
partialize: (state) => ({ viewMode: state.viewMode }),
}
),
{ name: 'ConstructionStore' }
)
);
6. Page Pattern
// pages/ProjectsPage.tsx
import { FC, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
PageHeader,
SearchInput,
FilterBar,
Pagination,
LoadingSpinner,
ErrorMessage,
EmptyState
} from '@/shared/components';
import { useProjects, useCreateProject } from '../hooks/useProjects';
import { useConstructionStore } from '../store/construction.store';
import { ProjectList, ProjectGrid, ProjectKanban } from '../components';
import { CreateProjectModal } from '../components/CreateProjectModal';
export const ProjectsPage: FC = () => {
const navigate = useNavigate();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const { filters, viewMode, setFilters, setViewMode } = useConstructionStore();
const { data, isLoading, error, refetch } = useProjects(filters);
const createProject = useCreateProject();
const handleProjectClick = (id: string) => {
navigate(`/projects/${id}`);
};
const handleCreateProject = async (dto: CreateProjectDto) => {
await createProject.mutateAsync(dto);
setIsCreateModalOpen(false);
};
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
return (
<div className="space-y-6">
<PageHeader
title="Proyectos"
subtitle={`${data?.total || 0} proyectos encontrados`}
actions={
<Button onClick={() => setIsCreateModalOpen(true)}>
+ Nuevo Proyecto
</Button>
}
/>
<div className="flex gap-4 items-center">
<SearchInput
value={filters.search}
onChange={(search) => setFilters({ search })}
placeholder="Buscar proyectos..."
/>
<FilterBar
filters={filters}
onChange={setFilters}
options={filterOptions}
/>
<ViewToggle value={viewMode} onChange={setViewMode} />
</div>
{data?.items.length === 0 ? (
<EmptyState
title="Sin proyectos"
description="No hay proyectos que coincidan con los filtros"
action={
<Button onClick={() => setIsCreateModalOpen(true)}>
Crear primer proyecto
</Button>
}
/>
) : (
<>
{viewMode === 'list' && (
<ProjectList projects={data.items} onClick={handleProjectClick} />
)}
{viewMode === 'grid' && (
<ProjectGrid projects={data.items} onClick={handleProjectClick} />
)}
{viewMode === 'kanban' && (
<ProjectKanban projects={data.items} onClick={handleProjectClick} />
)}
<Pagination
total={data.total}
page={filters.page}
limit={filters.limit}
onChange={(page) => setFilters({ page })}
/>
</>
)}
<CreateProjectModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSubmit={handleCreateProject}
isLoading={createProject.isPending}
/>
</div>
);
};
Componentes Compartidos
UI Components
| Componente |
Descripcion |
Props |
| Button |
Boton con variantes |
variant, size, disabled, loading |
| Card |
Contenedor con sombra |
className, onClick, children |
| Badge |
Etiqueta de estado |
variant, size, children |
| Modal |
Dialogo modal |
isOpen, onClose, title, children |
| Tooltip |
Tooltip informativo |
content, position, children |
| Dropdown |
Menu desplegable |
items, trigger, onSelect |
| Avatar |
Avatar de usuario |
src, name, size |
| Progress |
Barra de progreso |
value, max, variant |
| Spinner |
Indicador de carga |
size, color |
| Alert |
Mensaje de alerta |
type, title, message, onClose |
Form Components
| Componente |
Descripcion |
Props |
| Input |
Campo de texto |
label, error, helper, ...inputProps |
| Select |
Selector |
options, label, error, onChange |
| Checkbox |
Casilla de verificacion |
label, checked, onChange |
| Radio |
Boton de radio |
options, value, onChange |
| DatePicker |
Selector de fecha |
value, onChange, format |
| FileUpload |
Carga de archivos |
accept, maxSize, onUpload |
| TextArea |
Area de texto |
label, rows, maxLength |
| NumberInput |
Campo numerico |
min, max, step, format |
| CurrencyInput |
Campo de moneda |
currency, locale, onChange |
Table Components
| Componente |
Descripcion |
Props |
| DataTable |
Tabla de datos |
columns, data, onSort, onFilter |
| TablePagination |
Paginacion de tabla |
total, page, limit, onChange |
| TableFilters |
Filtros de tabla |
filters, onChange |
| TableActions |
Acciones de fila |
actions, row |
| ExportButton |
Exportar datos |
formats, onExport |
Chart Components
| Componente |
Descripcion |
Props |
| LineChart |
Grafico de lineas |
data, xKey, yKey, options |
| BarChart |
Grafico de barras |
data, xKey, yKey, options |
| PieChart |
Grafico circular |
data, nameKey, valueKey |
| GaugeChart |
Indicador circular |
value, max, thresholds |
| AreaChart |
Grafico de area |
data, xKey, yKey, options |
Modulos Frontend
Routing
Estructura de Rutas
// app/router.tsx
import { createBrowserRouter } from 'react-router-dom';
import { MainLayout } from '@/layouts/MainLayout';
import { AuthLayout } from '@/layouts/AuthLayout';
export const router = createBrowserRouter([
{
path: '/',
element: <MainLayout />,
children: [
// Dashboard
{ index: true, element: <DashboardPage /> },
// Construction
{ path: 'projects', element: <ProjectsPage /> },
{ path: 'projects/:id', element: <ProjectDetailPage /> },
{ path: 'projects/:id/budget', element: <BudgetPage /> },
{ path: 'projects/:id/progress', element: <ProgressPage /> },
{ path: 'developments', element: <DevelopmentsPage /> },
{ path: 'developments/:id', element: <DevelopmentDetailPage /> },
// Compliance
{ path: 'compliance', element: <ComplianceDashboardPage /> },
{ path: 'compliance/programs', element: <ProgramsPage /> },
{ path: 'compliance/project/:id', element: <ProjectCompliancePage /> },
{ path: 'compliance/audits', element: <AuditsPage /> },
{ path: 'compliance/audits/:id', element: <AuditDetailPage /> },
// Finance
{ path: 'finance', element: <FinanceDashboardPage /> },
{ path: 'finance/accounting', element: <AccountingPage /> },
{ path: 'finance/payables', element: <PayablesPage /> },
{ path: 'finance/receivables', element: <ReceivablesPage /> },
{ path: 'finance/cash-flow', element: <CashFlowPage /> },
{ path: 'finance/bank', element: <BankReconciliationPage /> },
{ path: 'finance/reports', element: <FinanceReportsPage /> },
// Assets
{ path: 'assets', element: <AssetsPage /> },
{ path: 'assets/:id', element: <AssetDetailPage /> },
{ path: 'assets/maintenance', element: <MaintenancePage /> },
{ path: 'assets/work-orders', element: <WorkOrdersPage /> },
{ path: 'assets/tracking', element: <TrackingMapPage /> },
// Documents
{ path: 'documents', element: <DocumentsPage /> },
{ path: 'documents/folder/:id', element: <FolderPage /> },
{ path: 'documents/plans', element: <PlansPage /> },
{ path: 'documents/approvals', element: <ApprovalsPage /> },
// Settings
{ path: 'settings', element: <SettingsPage /> },
],
},
{
path: '/auth',
element: <AuthLayout />,
children: [
{ path: 'login', element: <LoginPage /> },
{ path: 'forgot-password', element: <ForgotPasswordPage /> },
{ path: 'reset-password', element: <ResetPasswordPage /> },
],
},
]);
State Management
Global State (Zustand)
stores/
├── auth.store.ts // Autenticacion y usuario actual
├── tenant.store.ts // Contexto de tenant
├── ui.store.ts // Estado de UI (sidebar, theme, etc.)
├── notifications.store.ts // Notificaciones
└── preferences.store.ts // Preferencias de usuario
Server State (TanStack Query)
- Datos de servidor manejados por TanStack Query
- Cache automatico con invalidacion
- Prefetching para navegacion rapida
- Optimistic updates para mejor UX
Referencias
Ultima actualizacion: 2025-12-05