# 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: '&copy; <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
