From ce4f3aff8c277eed8cf605c1d0b478f38b03cb58 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Sun, 11 Jan 2026 17:03:46 +0100 Subject: [PATCH] Add equipment fail feature --- Lista_de_Mejoras_Propuestas.md | 185 ++++++++++ frontend/src/api/services/equipment.ts | 78 +++- frontend/src/api/types/equipment.ts | 21 ++ frontend/src/api/types/events.ts | 87 ++--- .../domain/equipment/MarkAsRepairedModal.tsx | 335 ++++++++++++++++++ .../domain/equipment/ReportFailureModal.tsx | 275 ++++++++++++++ frontend/src/components/ui/FileUpload.tsx | 143 ++++++++ frontend/src/components/ui/index.ts | 1 + frontend/src/locales/en/equipment.json | 35 ++ frontend/src/locales/es/equipment.json | 35 ++ frontend/src/locales/eu/equipment.json | 35 ++ .../operations/maquinaria/MaquinariaPage.tsx | 123 +++++++ .../templates/equipment_failure_email.html | 165 +++++++++ .../templates/equipment_repaired_email.html | 159 +++++++++ services/production/app/api/equipment.py | 301 ++++++++++++++++ services/production/app/models/production.py | 4 + services/production/app/schemas/equipment.py | 21 ++ .../app/services/production_service.py | 148 +++++++- .../versions/001_unified_initial_schema.py | 1 + 19 files changed, 2101 insertions(+), 51 deletions(-) create mode 100644 Lista_de_Mejoras_Propuestas.md create mode 100644 frontend/src/components/domain/equipment/MarkAsRepairedModal.tsx create mode 100644 frontend/src/components/domain/equipment/ReportFailureModal.tsx create mode 100644 frontend/src/components/ui/FileUpload.tsx create mode 100644 services/notification/app/templates/equipment_failure_email.html create mode 100644 services/notification/app/templates/equipment_repaired_email.html diff --git a/Lista_de_Mejoras_Propuestas.md b/Lista_de_Mejoras_Propuestas.md new file mode 100644 index 00000000..f7a3b350 --- /dev/null +++ b/Lista_de_Mejoras_Propuestas.md @@ -0,0 +1,185 @@ +# Lista de Mejoras Propuestas + +## 1. Nueva Sección: Seguridad y Ciberseguridad (Añadir después de 5.2) + +### 5.3. Arquitectura de Seguridad y Cumplimiento Normativo Europeo + +**Autenticación y Autorización:** +- JWT con rotación de tokens cada 15 minutos +- Control de acceso basado en roles (RBAC) +- Rate limiting (300 req/min por cliente) +- Autenticación multifactor (MFA) planificada para Q1 2026 + +**Protección de Datos:** +- Cifrado AES-256 en reposo +- HTTPS obligatorio con certificados Let's Encrypt auto-renovables +- Aislamiento multi-tenant a nivel de base de datos +- Prevención SQL injection mediante Pydantic schemas +- Protección XSS y CORS + +**Monitorización y Trazabilidad:** +- OpenTelemetry para trazabilidad distribuida end-to-end +- SigNoz como plataforma unificada de observabilidad +- Logs centralizados con correlación de trazas +- Auditoría completa de accesos y cambios +- Alertas en tiempo real (email y Slack) + +**Cumplimiento RGPD:** +- Privacy by design +- Derecho al olvido implementado +- Exportación de datos en CSV/JSON +- Gestión de consentimientos con historial +- Anonimización de datos analíticos + +**Infraestructura Segura:** +- Kubernetes con políticas de seguridad de pods +- Actualizaciones automáticas de seguridad +- Backups cifrados diarios +- Plan de recuperación ante desastres (DR) +- PostgreSQL 17 con configuraciones hardened + +## 2. Actualizar Sección 4.4 (Competencia) - Añadir Ventaja de Seguridad + +**Ventaja Competitiva en Seguridad:** +- Arquitectura "Security-First" vs. soluciones legacy vulnerables +- Cumplimiento RGPD desde el diseño (competidores retrofitting) +- Observabilidad completa (OpenTelemetry + SigNoz) vs. cajas negras +- Certificaciones de seguridad planificadas (ISO 27001, ENS) +- Alineación con NIS2 Directive (obligatoria 2024 para cadena alimentaria) + +## 3. Actualizar Sección 8.1 (Financiación) - Nuevas Líneas de Ayuda + +### Añadir subsección: "Financiación Europea en Ciberseguridad 2026-2027" + +**Programas Europeos Identificados (2026-2027):** + +1. **Digital Europe Programme - Ciberseguridad (€390M totales)** + - **UPTAKE Program**: €15M para SMEs en cumplimiento normativo + - **AI-Based Cybersecurity**: €15M para sistemas IA de seguridad + - Cofinanciación: hasta 75% de costes de proyecto + - Proyectos: €3-5M por proyecto, duración 36 meses + - **Elegibilidad**: Bakery-IA califica como SME tecnológica + - **Solicitud estimada**: €200.000 (Q1 2026) + +2. **INCIBE EMPRENDE Program (España Digital 2026)** + - Presupuesto: €191M (2023-2026) + - 34 entidades colaboradoras en España + - Ideación, incubación y aceleración en ciberseguridad + - Fondos Next Generation-EU + - **Solicitud estimada**: €50.000 (Q2 2026) + +3. **ENISA Emprendedoras Digitales** + - Préstamos participativos hasta €51M movilizables + - Líneas específicas para emprendimiento digital + - Sin avales personales, condiciones flexibles + - **Solicitud estimada**: €75.000 (Q2 2026) + +**Alineación con Prioridades UE:** +- NIS2 Directive (seguridad sector alimentario) +- Cyber Resilience Act (productos digitales seguros) +- AI Act (IA transparente y auditable) +- GDPR (protección datos desde diseño) + +## 4. Actualizar Sección 5.2 (Arquitectura Técnica) + +### Añadir detalles de la documentación técnica: + +**Arquitectura de Microservicios (21 servicios independientes):** +- API Gateway centralizado con JWT y cache Redis (95% hit rate) +- Frontend: React 18 + TypeScript (PWA mobile-first) +- 18 bases de datos PostgreSQL 17 (patrón database-per-service) +- Redis 7.4 para caché y RabbitMQ 4.1 para eventos +- Kubernetes en VPS (escalabilidad horizontal) + +**Innovación Técnica Destacable:** +- **Sistema de Alertas Enriquecidas** (3 niveles: Alertas/Notificaciones/Recomendaciones) +- **Priorización Inteligente** con scoring 0-100 (4 factores ponderados) +- **Escalado Temporal** (+10 a 48h, +20 a 72h, +30 cerca deadline) +- **Encadenamiento Causal** (stock shortage → retraso producción → riesgo pedido) +- **Deduplicación** (95% reducción spam de alertas) +- **SSE + WebSocket** para actualizaciones en tiempo real +- **Prophet ML** con 20+ features (AEMET, tráfico Madrid, festivos) + +**Observabilidad de Clase Empresarial:** +- **SigNoz**: Trazas, métricas y logs unificados +- **OpenTelemetry**: Auto-instrumentación de 18 servicios +- **ClickHouse**: Backend de alto rendimiento para análisis +- **Alerting**: Multi-canal (email, Slack) vía AlertManager +- Monitorización: 18 DBs PostgreSQL, Redis, RabbitMQ, Kubernetes + +## 5. Actualizar Resumen Ejecutivo (Sección 0) + +### Añadir bullet: + +- **Seguridad y Cumplimiento:** Arquitectura Security-First con cumplimiento RGPD, observabilidad completa (OpenTelemetry/SigNoz), y alineación con normativas europeas (NIS2, Cyber Resilience Act). Elegible para €390M del Digital Europe Programme en ciberseguridad. + +## 6. Actualizar Sección 9 (Decálogo) - Añadir Oportunidades de Financiación + +**6. Alineación Estratégica con Prioridades Europeas 2026-2027:** + - **€390M disponibles** en Digital Europe Programme para ciberseguridad + - **€191M** del programa INCIBE EMPRENDE (España Digital 2026) + - Bakery-IA califica para **3 líneas de financiación simultáneas**: + * UPTAKE (€200K) - Cumplimiento normativo SMEs + * INCIBE EMPRENDE (€50K) - Aceleración cybersecurity startups + * ENISA Digital (€75K) - Préstamo participativo + - **Total potencial**: €325.000 en financiación no dilutiva adicional + - Ventaja competitiva: Security-First vs. competidores legacy + +## 7. Nueva Tabla de Costes Operativos - Añadir Línea de Seguridad + +| **Seguridad y Cumplimiento** | | | +| Certificado SSL (Let's Encrypt) | Gratuito | €0 | Renovación automática | +| SigNoz Observability (self-hosted) | Incluido en VPS | €0 | Vs. €500+/mes en SaaS | +| Auditoría RGPD anual | Externa | €1,200/año | Compliance obligatorio | +| Backups cifrados (off-site) | Backblaze B2 | €5/mes | €60/año | + +## 8. Actualizar Roadmap (Sección 9) - Añadir Hitos de Seguridad + +**Q1 2026:** +- ✅ Implementación MFA (Multi-Factor Authentication) +- ✅ Solicitud Digital Europe Programme (UPTAKE) +- ✅ Auditoría RGPD externa + +**Q2 2026:** +- 📋 Certificación ISO 27001 (inicio proceso) +- 📋 Implementación NIS2 compliance +- 📋 Solicitud INCIBE EMPRENDE + +**Q3 2026:** +- 📋 Penetration testing externo +- 📋 Certificación ENS (Esquema Nacional de Seguridad) + +## 9. Actualizar Petición Concreta a VUE (Sección 9.2) + +### Añadir nuevo punto 5: + +**5. Conexión con Programas Europeos de Ciberseguridad:** + - Orientación para solicitud Digital Europe Programme (UPTAKE: €200K) + - Introducción a INCIBE EMPRENDE y red de 34 entidades colaboradoras + - Asesoramiento en preparación de propuestas técnicas para fondos EU + - Contacto con CDTI para programa NEOTEC (R&D+Ciberseguridad) + +## 10. Añadir Anexo 7 - Compliance y Certificaciones + +## ANEXO 7: ROADMAP DE COMPLIANCE Y CERTIFICACIONES + +**Normativas Aplicables:** +- ✅ RGPD (Reglamento General de Protección de Datos) - Implementado +- 📋 NIS2 Directive (Seguridad de redes y sistemas de información) - Q2 2026 +- 📋 Cyber Resilience Act (Productos digitales seguros) - Q3 2026 +- 📋 AI Act (Transparencia y auditoría de IA) - Q4 2026 + +**Certificaciones Planificadas:** +- 📋 ISO 27001 (Gestión de Seguridad de la Información) - 12-18 meses +- 📋 ENS Medio (Esquema Nacional de Seguridad) - 6-9 meses +- 📋 SOC 2 Type II (para clientes Enterprise) - 18-24 meses + +**Inversión Estimada en Compliance (3 años):** €25,000 +**ROI Esperado:** Acceso a clientes Enterprise (+€150K ARR potencial) + +## Resumen de Cambios Cuantitativos: +- Nueva financiación identificada: €325.000 (vs. €18.000 original) +- Nuevos programas: 3 líneas europeas de ciberseguridad +- Secciones nuevas: 2 (Seguridad 5.3, Compliance Anexo 7) +- Actualizaciones: 8 secciones existentes mejoradas +- Ventaja competitiva: Security-First enfatizada en 4 lugares \ No newline at end of file diff --git a/frontend/src/api/services/equipment.ts b/frontend/src/api/services/equipment.ts index d001d1dd..5b96b036 100644 --- a/frontend/src/api/services/equipment.ts +++ b/frontend/src/api/services/equipment.ts @@ -28,7 +28,7 @@ class EquipmentService { model: response.model || '', serialNumber: response.serial_number || '', location: response.location || '', - status: response.status, + status: response.status.toLowerCase() as Equipment['status'], installDate: response.install_date || new Date().toISOString().split('T')[0], lastMaintenance: response.last_maintenance_date || new Date().toISOString().split('T')[0], nextMaintenance: response.next_maintenance_date || new Date().toISOString().split('T')[0], @@ -52,6 +52,7 @@ class EquipmentService { weight: response.weight_kg || 0 }, is_active: response.is_active, + support_contact: response.support_contact || undefined, created_at: response.created_at, updated_at: response.updated_at }; @@ -80,7 +81,8 @@ class EquipmentService { weight_kg: equipment.specifications?.weight, current_temperature: equipment.temperature, target_temperature: equipment.targetTemperature, - is_active: equipment.is_active + is_active: equipment.is_active, + support_contact: equipment.support_contact }; } @@ -202,6 +204,78 @@ class EquipmentService { ); return data; } + + /** + * Report equipment failure + */ + async reportEquipmentFailure( + tenantId: string, + equipmentId: string, + failureData: { + failureType: string; + severity: string; + description: string; + photos?: File[]; + estimatedImpact: boolean; + } + ): Promise { + const apiData = { + failureType: failureData.failureType, + severity: failureData.severity, + description: failureData.description, + estimatedImpact: failureData.estimatedImpact, + // Note: Photos would be handled separately in a real implementation + // For now, we'll just send the metadata + photos: failureData.photos ? failureData.photos.map(p => p.name) : [] + }; + + const data: EquipmentResponse = await apiClient.post( + `${this.baseURL}/${tenantId}/production/equipment/${equipmentId}/report-failure`, + apiData, + { + headers: { 'X-Tenant-ID': tenantId } + } + ); + return this.convertToEquipment(data); + } + + /** + * Mark equipment as repaired + */ + async markEquipmentAsRepaired( + tenantId: string, + equipmentId: string, + repairData: { + repairDate: string; + technicianName: string; + repairDescription: string; + partsReplaced: string[]; + cost: number; + photos?: File[]; + testResults: boolean; + } + ): Promise { + const apiData = { + repairDate: repairData.repairDate, + technicianName: repairData.technicianName, + repairDescription: repairData.repairDescription, + partsReplaced: repairData.partsReplaced, + cost: repairData.cost, + testResults: repairData.testResults, + // Note: Photos would be handled separately in a real implementation + // For now, we'll just send the metadata + photos: repairData.photos ? repairData.photos.map(p => p.name) : [] + }; + + const data: EquipmentResponse = await apiClient.post( + `${this.baseURL}/${tenantId}/production/equipment/${equipmentId}/mark-repaired`, + apiData, + { + headers: { 'X-Tenant-ID': tenantId } + } + ); + return this.convertToEquipment(data); + } } export const equipmentService = new EquipmentService(); diff --git a/frontend/src/api/types/equipment.ts b/frontend/src/api/types/equipment.ts index 5c2ecc65..d582be11 100644 --- a/frontend/src/api/types/equipment.ts +++ b/frontend/src/api/types/equipment.ts @@ -53,6 +53,13 @@ export interface Equipment { maintenanceHistory: MaintenanceHistory[]; specifications: EquipmentSpecifications; is_active?: boolean; + support_contact?: { + email?: string; + phone?: string; + company?: string; + contract_number?: string; + response_time_sla?: number; + }; created_at?: string; updated_at?: string; } @@ -81,6 +88,13 @@ export interface EquipmentCreate { current_temperature?: number; target_temperature?: number; notes?: string; + support_contact?: { + email?: string; + phone?: string; + company?: string; + contract_number?: string; + response_time_sla?: number; + }; } export interface EquipmentUpdate { @@ -129,6 +143,13 @@ export interface EquipmentResponse { target_temperature: number | null; is_active: boolean; notes: string | null; + support_contact: { + email?: string; + phone?: string; + company?: string; + contract_number?: string; + response_time_sla?: number; + } | null; created_at: string; updated_at: string; } diff --git a/frontend/src/api/types/events.ts b/frontend/src/api/types/events.ts index 802c191f..4740040c 100644 --- a/frontend/src/api/types/events.ts +++ b/frontend/src/api/types/events.ts @@ -349,64 +349,55 @@ export function isRecommendation(event: EventResponse | Event): event is Recomme // ============================================================ export function getPriorityColor(level: PriorityLevel | string): string { - switch (level) { - case PriorityLevel.CRITICAL: - case 'critical': - return 'var(--color-error)'; - case PriorityLevel.IMPORTANT: - case 'important': - return 'var(--color-warning)'; - case PriorityLevel.STANDARD: - case 'standard': - return 'var(--color-info)'; - case PriorityLevel.INFO: - case 'info': - return 'var(--color-success)'; - default: - return 'var(--color-info)'; + const levelValue = String(level); + + if (levelValue === PriorityLevel.CRITICAL || levelValue === 'critical') { + return 'var(--color-error)'; + } else if (levelValue === PriorityLevel.IMPORTANT || levelValue === 'important') { + return 'var(--color-warning)'; + } else if (levelValue === PriorityLevel.STANDARD || levelValue === 'standard') { + return 'var(--color-info)'; + } else if (levelValue === PriorityLevel.INFO || levelValue === 'info') { + return 'var(--color-success)'; + } else { + return 'var(--color-info)'; } } export function getPriorityIcon(level: PriorityLevel | string): string { - switch (level) { - case PriorityLevel.CRITICAL: - case 'critical': - return 'alert-triangle'; - case PriorityLevel.IMPORTANT: - case 'important': - return 'alert-circle'; - case PriorityLevel.STANDARD: - case 'standard': - return 'info'; - case PriorityLevel.INFO: - case 'info': - return 'check-circle'; - default: - return 'info'; + const levelValue = String(level); + + if (levelValue === PriorityLevel.CRITICAL || levelValue === 'critical') { + return 'alert-triangle'; + } else if (levelValue === PriorityLevel.IMPORTANT || levelValue === 'important') { + return 'alert-circle'; + } else if (levelValue === PriorityLevel.STANDARD || levelValue === 'standard') { + return 'info'; + } else if (levelValue === PriorityLevel.INFO || levelValue === 'info') { + return 'check-circle'; + } else { + return 'info'; } } export function getTypeClassBadgeVariant( typeClass: AlertTypeClass | string ): 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'outline' { - switch (typeClass) { - case AlertTypeClass.ACTION_NEEDED: - case 'action_needed': - return 'error'; - case AlertTypeClass.PREVENTED_ISSUE: - case 'prevented_issue': - return 'success'; - case AlertTypeClass.TREND_WARNING: - case 'trend_warning': - return 'warning'; - case AlertTypeClass.ESCALATION: - case 'escalation': - return 'error'; - case AlertTypeClass.INFORMATION: - case 'information': - return 'info'; - default: - return 'default'; + // Convert to string and compare with known values + const typeValue = String(typeClass); + + if (typeValue === AlertTypeClass.ACTION_NEEDED || typeValue === 'action_needed') { + return 'error'; + } else if (typeValue === AlertTypeClass.PREVENTED_ISSUE || typeValue === 'prevented_issue') { + return 'success'; + } else if (typeValue === AlertTypeClass.TREND_WARNING || typeValue === 'trend_warning') { + return 'warning'; + } else if (typeValue === AlertTypeClass.ESCALATION || typeValue === 'escalation') { + return 'error'; + } else if (typeValue === AlertTypeClass.INFORMATION || typeValue === 'information') { + return 'info'; + } else { + return 'default'; } } diff --git a/frontend/src/components/domain/equipment/MarkAsRepairedModal.tsx b/frontend/src/components/domain/equipment/MarkAsRepairedModal.tsx new file mode 100644 index 00000000..3c89f062 --- /dev/null +++ b/frontend/src/components/domain/equipment/MarkAsRepairedModal.tsx @@ -0,0 +1,335 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CheckCircle, Clock, FileText, HelpCircle, User, Wrench } from 'lucide-react'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button, Input, Textarea, Select, SelectItem, FileUpload, Alert, type SelectOption } from '../../../components/ui'; +import { Equipment } from '../../../api/types/equipment'; +import { useToast } from '../../../hooks/useToast'; + +interface MarkAsRepairedModalProps { + isOpen: boolean; + onClose: () => void; + equipment: Equipment; + onMarkAsRepaired: (repairData: { + repairDate: string; + technicianName: string; + repairDescription: string; + partsReplaced: string[]; + cost: number; + photos?: File[]; + testResults: boolean; + }) => Promise; + isLoading?: boolean; +} + +export const MarkAsRepairedModal: React.FC = ({ + isOpen, + onClose, + equipment, + onMarkAsRepaired, + isLoading = false +}) => { + const { t } = useTranslation(['equipment', 'common']); + const { showToast } = useToast(); + + const [repairDate, setRepairDate] = useState(new Date().toISOString().split('T')[0]); + const [technicianName, setTechnicianName] = useState(''); + const [repairDescription, setRepairDescription] = useState(''); + const [partsReplaced, setPartsReplaced] = useState([]); + const [newPart, setNewPart] = useState(''); + const [cost, setCost] = useState(0); + const [photos, setPhotos] = useState([]); + const [testResults, setTestResults] = useState(true); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!repairDescription.trim()) { + setError(t('errors.repair_description_required') || 'Repair description is required'); + return; + } + + if (!technicianName.trim()) { + setError(t('errors.technician_required') || 'Technician name is required'); + return; + } + + try { + await onMarkAsRepaired({ + repairDate, + technicianName, + repairDescription, + partsReplaced, + cost, + photos, + testResults + }); + + showToast({ + title: t('success.repair_completed') || 'Repair Completed', + message: t('success.repair_completed_message', { equipment: equipment.name }) || `${equipment.name} has been marked as repaired successfully`, + type: 'success' + }); + + // Reset form + setRepairDescription(''); + setTechnicianName(''); + setPartsReplaced([]); + setNewPart(''); + setCost(0); + setPhotos([]); + setTestResults(true); + onClose(); + } catch (err) { + setError(err instanceof Error ? err.message : t('errors.repair_failed') || 'Failed to mark as repaired'); + showToast({ + title: t('errors.repair_failed') || 'Repair Failed', + message: err instanceof Error ? err.message : t('errors.try_again') || 'Please try again', + type: 'error' + }); + } + }; + + const handleAddPart = () => { + if (newPart.trim() && !partsReplaced.includes(newPart.trim())) { + setPartsReplaced([...partsReplaced, newPart.trim()]); + setNewPart(''); + } + }; + + const handleRemovePart = (part: string) => { + setPartsReplaced(partsReplaced.filter(p => p !== part)); + }; + + const handlePhotoUpload = (files: File[]) => { + setPhotos(files); + }; + + // Calculate downtime + const calculateDowntime = () => { + if (!equipment.lastMaintenance) return 0; + + const lastMaintenanceDate = new Date(equipment.lastMaintenance); + const repairDateObj = new Date(repairDate); + + const diffHours = Math.abs(repairDateObj.getTime() - lastMaintenanceDate.getTime()) / (1000 * 60 * 60); + return Math.round(diffHours); + }; + + return ( + +
+ +
+ +

+ {t('mark_repaired.title', { equipment: equipment.name }) || `Mark as Repaired: ${equipment.name}`} +

+
+
+ + + {error && ( + + {error} + + )} + +
+ {/* Equipment Info */} +
+

+ + {t('equipment_info.title') || 'Equipment Information'} +

+
+
+ {t('fields.type') || 'Type'}: + {equipment.type} +
+
+ {t('fields.model') || 'Model'}: + {equipment.model || 'N/A'} +
+
+ {t('fields.location') || 'Location'}: + {equipment.location || 'N/A'} +
+
+ {t('fields.serial_number') || 'Serial'}: + {equipment.serialNumber || 'N/A'} +
+
+
+ + {/* Repair Date */} +
+ + setRepairDate(e.target.value)} + required + /> +
+ + {/* Technician Name */} +
+ + setTechnicianName(e.target.value)} + placeholder={t('placeholders.technician_name') || 'Enter technician name'} + required + /> +
+ + {/* Repair Description */} +
+ +