608 lines
18 KiB
Markdown
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*
|