# 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; 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; } 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; } 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) => 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; addProgressEntry: (entry: ProgressEntry) => Promise; updateProgressEntry: (id: string, data: Partial) => Promise; loadCurveData: (projectId: string) => Promise; loadEVMMetrics: (projectId: string) => Promise; setFilters: (filters: Partial) => void; clearFilters: () => void; } const useProgressStore = create((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; subscribeToUpdates: (projectId?: string) => void; unsubscribeFromUpdates: () => void; addRealtimeUpdate: (update: RealtimeUpdate) => void; acknowledgeAlert: (alertId: string) => Promise; } ``` ## 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(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: '© OpenStreetMap', }, }; export const getMarkerIcon = (status: ProjectStatus) => { const color = { active: '#22c55e', warning: '#f59e0b', critical: '#ef4444', completed: '#6366f1', }[status]; return new L.DivIcon({ html: `
`, 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 = ({ projects, selectedProject, onProjectSelect, showProgress = true, showAlerts = true, }) => { return ( {projects.map((project) => ( onProjectSelect(project.id), }} >

{project.name}

{showProgress && (

Avance

{project.progress_percent}%

)} {showAlerts && project.alerts_count > 0 && (
{project.alerts_count} alertas
)}
))} ); }; ``` ## 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( `${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( `${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( `${this.baseUrl}/projects/${projectId}/curve-s?${params}` ); return response.data; } async getEVMMetrics(projectId: string) { const response = await axios.get( `${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(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( `${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 }> } /> } /> } /> } /> ``` ### 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'; ``` ### 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(({ 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(); 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