# ET-CAL-001: Implementación Frontend - Módulo de Calidad **Épica:** MAI-006 - Calidad **Módulo:** Frontend - Gestión de Calidad **Responsable Técnico:** Equipo Frontend **Fecha:** 2025-12-06 **Versión:** 1.0 --- ## 1. Objetivo Técnico Implementar la interfaz de usuario completa para el módulo de Calidad del sistema de construcción, permitiendo: - Gestión de inspecciones de calidad - Registro y seguimiento de no conformidades (NC) - Gestión de pruebas de laboratorio - Control de certificaciones - Dashboard ejecutivo con KPIs de calidad - Workflows de aprobación y cierre --- ## 2. Stack Tecnológico ### Frontend ```typescript - React 18+ con TypeScript - Vite 5+ como bundler - Zustand para state management - React Query (TanStack Query) para data fetching - React Router 6+ para navegación - Tailwind CSS para estilos - Shadcn/ui para componentes base - React Hook Form + Zod para formularios - Recharts para gráficos - date-fns para manejo de fechas ``` ### Integración ```typescript - Axios para HTTP client - React Dropzone para upload de archivos - React Signature Canvas para firmas digitales ``` --- ## 3. Estructura de Feature Modules ``` src/features/quality/ ├── inspections/ │ ├── components/ │ │ ├── InspectionChecklist.tsx │ │ ├── InspectionForm.tsx │ │ ├── InspectionList.tsx │ │ ├── InspectionDetail.tsx │ │ ├── ChecklistItem.tsx │ │ └── SignaturePanel.tsx │ ├── hooks/ │ │ ├── useInspections.ts │ │ ├── useCreateInspection.ts │ │ └── useSignInspection.ts │ ├── api/ │ │ └── inspectionApi.ts │ └── types/ │ └── inspection.types.ts │ ├── non-conformities/ │ ├── components/ │ │ ├── NCForm.tsx │ │ ├── NCTimeline.tsx │ │ ├── NCList.tsx │ │ ├── NCDetail.tsx │ │ ├── NCStatusBadge.tsx │ │ └── CorrectiveActionForm.tsx │ ├── hooks/ │ │ ├── useNonConformities.ts │ │ ├── useCreateNC.ts │ │ └── useCloseNC.ts │ ├── api/ │ │ └── ncApi.ts │ └── types/ │ └── nc.types.ts │ ├── lab-tests/ │ ├── components/ │ │ ├── LabTestForm.tsx │ │ ├── LabTestResults.tsx │ │ ├── LabTestList.tsx │ │ ├── TestResultsChart.tsx │ │ └── ComplianceIndicator.tsx │ ├── hooks/ │ │ ├── useLabTests.ts │ │ └── useTestResults.ts │ ├── api/ │ │ └── labTestApi.ts │ └── types/ │ └── labTest.types.ts │ ├── certifications/ │ ├── components/ │ │ ├── CertificationCard.tsx │ │ ├── CertificationForm.tsx │ │ ├── CertificationList.tsx │ │ ├── CertificationDetail.tsx │ │ └── ExpiryAlert.tsx │ ├── hooks/ │ │ ├── useCertifications.ts │ │ └── useExpiringCertifications.ts │ ├── api/ │ │ └── certificationApi.ts │ └── types/ │ └── certification.types.ts │ ├── dashboard/ │ ├── components/ │ │ ├── QualityDashboard.tsx │ │ ├── QualityKPIs.tsx │ │ ├── ComplianceChart.tsx │ │ ├── NCTrendChart.tsx │ │ ├── InspectionStatusChart.tsx │ │ └── RecentActivity.tsx │ ├── hooks/ │ │ └── useQualityMetrics.ts │ └── api/ │ └── dashboardApi.ts │ └── stores/ ├── inspectionStore.ts ├── ncStore.ts ├── labTestStore.ts └── qualityFiltersStore.ts ``` --- ## 4. Componentes Principales ### 4.1 InspectionChecklist Componente para realizar inspecciones de calidad con checklist dinámico. ```tsx // src/features/quality/inspections/components/InspectionChecklist.tsx import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { ChecklistItem } from './ChecklistItem'; import { SignaturePanel } from './SignaturePanel'; import { useCreateInspection } from '../hooks/useCreateInspection'; import { InspectionTemplate, ChecklistItemData } from '../types/inspection.types'; interface InspectionChecklistProps { projectId: string; template: InspectionTemplate; unitId?: string; onComplete?: (inspectionId: string) => void; } const inspectionSchema = z.object({ items: z.array(z.object({ itemId: z.string(), isCompliant: z.boolean().nullable(), value: z.string().optional(), measurement: z.number().optional(), notes: z.string().optional(), photoIds: z.array(z.string()).default([]), })), generalNotes: z.string().optional(), signatureData: z.string().optional(), }); type InspectionFormData = z.infer; export function InspectionChecklist({ projectId, template, unitId, onComplete, }: InspectionChecklistProps) { const [currentSection, setCurrentSection] = useState(0); const [showSignature, setShowSignature] = useState(false); const { mutate: createInspection, isPending } = useCreateInspection(); const { register, handleSubmit, watch, setValue, formState: { errors }, } = useForm({ resolver: zodResolver(inspectionSchema), defaultValues: { items: template.items.map(item => ({ itemId: item.itemId, isCompliant: null, notes: '', photoIds: [], })), }, }); const sections = [...new Set(template.items.map(item => item.section))]; const currentItems = template.items.filter( item => item.section === sections[currentSection] ); const handleItemChange = ( itemId: string, field: keyof ChecklistItemData, value: any ) => { const items = watch('items'); const index = items.findIndex(i => i.itemId === itemId); if (index !== -1) { setValue(`items.${index}.${field}`, value); } }; const onSubmit = (data: InspectionFormData) => { // Calcular compliance const compliantItems = data.items.filter( item => item.isCompliant === true ).length; // Detectar no conformidades const nonConformities = data.items .filter(item => item.isCompliant === false) .map((item, index) => { const templateItem = template.items.find(t => t.itemId === item.itemId); return { ncId: `NC-${Date.now()}-${index}`, itemId: item.itemId, description: `${templateItem?.question}: ${item.notes || 'No conforme'}`, severity: 'major' as const, status: 'open' as const, }; }); createInspection({ projectId, unitId, templateId: template.id, inspectionDate: new Date(), items: data.items, totalItems: template.items.length, compliantItems, nonConformities, generalNotes: data.generalNotes, signatureData: data.signatureData, }, { onSuccess: (inspection) => { onComplete?.(inspection.id); }, }); }; const handleNextSection = () => { if (currentSection < sections.length - 1) { setCurrentSection(prev => prev + 1); } else { setShowSignature(true); } }; const handlePreviousSection = () => { if (currentSection > 0) { setCurrentSection(prev => prev - 1); } }; const progress = ((currentSection + 1) / sections.length) * 100; return (
{/* Header */}

{template.name}

{template.description}

{/* Progress Bar */}
Sección {currentSection + 1} de {sections.length} {Math.round(progress)}% completado
{!showSignature ? ( <> {/* Current Section */}

{sections[currentSection]}

{currentItems.map((item, index) => ( i.itemId === item.itemId)} onChange={(field, value) => handleItemChange(item.itemId, field, value)} /> ))}
{/* Navigation */}
) : ( <> {/* Signature Section */}

Firma de Inspección