1513 lines
37 KiB
Markdown
1513 lines
37 KiB
Markdown
# ET-OBRA-001: Especificación Técnica Frontend - Control de Obra
|
|
|
|
## 1. Información General
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Módulo** | MAI-005 - Control de Obra |
|
|
| **Vertical** | Construcción |
|
|
| **Tipo** | Especificación Técnica - Frontend |
|
|
| **Versión** | 1.0.0 |
|
|
| **Fecha** | 2025-12-06 |
|
|
| **Estado** | Borrador |
|
|
|
|
## 2. Objetivo
|
|
|
|
Definir la arquitectura, componentes y patrones de desarrollo del frontend para el módulo de Control de Obra, enfocado en la gestión de avance físico, financiero, reportes de obra y seguimiento ejecutivo.
|
|
|
|
## 3. Stack Tecnológico
|
|
|
|
### 3.1 Core Technologies
|
|
|
|
| Tecnología | Versión | Propósito |
|
|
|------------|---------|-----------|
|
|
| React | 18.3.x | UI Library |
|
|
| TypeScript | 5.3.x | Type Safety |
|
|
| Vite | 5.0.x | Build Tool & Dev Server |
|
|
| Zustand | 4.5.x | State Management |
|
|
| React Router | 6.20.x | Routing & Navigation |
|
|
|
|
### 3.2 UI & Styling
|
|
|
|
| Librería | Versión | Uso |
|
|
|----------|---------|-----|
|
|
| Tailwind CSS | 3.4.x | Utility-first CSS |
|
|
| Headless UI | 2.0.x | Accessible Components |
|
|
| Lucide React | 0.300.x | Icon System |
|
|
| Framer Motion | 11.0.x | Animations |
|
|
|
|
### 3.3 Data Visualization
|
|
|
|
| Librería | Versión | Uso |
|
|
|----------|---------|-----|
|
|
| Chart.js | 4.4.x | Charting Library |
|
|
| react-chartjs-2 | 5.2.x | React Wrapper |
|
|
| Leaflet | 1.9.x | Interactive Maps |
|
|
| react-leaflet | 4.2.x | React Integration |
|
|
|
|
### 3.4 Real-time & Networking
|
|
|
|
| Librería | Versión | Uso |
|
|
|----------|---------|-----|
|
|
| Socket.io Client | 4.6.x | WebSocket Communication |
|
|
| Axios | 1.6.x | HTTP Client |
|
|
| React Query | 5.17.x | Server State Management |
|
|
|
|
### 3.5 Forms & Validation
|
|
|
|
| Librería | Versión | Uso |
|
|
|----------|---------|-----|
|
|
| React Hook Form | 7.49.x | Form Management |
|
|
| Zod | 3.22.x | Schema Validation |
|
|
| date-fns | 3.0.x | Date Utilities |
|
|
|
|
### 3.6 File Management
|
|
|
|
| Librería | Versión | Uso |
|
|
|----------|---------|-----|
|
|
| React Dropzone | 14.2.x | File Upload |
|
|
| react-image-lightbox | 5.1.x | Photo Gallery |
|
|
| react-pdf | 7.7.x | PDF Preview |
|
|
|
|
## 4. Arquitectura Frontend
|
|
|
|
### 4.1 Estructura de Directorios
|
|
|
|
```
|
|
src/
|
|
├── features/
|
|
│ ├── progress/ # Avance de Obra
|
|
│ │ ├── components/
|
|
│ │ │ ├── ProgressForm.tsx
|
|
│ │ │ ├── CurveSChart.tsx
|
|
│ │ │ ├── EVMDashboard.tsx
|
|
│ │ │ ├── ProgressTable.tsx
|
|
│ │ │ └── ProgressFilters.tsx
|
|
│ │ ├── hooks/
|
|
│ │ │ ├── useProgress.ts
|
|
│ │ │ ├── useCurveS.ts
|
|
│ │ │ └── useEVM.ts
|
|
│ │ ├── stores/
|
|
│ │ │ └── progressStore.ts
|
|
│ │ ├── services/
|
|
│ │ │ └── progressService.ts
|
|
│ │ ├── types/
|
|
│ │ │ └── progress.types.ts
|
|
│ │ └── utils/
|
|
│ │ └── evmCalculations.ts
|
|
│ │
|
|
│ ├── work-log/ # Bitácora de Obra
|
|
│ │ ├── components/
|
|
│ │ │ ├── WorkLogForm.tsx
|
|
│ │ │ ├── WorkLogTimeline.tsx
|
|
│ │ │ ├── PhotoGallery.tsx
|
|
│ │ │ ├── PhotoUploader.tsx
|
|
│ │ │ ├── WeatherWidget.tsx
|
|
│ │ │ └── WorkLogFilters.tsx
|
|
│ │ ├── hooks/
|
|
│ │ │ ├── useWorkLog.ts
|
|
│ │ │ ├── usePhotoUpload.ts
|
|
│ │ │ └── useWeather.ts
|
|
│ │ ├── stores/
|
|
│ │ │ └── workLogStore.ts
|
|
│ │ ├── services/
|
|
│ │ │ └── workLogService.ts
|
|
│ │ └── types/
|
|
│ │ └── workLog.types.ts
|
|
│ │
|
|
│ ├── estimations/ # Estimaciones
|
|
│ │ ├── components/
|
|
│ │ │ ├── EstimationForm.tsx
|
|
│ │ │ ├── EstimationTable.tsx
|
|
│ │ │ ├── ConceptSelector.tsx
|
|
│ │ │ ├── EstimationSummary.tsx
|
|
│ │ │ └── EstimationHistory.tsx
|
|
│ │ ├── hooks/
|
|
│ │ │ ├── useEstimation.ts
|
|
│ │ │ └── useEstimationCalc.ts
|
|
│ │ ├── stores/
|
|
│ │ │ └── estimationStore.ts
|
|
│ │ ├── services/
|
|
│ │ │ └── estimationService.ts
|
|
│ │ └── types/
|
|
│ │ └── estimation.types.ts
|
|
│ │
|
|
│ └── dashboard/ # Dashboard Ejecutivo
|
|
│ ├── components/
|
|
│ │ ├── ExecutiveDashboard.tsx
|
|
│ │ ├── ProgressKPIs.tsx
|
|
│ │ ├── FinancialKPIs.tsx
|
|
│ │ ├── ProjectMap.tsx
|
|
│ │ ├── CriticalAlerts.tsx
|
|
│ │ └── RecentActivity.tsx
|
|
│ ├── hooks/
|
|
│ │ ├── useDashboard.ts
|
|
│ │ ├── useRealtime.ts
|
|
│ │ └── useKPIs.ts
|
|
│ ├── stores/
|
|
│ │ └── dashboardStore.ts
|
|
│ └── services/
|
|
│ └── dashboardService.ts
|
|
│
|
|
├── shared/
|
|
│ ├── components/
|
|
│ │ ├── charts/
|
|
│ │ │ ├── LineChart.tsx
|
|
│ │ │ ├── BarChart.tsx
|
|
│ │ │ ├── PieChart.tsx
|
|
│ │ │ └── GaugeChart.tsx
|
|
│ │ ├── maps/
|
|
│ │ │ ├── ProjectMap.tsx
|
|
│ │ │ ├── LocationMarker.tsx
|
|
│ │ │ └── MapControls.tsx
|
|
│ │ ├── forms/
|
|
│ │ │ ├── FormInput.tsx
|
|
│ │ │ ├── FormSelect.tsx
|
|
│ │ │ ├── FormDatePicker.tsx
|
|
│ │ │ └── FormFileUpload.tsx
|
|
│ │ └── layout/
|
|
│ │ ├── PageHeader.tsx
|
|
│ │ ├── Card.tsx
|
|
│ │ └── Tabs.tsx
|
|
│ ├── hooks/
|
|
│ │ ├── useWebSocket.ts
|
|
│ │ ├── useFileUpload.ts
|
|
│ │ └── useExport.ts
|
|
│ ├── utils/
|
|
│ │ ├── chartConfig.ts
|
|
│ │ ├── mapConfig.ts
|
|
│ │ ├── formatters.ts
|
|
│ │ └── validators.ts
|
|
│ └── types/
|
|
│ └── common.types.ts
|
|
│
|
|
├── config/
|
|
│ ├── chartDefaults.ts
|
|
│ ├── mapDefaults.ts
|
|
│ └── websocket.config.ts
|
|
│
|
|
└── App.tsx
|
|
```
|
|
|
|
### 4.2 Patrón de Feature Modules
|
|
|
|
Cada feature module es autónomo e incluye:
|
|
|
|
- **Components**: Componentes React específicos del feature
|
|
- **Hooks**: Custom hooks para lógica reutilizable
|
|
- **Stores**: Estado global con Zustand
|
|
- **Services**: Comunicación con API
|
|
- **Types**: Definiciones TypeScript
|
|
- **Utils**: Funciones auxiliares
|
|
|
|
## 5. Componentes Principales
|
|
|
|
### 5.1 Progress Module
|
|
|
|
#### 5.1.1 ProgressForm
|
|
|
|
**Propósito**: Formulario para registrar avance de conceptos
|
|
|
|
```typescript
|
|
interface ProgressFormProps {
|
|
projectId: string;
|
|
estimationId?: string;
|
|
initialData?: ProgressEntry;
|
|
onSubmit: (data: ProgressFormData) => Promise<void>;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
interface ProgressFormData {
|
|
concept_id: string;
|
|
period_start: Date;
|
|
period_end: Date;
|
|
quantity_executed: number;
|
|
photos: File[];
|
|
observations: string;
|
|
location?: {
|
|
lat: number;
|
|
lng: number;
|
|
};
|
|
}
|
|
```
|
|
|
|
**Features**:
|
|
- Selección de conceptos del catálogo
|
|
- Cálculo automático de importes
|
|
- Upload de fotos con preview
|
|
- Geolocalización opcional
|
|
- Validación de cantidades vs. estimado
|
|
|
|
#### 5.1.2 CurveSChart
|
|
|
|
**Propósito**: Visualización de Curva S (Planned vs Actual)
|
|
|
|
```typescript
|
|
interface CurveSChartProps {
|
|
projectId: string;
|
|
dateRange?: { start: Date; end: Date };
|
|
showBaseline?: boolean;
|
|
showForecast?: boolean;
|
|
}
|
|
|
|
interface CurveSData {
|
|
date: Date;
|
|
planned_cumulative: number;
|
|
actual_cumulative: number;
|
|
forecast_cumulative?: number;
|
|
}
|
|
```
|
|
|
|
**Chart Configuration**:
|
|
- Tipo: Line Chart (Chart.js)
|
|
- Datasets:
|
|
- Planned: Línea azul continua
|
|
- Actual: Línea verde continua con markers
|
|
- Forecast: Línea naranja punteada
|
|
- Interactividad:
|
|
- Tooltip con detalles
|
|
- Zoom/Pan
|
|
- Export PNG/PDF
|
|
|
|
#### 5.1.3 EVMDashboard
|
|
|
|
**Propósito**: Dashboard de Earned Value Management
|
|
|
|
```typescript
|
|
interface EVMMetrics {
|
|
pv: number; // Planned Value
|
|
ev: number; // Earned Value
|
|
ac: number; // Actual Cost
|
|
sv: number; // Schedule Variance
|
|
cv: number; // Cost Variance
|
|
spi: number; // Schedule Performance Index
|
|
cpi: number; // Cost Performance Index
|
|
eac: number; // Estimate at Completion
|
|
etc: number; // Estimate to Complete
|
|
vac: number; // Variance at Completion
|
|
}
|
|
```
|
|
|
|
**Visualizations**:
|
|
- Gauge charts para SPI/CPI
|
|
- Bar chart comparativo PV/EV/AC
|
|
- KPI cards con varianzas
|
|
- Forecast trends
|
|
|
|
### 5.2 Work Log Module
|
|
|
|
#### 5.2.1 WorkLogForm
|
|
|
|
**Propósito**: Formulario para bitácora diaria
|
|
|
|
```typescript
|
|
interface WorkLogFormProps {
|
|
projectId: string;
|
|
date: Date;
|
|
initialData?: WorkLog;
|
|
onSubmit: (data: WorkLogFormData) => Promise<void>;
|
|
}
|
|
|
|
interface WorkLogFormData {
|
|
date: Date;
|
|
weather: {
|
|
condition: WeatherCondition;
|
|
temperature: number;
|
|
humidity: number;
|
|
};
|
|
workforce: {
|
|
category: string;
|
|
count: number;
|
|
}[];
|
|
equipment: {
|
|
type: string;
|
|
hours: number;
|
|
}[];
|
|
activities: string;
|
|
incidents: string;
|
|
materials_received: {
|
|
material: string;
|
|
quantity: number;
|
|
}[];
|
|
photos: File[];
|
|
visitors?: {
|
|
name: string;
|
|
company: string;
|
|
purpose: string;
|
|
}[];
|
|
}
|
|
```
|
|
|
|
#### 5.2.2 WorkLogTimeline
|
|
|
|
**Propósito**: Vista cronológica de bitácoras
|
|
|
|
```typescript
|
|
interface WorkLogTimelineProps {
|
|
projectId: string;
|
|
dateRange: { start: Date; end: Date };
|
|
filters?: WorkLogFilters;
|
|
groupBy?: 'day' | 'week' | 'month';
|
|
}
|
|
```
|
|
|
|
**Features**:
|
|
- Timeline vertical con cards
|
|
- Filtros por fecha, clima, actividad
|
|
- Search full-text
|
|
- Export to PDF
|
|
|
|
#### 5.2.3 PhotoGallery
|
|
|
|
**Propósito**: Galería de fotos de obra
|
|
|
|
```typescript
|
|
interface PhotoGalleryProps {
|
|
photos: WorkPhoto[];
|
|
onPhotoClick: (photo: WorkPhoto) => void;
|
|
layout?: 'grid' | 'masonry';
|
|
filterBy?: {
|
|
date?: Date;
|
|
tags?: string[];
|
|
location?: string;
|
|
};
|
|
}
|
|
|
|
interface WorkPhoto {
|
|
id: string;
|
|
url: string;
|
|
thumbnail_url: string;
|
|
caption: string;
|
|
date: Date;
|
|
location?: {
|
|
lat: number;
|
|
lng: number;
|
|
};
|
|
tags: string[];
|
|
work_log_id: string;
|
|
}
|
|
```
|
|
|
|
**Features**:
|
|
- Lazy loading de imágenes
|
|
- Lightbox para vista completa
|
|
- Filtros y búsqueda
|
|
- Download individual/batch
|
|
- Geolocalización en mapa
|
|
|
|
### 5.3 Estimations Module
|
|
|
|
#### 5.3.1 EstimationForm
|
|
|
|
**Propósito**: Formulario para crear estimaciones
|
|
|
|
```typescript
|
|
interface EstimationFormProps {
|
|
projectId: string;
|
|
estimationNumber?: number;
|
|
initialData?: Estimation;
|
|
onSubmit: (data: EstimationFormData) => Promise<void>;
|
|
}
|
|
|
|
interface EstimationFormData {
|
|
estimation_number: number;
|
|
period_start: Date;
|
|
period_end: Date;
|
|
concepts: EstimationConcept[];
|
|
retention_percentage: number;
|
|
advance_payment_deduction: number;
|
|
notes: string;
|
|
}
|
|
|
|
interface EstimationConcept {
|
|
concept_id: string;
|
|
quantity_previous: number;
|
|
quantity_current: number;
|
|
quantity_total: number;
|
|
amount_current: number;
|
|
}
|
|
```
|
|
|
|
#### 5.3.2 EstimationTable
|
|
|
|
**Propósito**: Tabla interactiva de conceptos
|
|
|
|
```typescript
|
|
interface EstimationTableProps {
|
|
concepts: EstimationConcept[];
|
|
editable?: boolean;
|
|
onConceptUpdate?: (conceptId: string, data: Partial<EstimationConcept>) => void;
|
|
showSummary?: boolean;
|
|
}
|
|
```
|
|
|
|
**Features**:
|
|
- Edición inline de cantidades
|
|
- Cálculos automáticos
|
|
- Totales y subtotales
|
|
- Validación de cantidades
|
|
- Export Excel/PDF
|
|
- Resaltado de variaciones
|
|
|
|
### 5.4 Dashboard Module
|
|
|
|
#### 5.4.1 ExecutiveDashboard
|
|
|
|
**Propósito**: Dashboard ejecutivo en tiempo real
|
|
|
|
```typescript
|
|
interface ExecutiveDashboardProps {
|
|
projectId?: string;
|
|
portfolioView?: boolean;
|
|
refreshInterval?: number;
|
|
}
|
|
```
|
|
|
|
**Layout**:
|
|
```
|
|
┌─────────────────────────────────────────────────┐
|
|
│ KPIs Row │
|
|
│ [Progress] [Cost] [Schedule] [Quality] │
|
|
├─────────────────────────────────────────────────┤
|
|
│ Charts Row │
|
|
│ [Curve S] [EVM Chart] │
|
|
├─────────────────────────────────────────────────┤
|
|
│ Maps & Alerts │
|
|
│ [Project Map] [Critical Alerts] │
|
|
├─────────────────────────────────────────────────┤
|
|
│ Activity & Status │
|
|
│ [Recent Activity] [Project Status] │
|
|
└─────────────────────────────────────────────────┘
|
|
```
|
|
|
|
#### 5.4.2 ProjectMap
|
|
|
|
**Propósito**: Mapa interactivo de proyectos
|
|
|
|
```typescript
|
|
interface ProjectMapProps {
|
|
projects: ProjectLocation[];
|
|
selectedProject?: string;
|
|
onProjectSelect: (projectId: string) => void;
|
|
showProgress?: boolean;
|
|
showAlerts?: boolean;
|
|
}
|
|
|
|
interface ProjectLocation {
|
|
id: string;
|
|
name: string;
|
|
coordinates: [number, number];
|
|
progress_percent: number;
|
|
status: ProjectStatus;
|
|
alerts_count: number;
|
|
}
|
|
```
|
|
|
|
**Leaflet Configuration**:
|
|
- Base layer: OpenStreetMap
|
|
- Custom markers por estado de proyecto
|
|
- Popup con KPIs del proyecto
|
|
- Cluster para múltiples proyectos
|
|
- Controls: Zoom, fullscreen, layer selector
|
|
|
|
## 6. State Management
|
|
|
|
### 6.1 Zustand Stores
|
|
|
|
#### 6.1.1 Progress Store
|
|
|
|
```typescript
|
|
interface ProgressState {
|
|
// State
|
|
currentProject: string | null;
|
|
progressEntries: ProgressEntry[];
|
|
curveData: CurveSData[];
|
|
evmMetrics: EVMMetrics | null;
|
|
filters: ProgressFilters;
|
|
|
|
// Actions
|
|
setProject: (projectId: string) => void;
|
|
loadProgressEntries: (projectId: string) => Promise<void>;
|
|
addProgressEntry: (entry: ProgressEntry) => Promise<void>;
|
|
updateProgressEntry: (id: string, data: Partial<ProgressEntry>) => Promise<void>;
|
|
loadCurveData: (projectId: string) => Promise<void>;
|
|
loadEVMMetrics: (projectId: string) => Promise<void>;
|
|
setFilters: (filters: Partial<ProgressFilters>) => void;
|
|
clearFilters: () => void;
|
|
}
|
|
|
|
const useProgressStore = create<ProgressState>((set, get) => ({
|
|
// Implementation
|
|
}));
|
|
```
|
|
|
|
#### 6.1.2 Dashboard Store
|
|
|
|
```typescript
|
|
interface DashboardState {
|
|
// State
|
|
kpis: DashboardKPIs | null;
|
|
realtimeData: RealtimeUpdate[];
|
|
alerts: Alert[];
|
|
isConnected: boolean;
|
|
lastUpdate: Date | null;
|
|
|
|
// Actions
|
|
loadKPIs: (projectId?: string) => Promise<void>;
|
|
subscribeToUpdates: (projectId?: string) => void;
|
|
unsubscribeFromUpdates: () => void;
|
|
addRealtimeUpdate: (update: RealtimeUpdate) => void;
|
|
acknowledgeAlert: (alertId: string) => Promise<void>;
|
|
}
|
|
```
|
|
|
|
## 7. WebSocket Integration
|
|
|
|
### 7.1 Socket.io Client Setup
|
|
|
|
```typescript
|
|
// config/websocket.config.ts
|
|
export const SOCKET_CONFIG = {
|
|
url: import.meta.env.VITE_WS_URL || 'http://localhost:3000',
|
|
options: {
|
|
transports: ['websocket', 'polling'],
|
|
reconnection: true,
|
|
reconnectionDelay: 1000,
|
|
reconnectionDelayMax: 5000,
|
|
reconnectionAttempts: 5,
|
|
},
|
|
};
|
|
|
|
// shared/hooks/useWebSocket.ts
|
|
export const useWebSocket = (namespace: string = '/') => {
|
|
const [socket, setSocket] = useState<Socket | null>(null);
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const newSocket = io(`${SOCKET_CONFIG.url}${namespace}`, SOCKET_CONFIG.options);
|
|
|
|
newSocket.on('connect', () => setIsConnected(true));
|
|
newSocket.on('disconnect', () => setIsConnected(false));
|
|
|
|
setSocket(newSocket);
|
|
|
|
return () => {
|
|
newSocket.close();
|
|
};
|
|
}, [namespace]);
|
|
|
|
return { socket, isConnected };
|
|
};
|
|
```
|
|
|
|
### 7.2 Real-time Events
|
|
|
|
```typescript
|
|
// Dashboard realtime updates
|
|
socket.on('progress:updated', (data: ProgressUpdate) => {
|
|
dashboardStore.getState().addRealtimeUpdate(data);
|
|
progressStore.getState().loadProgressEntries(data.project_id);
|
|
});
|
|
|
|
socket.on('estimation:created', (data: EstimationCreated) => {
|
|
dashboardStore.getState().addRealtimeUpdate(data);
|
|
estimationStore.getState().loadEstimations(data.project_id);
|
|
});
|
|
|
|
socket.on('alert:created', (data: Alert) => {
|
|
dashboardStore.getState().addRealtimeUpdate(data);
|
|
// Show toast notification
|
|
});
|
|
|
|
socket.on('kpi:updated', (data: KPIUpdate) => {
|
|
dashboardStore.getState().loadKPIs(data.project_id);
|
|
});
|
|
```
|
|
|
|
## 8. Chart.js Configurations
|
|
|
|
### 8.1 Curve S Chart
|
|
|
|
```typescript
|
|
// shared/utils/chartConfig.ts
|
|
export const curveSChartConfig = (data: CurveSData[]): ChartConfiguration => ({
|
|
type: 'line',
|
|
data: {
|
|
labels: data.map(d => format(d.date, 'MMM yyyy')),
|
|
datasets: [
|
|
{
|
|
label: 'Planeado',
|
|
data: data.map(d => d.planned_cumulative),
|
|
borderColor: 'rgb(59, 130, 246)',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
tension: 0.4,
|
|
},
|
|
{
|
|
label: 'Real',
|
|
data: data.map(d => d.actual_cumulative),
|
|
borderColor: 'rgb(34, 197, 94)',
|
|
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
|
tension: 0.4,
|
|
pointRadius: 4,
|
|
},
|
|
{
|
|
label: 'Pronóstico',
|
|
data: data.map(d => d.forecast_cumulative),
|
|
borderColor: 'rgb(249, 115, 22)',
|
|
backgroundColor: 'rgba(249, 115, 22, 0.1)',
|
|
borderDash: [5, 5],
|
|
tension: 0.4,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
},
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
callbacks: {
|
|
label: (context) => {
|
|
return `${context.dataset.label}: ${formatCurrency(context.parsed.y)}`;
|
|
},
|
|
},
|
|
},
|
|
zoom: {
|
|
zoom: {
|
|
wheel: { enabled: true },
|
|
pinch: { enabled: true },
|
|
mode: 'x',
|
|
},
|
|
pan: {
|
|
enabled: true,
|
|
mode: 'x',
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: (value) => formatCurrency(value),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
### 8.2 EVM Performance Chart
|
|
|
|
```typescript
|
|
export const evmChartConfig = (metrics: EVMMetrics): ChartConfiguration => ({
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['Valor'],
|
|
datasets: [
|
|
{
|
|
label: 'Valor Planeado (PV)',
|
|
data: [metrics.pv],
|
|
backgroundColor: 'rgba(59, 130, 246, 0.7)',
|
|
},
|
|
{
|
|
label: 'Valor Ganado (EV)',
|
|
data: [metrics.ev],
|
|
backgroundColor: 'rgba(34, 197, 94, 0.7)',
|
|
},
|
|
{
|
|
label: 'Costo Real (AC)',
|
|
data: [metrics.ac],
|
|
backgroundColor: 'rgba(239, 68, 68, 0.7)',
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
indexAxis: 'y',
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: (context) => {
|
|
return `${context.dataset.label}: ${formatCurrency(context.parsed.x)}`;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
x: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: (value) => formatCurrency(value),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
### 8.3 Performance Index Gauges
|
|
|
|
```typescript
|
|
export const gaugeChartConfig = (
|
|
value: number,
|
|
label: string,
|
|
thresholds: { warning: number; danger: number }
|
|
): ChartConfiguration => ({
|
|
type: 'doughnut',
|
|
data: {
|
|
datasets: [
|
|
{
|
|
data: [value, 2 - value],
|
|
backgroundColor: [
|
|
getGaugeColor(value, thresholds),
|
|
'rgba(229, 231, 235, 0.3)',
|
|
],
|
|
borderWidth: 0,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
rotation: -90,
|
|
circumference: 180,
|
|
cutout: '75%',
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: { enabled: false },
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
## 9. Leaflet Integration
|
|
|
|
### 9.1 Map Configuration
|
|
|
|
```typescript
|
|
// shared/utils/mapConfig.ts
|
|
export const MAP_CONFIG = {
|
|
defaultCenter: [19.4326, -99.1332] as [number, number], // CDMX
|
|
defaultZoom: 13,
|
|
minZoom: 5,
|
|
maxZoom: 18,
|
|
tileLayer: {
|
|
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
|
},
|
|
};
|
|
|
|
export const getMarkerIcon = (status: ProjectStatus) => {
|
|
const color = {
|
|
active: '#22c55e',
|
|
warning: '#f59e0b',
|
|
critical: '#ef4444',
|
|
completed: '#6366f1',
|
|
}[status];
|
|
|
|
return new L.DivIcon({
|
|
html: `<div style="background-color: ${color}; width: 32px; height: 32px; border-radius: 50%; border: 3px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>`,
|
|
className: 'custom-marker',
|
|
iconSize: [32, 32],
|
|
iconAnchor: [16, 16],
|
|
});
|
|
};
|
|
```
|
|
|
|
### 9.2 ProjectMap Component
|
|
|
|
```typescript
|
|
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
|
|
import 'leaflet/dist/leaflet.css';
|
|
|
|
export const ProjectMap: React.FC<ProjectMapProps> = ({
|
|
projects,
|
|
selectedProject,
|
|
onProjectSelect,
|
|
showProgress = true,
|
|
showAlerts = true,
|
|
}) => {
|
|
return (
|
|
<MapContainer
|
|
center={MAP_CONFIG.defaultCenter}
|
|
zoom={MAP_CONFIG.defaultZoom}
|
|
style={{ height: '100%', width: '100%' }}
|
|
>
|
|
<TileLayer
|
|
url={MAP_CONFIG.tileLayer.url}
|
|
attribution={MAP_CONFIG.tileLayer.attribution}
|
|
/>
|
|
|
|
{projects.map((project) => (
|
|
<Marker
|
|
key={project.id}
|
|
position={project.coordinates}
|
|
icon={getMarkerIcon(project.status)}
|
|
eventHandlers={{
|
|
click: () => onProjectSelect(project.id),
|
|
}}
|
|
>
|
|
<Popup>
|
|
<div className="p-2">
|
|
<h3 className="font-bold text-sm">{project.name}</h3>
|
|
{showProgress && (
|
|
<div className="mt-2">
|
|
<p className="text-xs text-gray-600">Avance</p>
|
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className="bg-green-500 h-2 rounded-full"
|
|
style={{ width: `${project.progress_percent}%` }}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-right">{project.progress_percent}%</p>
|
|
</div>
|
|
)}
|
|
{showAlerts && project.alerts_count > 0 && (
|
|
<div className="mt-2 flex items-center text-red-600">
|
|
<AlertTriangle size={14} className="mr-1" />
|
|
<span className="text-xs">{project.alerts_count} alertas</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Popup>
|
|
</Marker>
|
|
))}
|
|
</MapContainer>
|
|
);
|
|
};
|
|
```
|
|
|
|
## 10. Custom Hooks
|
|
|
|
### 10.1 useProgress
|
|
|
|
```typescript
|
|
export const useProgress = (projectId: string) => {
|
|
const store = useProgressStore();
|
|
|
|
const { data: entries, isLoading, error } = useQuery({
|
|
queryKey: ['progress', projectId],
|
|
queryFn: () => progressService.getProgressEntries(projectId),
|
|
enabled: !!projectId,
|
|
});
|
|
|
|
const addEntry = useMutation({
|
|
mutationFn: (data: ProgressFormData) =>
|
|
progressService.createProgressEntry(projectId, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['progress', projectId] });
|
|
store.loadProgressEntries(projectId);
|
|
},
|
|
});
|
|
|
|
return {
|
|
entries,
|
|
isLoading,
|
|
error,
|
|
addEntry: addEntry.mutate,
|
|
isAdding: addEntry.isPending,
|
|
};
|
|
};
|
|
```
|
|
|
|
### 10.2 useCurveS
|
|
|
|
```typescript
|
|
export const useCurveS = (projectId: string, dateRange?: DateRange) => {
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['curve-s', projectId, dateRange],
|
|
queryFn: () => progressService.getCurveSData(projectId, dateRange),
|
|
enabled: !!projectId,
|
|
});
|
|
|
|
const chartConfig = useMemo(() => {
|
|
if (!data) return null;
|
|
return curveSChartConfig(data);
|
|
}, [data]);
|
|
|
|
return { data, chartConfig, isLoading };
|
|
};
|
|
```
|
|
|
|
### 10.3 useRealtime
|
|
|
|
```typescript
|
|
export const useRealtime = (projectId?: string) => {
|
|
const { socket, isConnected } = useWebSocket('/dashboard');
|
|
const dashboardStore = useDashboardStore();
|
|
|
|
useEffect(() => {
|
|
if (!socket || !isConnected) return;
|
|
|
|
if (projectId) {
|
|
socket.emit('subscribe:project', projectId);
|
|
}
|
|
|
|
socket.on('progress:updated', dashboardStore.addRealtimeUpdate);
|
|
socket.on('estimation:created', dashboardStore.addRealtimeUpdate);
|
|
socket.on('alert:created', dashboardStore.addRealtimeUpdate);
|
|
socket.on('kpi:updated', () => dashboardStore.loadKPIs(projectId));
|
|
|
|
return () => {
|
|
socket.off('progress:updated');
|
|
socket.off('estimation:created');
|
|
socket.off('alert:created');
|
|
socket.off('kpi:updated');
|
|
|
|
if (projectId) {
|
|
socket.emit('unsubscribe:project', projectId);
|
|
}
|
|
};
|
|
}, [socket, isConnected, projectId]);
|
|
|
|
return { isConnected };
|
|
};
|
|
```
|
|
|
|
### 10.4 usePhotoUpload
|
|
|
|
```typescript
|
|
export const usePhotoUpload = () => {
|
|
const [uploading, setUploading] = useState(false);
|
|
const [progress, setProgress] = useState(0);
|
|
|
|
const uploadPhotos = async (files: File[], workLogId: string) => {
|
|
setUploading(true);
|
|
setProgress(0);
|
|
|
|
const formData = new FormData();
|
|
files.forEach((file) => {
|
|
formData.append('photos', file);
|
|
});
|
|
formData.append('work_log_id', workLogId);
|
|
|
|
try {
|
|
const response = await axios.post('/api/work-logs/photos', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
onUploadProgress: (progressEvent) => {
|
|
const percent = Math.round(
|
|
(progressEvent.loaded * 100) / (progressEvent.total || 1)
|
|
);
|
|
setProgress(percent);
|
|
},
|
|
});
|
|
|
|
return response.data;
|
|
} finally {
|
|
setUploading(false);
|
|
setProgress(0);
|
|
}
|
|
};
|
|
|
|
return { uploadPhotos, uploading, progress };
|
|
};
|
|
```
|
|
|
|
## 11. Services Layer
|
|
|
|
### 11.1 Progress Service
|
|
|
|
```typescript
|
|
// features/progress/services/progressService.ts
|
|
class ProgressService {
|
|
private baseUrl = '/api/progress';
|
|
|
|
async getProgressEntries(projectId: string, filters?: ProgressFilters) {
|
|
const params = new URLSearchParams();
|
|
if (filters?.dateFrom) params.append('date_from', filters.dateFrom.toISOString());
|
|
if (filters?.dateTo) params.append('date_to', filters.dateTo.toISOString());
|
|
|
|
const response = await axios.get<ProgressEntry[]>(
|
|
`${this.baseUrl}/projects/${projectId}/entries?${params}`
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
async createProgressEntry(projectId: string, data: ProgressFormData) {
|
|
const formData = new FormData();
|
|
formData.append('concept_id', data.concept_id);
|
|
formData.append('period_start', data.period_start.toISOString());
|
|
formData.append('period_end', data.period_end.toISOString());
|
|
formData.append('quantity_executed', data.quantity_executed.toString());
|
|
formData.append('observations', data.observations);
|
|
|
|
data.photos.forEach((photo) => {
|
|
formData.append('photos', photo);
|
|
});
|
|
|
|
const response = await axios.post<ProgressEntry>(
|
|
`${this.baseUrl}/projects/${projectId}/entries`,
|
|
formData
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
async getCurveSData(projectId: string, dateRange?: DateRange) {
|
|
const params = new URLSearchParams();
|
|
if (dateRange) {
|
|
params.append('start_date', dateRange.start.toISOString());
|
|
params.append('end_date', dateRange.end.toISOString());
|
|
}
|
|
|
|
const response = await axios.get<CurveSData[]>(
|
|
`${this.baseUrl}/projects/${projectId}/curve-s?${params}`
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
async getEVMMetrics(projectId: string) {
|
|
const response = await axios.get<EVMMetrics>(
|
|
`${this.baseUrl}/projects/${projectId}/evm`
|
|
);
|
|
return response.data;
|
|
}
|
|
}
|
|
|
|
export const progressService = new ProgressService();
|
|
```
|
|
|
|
### 11.2 Dashboard Service
|
|
|
|
```typescript
|
|
class DashboardService {
|
|
private baseUrl = '/api/dashboard';
|
|
|
|
async getKPIs(projectId?: string) {
|
|
const url = projectId
|
|
? `${this.baseUrl}/projects/${projectId}/kpis`
|
|
: `${this.baseUrl}/portfolio/kpis`;
|
|
|
|
const response = await axios.get<DashboardKPIs>(url);
|
|
return response.data;
|
|
}
|
|
|
|
async getAlerts(projectId?: string, severity?: AlertSeverity) {
|
|
const params = new URLSearchParams();
|
|
if (projectId) params.append('project_id', projectId);
|
|
if (severity) params.append('severity', severity);
|
|
|
|
const response = await axios.get<Alert[]>(
|
|
`${this.baseUrl}/alerts?${params}`
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
async acknowledgeAlert(alertId: string) {
|
|
await axios.post(`${this.baseUrl}/alerts/${alertId}/acknowledge`);
|
|
}
|
|
}
|
|
|
|
export const dashboardService = new DashboardService();
|
|
```
|
|
|
|
## 12. Type Definitions
|
|
|
|
### 12.1 Progress Types
|
|
|
|
```typescript
|
|
// features/progress/types/progress.types.ts
|
|
export interface ProgressEntry {
|
|
id: string;
|
|
project_id: string;
|
|
concept_id: string;
|
|
concept_name: string;
|
|
unit: string;
|
|
unit_price: number;
|
|
period_start: Date;
|
|
period_end: Date;
|
|
quantity_previous: number;
|
|
quantity_current: number;
|
|
quantity_total: number;
|
|
amount_current: number;
|
|
amount_total: number;
|
|
progress_percent: number;
|
|
photos: Photo[];
|
|
location?: Location;
|
|
observations: string;
|
|
created_by: string;
|
|
created_at: Date;
|
|
}
|
|
|
|
export interface CurveSData {
|
|
date: Date;
|
|
planned_cumulative: number;
|
|
actual_cumulative: number;
|
|
forecast_cumulative?: number;
|
|
}
|
|
|
|
export interface EVMMetrics {
|
|
pv: number;
|
|
ev: number;
|
|
ac: number;
|
|
sv: number;
|
|
cv: number;
|
|
spi: number;
|
|
cpi: number;
|
|
bac: number;
|
|
eac: number;
|
|
etc: number;
|
|
vac: number;
|
|
tcpi: number;
|
|
}
|
|
|
|
export interface ProgressFilters {
|
|
dateFrom?: Date;
|
|
dateTo?: Date;
|
|
conceptIds?: string[];
|
|
minProgress?: number;
|
|
maxProgress?: number;
|
|
}
|
|
```
|
|
|
|
### 12.2 Work Log Types
|
|
|
|
```typescript
|
|
export interface WorkLog {
|
|
id: string;
|
|
project_id: string;
|
|
date: Date;
|
|
weather: Weather;
|
|
workforce: WorkforceEntry[];
|
|
equipment: EquipmentEntry[];
|
|
activities: string;
|
|
incidents: string;
|
|
materials_received: MaterialEntry[];
|
|
visitors: Visitor[];
|
|
photos: Photo[];
|
|
created_by: string;
|
|
created_at: Date;
|
|
}
|
|
|
|
export interface Weather {
|
|
condition: 'sunny' | 'cloudy' | 'rainy' | 'stormy';
|
|
temperature: number;
|
|
humidity: number;
|
|
wind_speed?: number;
|
|
}
|
|
|
|
export interface WorkforceEntry {
|
|
category: string;
|
|
count: number;
|
|
}
|
|
|
|
export interface EquipmentEntry {
|
|
type: string;
|
|
hours: number;
|
|
}
|
|
|
|
export interface Photo {
|
|
id: string;
|
|
url: string;
|
|
thumbnail_url: string;
|
|
caption: string;
|
|
location?: Location;
|
|
tags: string[];
|
|
uploaded_at: Date;
|
|
}
|
|
```
|
|
|
|
### 12.3 Dashboard Types
|
|
|
|
```typescript
|
|
export interface DashboardKPIs {
|
|
progress: {
|
|
physical_percent: number;
|
|
financial_percent: number;
|
|
schedule_variance_days: number;
|
|
cost_variance_percent: number;
|
|
};
|
|
financial: {
|
|
total_budget: number;
|
|
spent_to_date: number;
|
|
committed: number;
|
|
available: number;
|
|
};
|
|
schedule: {
|
|
start_date: Date;
|
|
planned_end_date: Date;
|
|
forecast_end_date: Date;
|
|
days_remaining: number;
|
|
};
|
|
quality: {
|
|
inspections_passed: number;
|
|
inspections_failed: number;
|
|
pending_corrections: number;
|
|
};
|
|
}
|
|
|
|
export interface Alert {
|
|
id: string;
|
|
project_id: string;
|
|
severity: 'info' | 'warning' | 'critical';
|
|
category: string;
|
|
title: string;
|
|
description: string;
|
|
created_at: Date;
|
|
acknowledged: boolean;
|
|
acknowledged_by?: string;
|
|
acknowledged_at?: Date;
|
|
}
|
|
|
|
export interface RealtimeUpdate {
|
|
type: 'progress' | 'estimation' | 'alert' | 'kpi';
|
|
project_id: string;
|
|
data: any;
|
|
timestamp: Date;
|
|
}
|
|
```
|
|
|
|
## 13. Utilities
|
|
|
|
### 13.1 EVM Calculations
|
|
|
|
```typescript
|
|
// features/progress/utils/evmCalculations.ts
|
|
export const calculateEVM = (
|
|
bac: number,
|
|
plannedProgress: number,
|
|
actualProgress: number,
|
|
actualCost: number
|
|
): EVMMetrics => {
|
|
const pv = bac * (plannedProgress / 100);
|
|
const ev = bac * (actualProgress / 100);
|
|
const ac = actualCost;
|
|
|
|
const sv = ev - pv;
|
|
const cv = ev - ac;
|
|
|
|
const spi = pv > 0 ? ev / pv : 0;
|
|
const cpi = ac > 0 ? ev / ac : 0;
|
|
|
|
const eac = cpi > 0 ? bac / cpi : bac;
|
|
const etc = eac - ac;
|
|
const vac = bac - eac;
|
|
const tcpi = (bac - ev) / (bac - ac);
|
|
|
|
return {
|
|
pv,
|
|
ev,
|
|
ac,
|
|
sv,
|
|
cv,
|
|
spi,
|
|
cpi,
|
|
bac,
|
|
eac,
|
|
etc,
|
|
vac,
|
|
tcpi,
|
|
};
|
|
};
|
|
|
|
export const getPerformanceStatus = (index: number): PerformanceStatus => {
|
|
if (index >= 0.95 && index <= 1.05) return 'good';
|
|
if (index >= 0.85 && index < 0.95) return 'warning';
|
|
if (index > 1.05 && index <= 1.15) return 'warning';
|
|
return 'critical';
|
|
};
|
|
```
|
|
|
|
### 13.2 Formatters
|
|
|
|
```typescript
|
|
// shared/utils/formatters.ts
|
|
export const formatCurrency = (value: number): string => {
|
|
return new Intl.NumberFormat('es-MX', {
|
|
style: 'currency',
|
|
currency: 'MXN',
|
|
}).format(value);
|
|
};
|
|
|
|
export const formatPercent = (value: number, decimals: number = 1): string => {
|
|
return `${value.toFixed(decimals)}%`;
|
|
};
|
|
|
|
export const formatNumber = (value: number, decimals: number = 2): string => {
|
|
return new Intl.NumberFormat('es-MX', {
|
|
minimumFractionDigits: decimals,
|
|
maximumFractionDigits: decimals,
|
|
}).format(value);
|
|
};
|
|
|
|
export const formatDate = (date: Date, formatStr: string = 'dd/MM/yyyy'): string => {
|
|
return format(date, formatStr, { locale: es });
|
|
};
|
|
```
|
|
|
|
## 14. Performance Optimization
|
|
|
|
### 14.1 Code Splitting
|
|
|
|
```typescript
|
|
// Lazy loading de feature modules
|
|
const ProgressModule = lazy(() => import('./features/progress'));
|
|
const WorkLogModule = lazy(() => import('./features/work-log'));
|
|
const EstimationsModule = lazy(() => import('./features/estimations'));
|
|
const DashboardModule = lazy(() => import('./features/dashboard'));
|
|
|
|
// En router
|
|
<Suspense fallback={<LoadingSpinner />}>
|
|
<Routes>
|
|
<Route path="/progress/*" element={<ProgressModule />} />
|
|
<Route path="/work-log/*" element={<WorkLogModule />} />
|
|
<Route path="/estimations/*" element={<EstimationsModule />} />
|
|
<Route path="/dashboard/*" element={<DashboardModule />} />
|
|
</Routes>
|
|
</Suspense>
|
|
```
|
|
|
|
### 14.2 Image Optimization
|
|
|
|
```typescript
|
|
// Lazy loading de imágenes en PhotoGallery
|
|
import { LazyLoadImage } from 'react-lazy-load-image-component';
|
|
import 'react-lazy-load-image-component/src/effects/blur.css';
|
|
|
|
<LazyLoadImage
|
|
src={photo.url}
|
|
placeholderSrc={photo.thumbnail_url}
|
|
effect="blur"
|
|
alt={photo.caption}
|
|
/>
|
|
```
|
|
|
|
### 14.3 Chart Memoization
|
|
|
|
```typescript
|
|
// Memoizar configuraciones de charts
|
|
const chartConfig = useMemo(() => {
|
|
return curveSChartConfig(data);
|
|
}, [data]);
|
|
|
|
// Usar React.memo para componentes de charts
|
|
export const CurveSChart = React.memo<CurveSChartProps>(({ data }) => {
|
|
// Component implementation
|
|
});
|
|
```
|
|
|
|
## 15. Testing Strategy
|
|
|
|
### 15.1 Unit Tests
|
|
|
|
```typescript
|
|
// features/progress/components/__tests__/ProgressForm.test.tsx
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { ProgressForm } from '../ProgressForm';
|
|
|
|
describe('ProgressForm', () => {
|
|
it('should validate quantity does not exceed budget', async () => {
|
|
const onSubmit = jest.fn();
|
|
render(<ProgressForm projectId="1" onSubmit={onSubmit} />);
|
|
|
|
const quantityInput = screen.getByLabelText(/cantidad/i);
|
|
fireEvent.change(quantityInput, { target: { value: '1000' } });
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/excede la cantidad presupuestada/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 15.2 Integration Tests
|
|
|
|
```typescript
|
|
// features/progress/__tests__/progressFlow.test.tsx
|
|
describe('Progress Flow', () => {
|
|
it('should create progress entry and update curve S', async () => {
|
|
// Test complete flow
|
|
});
|
|
});
|
|
```
|
|
|
|
## 16. Build & Deployment
|
|
|
|
### 16.1 Vite Configuration
|
|
|
|
```typescript
|
|
// vite.config.ts
|
|
export default defineConfig({
|
|
plugins: [react()],
|
|
build: {
|
|
rollupOptions: {
|
|
output: {
|
|
manualChunks: {
|
|
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
|
|
'charts': ['chart.js', 'react-chartjs-2'],
|
|
'maps': ['leaflet', 'react-leaflet'],
|
|
'forms': ['react-hook-form', 'zod'],
|
|
},
|
|
},
|
|
},
|
|
chunkSizeWarningLimit: 1000,
|
|
},
|
|
server: {
|
|
proxy: {
|
|
'/api': 'http://localhost:3000',
|
|
'/socket.io': {
|
|
target: 'http://localhost:3000',
|
|
ws: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
### 16.2 Environment Variables
|
|
|
|
```bash
|
|
# .env.example
|
|
VITE_API_URL=http://localhost:3000/api
|
|
VITE_WS_URL=http://localhost:3000
|
|
VITE_UPLOAD_MAX_SIZE=10485760
|
|
VITE_MAP_TILE_URL=https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
|
|
```
|
|
|
|
## 17. Notas de Implementación
|
|
|
|
### 17.1 Prioridades
|
|
|
|
1. **Fase 1**: Progress Module + CurveSChart
|
|
2. **Fase 2**: WorkLog Module + PhotoGallery
|
|
3. **Fase 3**: Estimations Module
|
|
4. **Fase 4**: Dashboard + WebSocket real-time
|
|
5. **Fase 5**: Optimizaciones y refinamiento
|
|
|
|
### 17.2 Consideraciones
|
|
|
|
- **Performance**: Implementar virtualización en tablas largas
|
|
- **Offline**: Considerar service workers para modo offline
|
|
- **Mobile**: Diseño responsive con breakpoints Tailwind
|
|
- **Accessibility**: Cumplir WCAG 2.1 AA
|
|
- **Security**: Sanitizar uploads, validar file types
|
|
- **UX**: Loading states, error boundaries, toast notifications
|
|
|
|
### 17.3 Dependencias con Backend
|
|
|
|
- API endpoints definidos en ET-OBRA-002-backend.md
|
|
- WebSocket events documentados
|
|
- Estructura de datos alineada
|
|
- Autenticación JWT con refresh tokens
|
|
|
|
## 18. Referencias
|
|
|
|
- [React 18 Docs](https://react.dev)
|
|
- [Chart.js Documentation](https://www.chartjs.org/docs/)
|
|
- [Leaflet Documentation](https://leafletjs.com/reference.html)
|
|
- [Socket.io Client API](https://socket.io/docs/v4/client-api/)
|
|
- [Zustand Guide](https://docs.pmnd.rs/zustand)
|
|
|
|
---
|
|
|
|
**Documento**: ET-OBRA-001-frontend.md
|
|
**Versión**: 1.0.0
|
|
**Última actualización**: 2025-12-06
|