erp-construccion/docs/06-frontend-specs/README.md

608 lines
18 KiB
Markdown

# 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
```tsx
// 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)
```tsx
// 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
```tsx
// 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)
```tsx
// 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
```tsx
// 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
| Modulo | Spec Componentes | Spec Paginas | Estado |
|--------|------------------|--------------|--------|
| shared | [COMP-shared.md](components/COMP-shared.md) | - | Documentado |
| construction | [COMP-construction.md](components/COMP-construction.md) | [PAGES-construction.md](pages/PAGES-construction.md) | Documentado |
| compliance | [COMP-compliance.md](components/COMP-compliance.md) | [PAGES-compliance.md](pages/PAGES-compliance.md) | Documentado |
| finance | [COMP-finance.md](components/COMP-finance.md) | [PAGES-finance.md](pages/PAGES-finance.md) | Documentado |
| assets | [COMP-assets.md](components/COMP-assets.md) | [PAGES-assets.md](pages/PAGES-assets.md) | Documentado |
| documents | [COMP-documents.md](components/COMP-documents.md) | [PAGES-documents.md](pages/PAGES-documents.md) | Documentado |
---
## Routing
### Estructura de Rutas
```tsx
// 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
- [Backend Specifications](../05-backend-specs/)
- [Domain Models](../04-modelado/domain-models/)
- [Epicas](../08-epicas/)
---
*Ultima actualizacion: 2025-12-05*