Add improved production UI 3

This commit is contained in:
Urtzi Alfaro
2025-09-23 19:24:22 +02:00
parent 7f871fc933
commit 7892c5a739
47 changed files with 6211 additions and 267 deletions

View File

@@ -49,14 +49,14 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
max_instances=1
)
# Equipment monitoring - disabled (equipment tables not available in production database)
# self.scheduler.add_job(
# self.check_equipment_status,
# CronTrigger(minute='*/3'),
# id='equipment_check',
# misfire_grace_time=30,
# max_instances=1
# )
# Equipment monitoring - check equipment status for maintenance alerts
self.scheduler.add_job(
self.check_equipment_status,
CronTrigger(minute='*/30'), # Check every 30 minutes
id='equipment_check',
misfire_grace_time=30,
max_instances=1
)
# Efficiency recommendations - every 30 minutes (recommendations)
self.scheduler.add_job(
@@ -394,19 +394,61 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
error=str(e))
async def check_equipment_status(self):
"""Check equipment status and failures (alerts)"""
# Equipment tables don't exist in production database - skip this check
logger.debug("Equipment check skipped - equipment tables not available in production database")
return
"""Check equipment status and maintenance requirements (alerts)"""
try:
self._checks_performed += 1
# Query equipment that needs attention
query = """
SELECT
e.id, e.tenant_id, e.name, e.type, e.status,
e.efficiency_percentage, e.uptime_percentage,
e.last_maintenance_date, e.next_maintenance_date,
e.maintenance_interval_days,
EXTRACT(DAYS FROM (e.next_maintenance_date - NOW())) as days_to_maintenance,
COUNT(ea.id) as active_alerts
FROM equipment e
LEFT JOIN alerts ea ON ea.equipment_id = e.id
AND ea.is_active = true
AND ea.is_resolved = false
WHERE e.is_active = true
AND e.tenant_id = $1
GROUP BY e.id, e.tenant_id, e.name, e.type, e.status,
e.efficiency_percentage, e.uptime_percentage,
e.last_maintenance_date, e.next_maintenance_date,
e.maintenance_interval_days
ORDER BY e.next_maintenance_date ASC
"""
tenants = await self.get_active_tenants()
for tenant_id in tenants:
try:
from sqlalchemy import text
async with self.db_manager.get_session() as session:
result = await session.execute(text(query), {"tenant_id": tenant_id})
equipment_list = result.fetchall()
for equipment in equipment_list:
await self._process_equipment_issue(equipment)
except Exception as e:
logger.error("Error checking equipment status",
tenant_id=str(tenant_id),
error=str(e))
except Exception as e:
logger.error("Equipment status check failed", error=str(e))
self._errors_count += 1
async def _process_equipment_issue(self, equipment: Dict[str, Any]):
"""Process equipment issue"""
try:
status = equipment['status']
efficiency = equipment.get('efficiency_percent', 100)
efficiency = equipment.get('efficiency_percentage', 100)
days_to_maintenance = equipment.get('days_to_maintenance', 30)
if status == 'error':
if status == 'down':
template_data = self.format_spanish_message(
'equipment_failure',
equipment_name=equipment['name']
@@ -422,41 +464,52 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
'equipment_id': str(equipment['id']),
'equipment_name': equipment['name'],
'equipment_type': equipment['type'],
'error_count': equipment.get('error_count', 0),
'last_reading': equipment.get('last_reading').isoformat() if equipment.get('last_reading') else None
'efficiency': efficiency
}
}, item_type='alert')
elif status == 'maintenance_required' or days_to_maintenance <= 1:
severity = 'high' if days_to_maintenance <= 1 else 'medium'
elif status == 'maintenance' or (days_to_maintenance is not None and days_to_maintenance <= 3):
severity = 'high' if (days_to_maintenance is not None and days_to_maintenance <= 1) else 'medium'
template_data = self.format_spanish_message(
'maintenance_required',
equipment_name=equipment['name'],
days_until_maintenance=max(0, int(days_to_maintenance)) if days_to_maintenance is not None else 3
)
await self.publish_item(equipment['tenant_id'], {
'type': 'maintenance_required',
'severity': severity,
'title': f'🔧 Mantenimiento Requerido: {equipment["name"]}',
'message': f'Equipo {equipment["name"]} requiere mantenimiento en {days_to_maintenance} días.',
'actions': ['Programar mantenimiento', 'Revisar historial', 'Preparar repuestos', 'Planificar parada'],
'title': template_data['title'],
'message': template_data['message'],
'actions': template_data['actions'],
'metadata': {
'equipment_id': str(equipment['id']),
'equipment_name': equipment['name'],
'days_to_maintenance': days_to_maintenance,
'last_maintenance': equipment.get('last_maintenance').isoformat() if equipment.get('last_maintenance') else None
'last_maintenance': equipment.get('last_maintenance_date')
}
}, item_type='alert')
elif efficiency < 80:
elif efficiency is not None and efficiency < 80:
severity = 'medium' if efficiency < 70 else 'low'
template_data = self.format_spanish_message(
'low_equipment_efficiency',
equipment_name=equipment['name'],
efficiency_percent=round(efficiency, 1)
)
await self.publish_item(equipment['tenant_id'], {
'type': 'low_equipment_efficiency',
'severity': severity,
'title': f'📉 Baja Eficiencia: {equipment["name"]}',
'message': f'Eficiencia del {equipment["name"]} bajó a {efficiency:.1f}%. Revisar funcionamiento.',
'actions': ['Revisar configuración', 'Limpiar equipo', 'Calibrar sensores', 'Revisar mantenimiento'],
'title': template_data['title'],
'message': template_data['message'],
'actions': template_data['actions'],
'metadata': {
'equipment_id': str(equipment['id']),
'efficiency_percent': float(efficiency),
'temperature': equipment.get('temperature'),
'vibration_level': equipment.get('vibration_level')
'equipment_name': equipment['name'],
'efficiency_percent': float(efficiency)
}
}, item_type='alert')

View File

@@ -0,0 +1,306 @@
# services/production/app/services/quality_template_service.py
"""
Quality Check Template Service for business logic and data operations
"""
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, func
from typing import List, Optional, Tuple
from uuid import UUID, uuid4
from datetime import datetime, timezone
from ..models.production import QualityCheckTemplate, ProcessStage
from ..schemas.quality_templates import QualityCheckTemplateCreate, QualityCheckTemplateUpdate
class QualityTemplateService:
"""Service for managing quality check templates"""
def __init__(self, db: Session):
self.db = db
async def create_template(
self,
tenant_id: str,
template_data: QualityCheckTemplateCreate
) -> QualityCheckTemplate:
"""Create a new quality check template"""
# Validate template code uniqueness if provided
if template_data.template_code:
existing = self.db.query(QualityCheckTemplate).filter(
and_(
QualityCheckTemplate.tenant_id == tenant_id,
QualityCheckTemplate.template_code == template_data.template_code
)
).first()
if existing:
raise ValueError(f"Template code '{template_data.template_code}' already exists")
# Create template
template = QualityCheckTemplate(
id=uuid4(),
tenant_id=UUID(tenant_id),
**template_data.dict()
)
self.db.add(template)
self.db.commit()
self.db.refresh(template)
return template
async def get_templates(
self,
tenant_id: str,
stage: Optional[ProcessStage] = None,
check_type: Optional[str] = None,
is_active: Optional[bool] = True,
skip: int = 0,
limit: int = 100
) -> Tuple[List[QualityCheckTemplate], int]:
"""Get quality check templates with filtering and pagination"""
query = self.db.query(QualityCheckTemplate).filter(
QualityCheckTemplate.tenant_id == tenant_id
)
# Apply filters
if is_active is not None:
query = query.filter(QualityCheckTemplate.is_active == is_active)
if check_type:
query = query.filter(QualityCheckTemplate.check_type == check_type)
if stage:
# Filter by applicable stages (JSON array contains stage)
query = query.filter(
func.json_contains(
QualityCheckTemplate.applicable_stages,
f'"{stage.value}"'
)
)
# Get total count
total = query.count()
# Apply pagination and ordering
templates = query.order_by(
QualityCheckTemplate.is_critical.desc(),
QualityCheckTemplate.is_required.desc(),
QualityCheckTemplate.name
).offset(skip).limit(limit).all()
return templates, total
async def get_template(
self,
tenant_id: str,
template_id: UUID
) -> Optional[QualityCheckTemplate]:
"""Get a specific quality check template"""
return self.db.query(QualityCheckTemplate).filter(
and_(
QualityCheckTemplate.tenant_id == tenant_id,
QualityCheckTemplate.id == template_id
)
).first()
async def update_template(
self,
tenant_id: str,
template_id: UUID,
template_data: QualityCheckTemplateUpdate
) -> Optional[QualityCheckTemplate]:
"""Update a quality check template"""
template = await self.get_template(tenant_id, template_id)
if not template:
return None
# Validate template code uniqueness if being updated
if template_data.template_code and template_data.template_code != template.template_code:
existing = self.db.query(QualityCheckTemplate).filter(
and_(
QualityCheckTemplate.tenant_id == tenant_id,
QualityCheckTemplate.template_code == template_data.template_code,
QualityCheckTemplate.id != template_id
)
).first()
if existing:
raise ValueError(f"Template code '{template_data.template_code}' already exists")
# Update fields
update_data = template_data.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(template, field, value)
template.updated_at = datetime.now(timezone.utc)
self.db.commit()
self.db.refresh(template)
return template
async def delete_template(
self,
tenant_id: str,
template_id: UUID
) -> bool:
"""Delete a quality check template"""
template = await self.get_template(tenant_id, template_id)
if not template:
return False
# Check if template is in use (you might want to add this check)
# For now, we'll allow deletion but in production you might want to:
# 1. Soft delete by setting is_active = False
# 2. Check for dependent quality checks
# 3. Prevent deletion if in use
self.db.delete(template)
self.db.commit()
return True
async def get_templates_for_stage(
self,
tenant_id: str,
stage: ProcessStage,
is_active: Optional[bool] = True
) -> List[QualityCheckTemplate]:
"""Get all quality check templates applicable to a specific process stage"""
query = self.db.query(QualityCheckTemplate).filter(
and_(
QualityCheckTemplate.tenant_id == tenant_id,
or_(
# Templates that specify applicable stages
func.json_contains(
QualityCheckTemplate.applicable_stages,
f'"{stage.value}"'
),
# Templates that don't specify stages (applicable to all)
QualityCheckTemplate.applicable_stages.is_(None)
)
)
)
if is_active is not None:
query = query.filter(QualityCheckTemplate.is_active == is_active)
return query.order_by(
QualityCheckTemplate.is_critical.desc(),
QualityCheckTemplate.is_required.desc(),
QualityCheckTemplate.weight.desc(),
QualityCheckTemplate.name
).all()
async def duplicate_template(
self,
tenant_id: str,
template_id: UUID
) -> Optional[QualityCheckTemplate]:
"""Duplicate an existing quality check template"""
original = await self.get_template(tenant_id, template_id)
if not original:
return None
# Create duplicate with modified name and code
duplicate_data = {
'name': f"{original.name} (Copy)",
'template_code': f"{original.template_code}_copy" if original.template_code else None,
'check_type': original.check_type,
'category': original.category,
'description': original.description,
'instructions': original.instructions,
'parameters': original.parameters,
'thresholds': original.thresholds,
'scoring_criteria': original.scoring_criteria,
'is_active': original.is_active,
'is_required': original.is_required,
'is_critical': original.is_critical,
'weight': original.weight,
'min_value': original.min_value,
'max_value': original.max_value,
'target_value': original.target_value,
'unit': original.unit,
'tolerance_percentage': original.tolerance_percentage,
'applicable_stages': original.applicable_stages,
'created_by': original.created_by
}
create_data = QualityCheckTemplateCreate(**duplicate_data)
return await self.create_template(tenant_id, create_data)
async def get_templates_by_recipe_config(
self,
tenant_id: str,
stage: ProcessStage,
recipe_quality_config: dict
) -> List[QualityCheckTemplate]:
"""Get quality check templates based on recipe configuration"""
# Extract template IDs from recipe configuration for the specific stage
stage_config = recipe_quality_config.get('stages', {}).get(stage.value)
if not stage_config:
return []
template_ids = stage_config.get('template_ids', [])
if not template_ids:
return []
# Get templates by IDs
templates = self.db.query(QualityCheckTemplate).filter(
and_(
QualityCheckTemplate.tenant_id == tenant_id,
QualityCheckTemplate.id.in_([UUID(tid) for tid in template_ids]),
QualityCheckTemplate.is_active == True
)
).order_by(
QualityCheckTemplate.is_critical.desc(),
QualityCheckTemplate.is_required.desc(),
QualityCheckTemplate.weight.desc()
).all()
return templates
async def validate_template_configuration(
self,
tenant_id: str,
template_data: dict
) -> Tuple[bool, List[str]]:
"""Validate quality check template configuration"""
errors = []
# Validate check type specific requirements
check_type = template_data.get('check_type')
if check_type in ['measurement', 'temperature', 'weight']:
if not template_data.get('unit'):
errors.append(f"Unit is required for {check_type} checks")
min_val = template_data.get('min_value')
max_val = template_data.get('max_value')
if min_val is not None and max_val is not None and min_val >= max_val:
errors.append("Minimum value must be less than maximum value")
# Validate scoring criteria
scoring = template_data.get('scoring_criteria', {})
if check_type == 'visual' and not scoring:
errors.append("Visual checks require scoring criteria")
# Validate process stages
stages = template_data.get('applicable_stages', [])
if stages:
valid_stages = [stage.value for stage in ProcessStage]
invalid_stages = [s for s in stages if s not in valid_stages]
if invalid_stages:
errors.append(f"Invalid process stages: {invalid_stages}")
return len(errors) == 0, errors