From 2de1e6ce405f360a0090eddd16f864af0eee7ac6 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Wed, 24 Sep 2025 16:42:23 +0200 Subject: [PATCH] Add quality template logic --- frontend/src/api/services/qualityTemplates.ts | 42 ++-- .../QualityTemplatesPage.tsx | 19 ++ frontend/src/router/AppRouter.tsx | 11 + frontend/src/router/routes.config.ts | 12 + services/production/app/api/production.py | 210 +++++++++++++++++- .../production/app/api/quality_templates.py | 174 --------------- services/production/app/main.py | 2 - services/production/app/models/production.py | 22 +- .../quality_template_repository.py | 152 +++++++++++++ .../app/services/production_alert_service.py | 8 +- .../app/services/quality_template_service.py | 26 +-- 11 files changed, 450 insertions(+), 228 deletions(-) create mode 100644 frontend/src/pages/app/database/quality-templates/QualityTemplatesPage.tsx delete mode 100644 services/production/app/api/quality_templates.py create mode 100644 services/production/app/repositories/quality_template_repository.py diff --git a/frontend/src/api/services/qualityTemplates.ts b/frontend/src/api/services/qualityTemplates.ts index 4442f582..fe0f85a2 100644 --- a/frontend/src/api/services/qualityTemplates.ts +++ b/frontend/src/api/services/qualityTemplates.ts @@ -16,7 +16,7 @@ import type { } from '../types/qualityTemplates'; class QualityTemplateService { - private readonly baseURL = '/production/api/v1/quality-templates'; + private readonly baseURL = '/tenants'; /** * Create a new quality check template @@ -25,10 +25,10 @@ class QualityTemplateService { tenantId: string, templateData: QualityCheckTemplateCreate ): Promise { - const response = await apiClient.post(this.baseURL, templateData, { + const data = await apiClient.post(`${this.baseURL}/${tenantId}/production/quality-templates`, templateData, { headers: { 'X-Tenant-ID': tenantId } }); - return response.data; + return data; } /** @@ -38,11 +38,11 @@ class QualityTemplateService { tenantId: string, params?: QualityTemplateQueryParams ): Promise { - const response = await apiClient.get(this.baseURL, { + const data = await apiClient.get(`${this.baseURL}/${tenantId}/production/quality-templates`, { params, headers: { 'X-Tenant-ID': tenantId } }); - return response.data; + return data; } /** @@ -52,10 +52,10 @@ class QualityTemplateService { tenantId: string, templateId: string ): Promise { - const response = await apiClient.get(`${this.baseURL}/${templateId}`, { + const data = await apiClient.get(`${this.baseURL}/${tenantId}/production/quality-templates/${templateId}`, { headers: { 'X-Tenant-ID': tenantId } }); - return response.data; + return data; } /** @@ -66,17 +66,17 @@ class QualityTemplateService { templateId: string, templateData: QualityCheckTemplateUpdate ): Promise { - const response = await apiClient.put(`${this.baseURL}/${templateId}`, templateData, { + const data = await apiClient.put(`${this.baseURL}/${tenantId}/production/quality-templates/${templateId}`, templateData, { headers: { 'X-Tenant-ID': tenantId } }); - return response.data; + return data; } /** * Delete a quality check template */ async deleteTemplate(tenantId: string, templateId: string): Promise { - await apiClient.delete(`${this.baseURL}/${templateId}`, { + await apiClient.delete(`${this.baseURL}/${tenantId}/production/quality-templates/${templateId}`, { headers: { 'X-Tenant-ID': tenantId } }); } @@ -89,11 +89,11 @@ class QualityTemplateService { stage: ProcessStage, isActive: boolean = true ): Promise { - const response = await apiClient.get(`${this.baseURL}/stages/${stage}`, { + const data = await apiClient.get(`${this.baseURL}/${tenantId}/production/quality-templates/stages/${stage}`, { params: { is_active: isActive }, headers: { 'X-Tenant-ID': tenantId } }); - return response.data; + return data; } /** @@ -103,10 +103,10 @@ class QualityTemplateService { tenantId: string, templateId: string ): Promise { - const response = await apiClient.post(`${this.baseURL}/${templateId}/duplicate`, {}, { + const data = await apiClient.post(`${this.baseURL}/${tenantId}/production/quality-templates/${templateId}/duplicate`, {}, { headers: { 'X-Tenant-ID': tenantId } }); - return response.data; + return data; } /** @@ -116,10 +116,10 @@ class QualityTemplateService { tenantId: string, executionData: QualityCheckExecutionRequest ): Promise { - const response = await apiClient.post('/production/api/v1/quality-checks/execute', executionData, { + const data = await apiClient.post(`${this.baseURL}/${tenantId}/production/quality-checks/execute`, executionData, { headers: { 'X-Tenant-ID': tenantId } }); - return response.data; + return data; } /** @@ -130,11 +130,11 @@ class QualityTemplateService { batchId: string, stage?: ProcessStage ): Promise { - const response = await apiClient.get('/production/api/v1/quality-checks', { + const data = await apiClient.get(`${this.baseURL}/${tenantId}/production/quality-checks`, { params: { batch_id: batchId, process_stage: stage }, headers: { 'X-Tenant-ID': tenantId } }); - return response.data; + return data; } /** @@ -168,15 +168,15 @@ class QualityTemplateService { templateData: Partial ): Promise<{ valid: boolean; errors: string[] }> { try { - const response = await apiClient.post(`${this.baseURL}/validate`, templateData, { + const data = await apiClient.post(`${this.baseURL}/${tenantId}/production/quality-templates/validate`, templateData, { headers: { 'X-Tenant-ID': tenantId } }); - return response.data; + return data; } catch (error: any) { if (error.response?.status === 400) { return { valid: false, - errors: [error.response.data.detail || 'Validation failed'] + errors: [error.response?.data?.detail || 'Validation failed'] }; } throw error; diff --git a/frontend/src/pages/app/database/quality-templates/QualityTemplatesPage.tsx b/frontend/src/pages/app/database/quality-templates/QualityTemplatesPage.tsx new file mode 100644 index 00000000..68cb80bc --- /dev/null +++ b/frontend/src/pages/app/database/quality-templates/QualityTemplatesPage.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { QualityTemplateManager } from '../../../../components/domain/production'; + +/** + * QualityTemplatesPage - Page wrapper for the QualityTemplateManager component + * + * This page provides access to quality template management functionality, + * allowing users to create, edit, duplicate, and manage quality control templates + * that are used during production processes. + */ +const QualityTemplatesPage: React.FC = () => { + return ( +
+ +
+ ); +}; + +export default QualityTemplatesPage; \ No newline at end of file diff --git a/frontend/src/router/AppRouter.tsx b/frontend/src/router/AppRouter.tsx index 0e090753..5903b3b0 100644 --- a/frontend/src/router/AppRouter.tsx +++ b/frontend/src/router/AppRouter.tsx @@ -37,6 +37,7 @@ const OrganizationsPage = React.lazy(() => import('../pages/app/settings/organiz // Database pages const DatabasePage = React.lazy(() => import('../pages/app/database/DatabasePage')); const ModelsConfigPage = React.lazy(() => import('../pages/app/database/models/ModelsConfigPage')); +const QualityTemplatesPage = React.lazy(() => import('../pages/app/database/quality-templates/QualityTemplatesPage')); // Data pages const WeatherPage = React.lazy(() => import('../pages/app/data/weather/WeatherPage')); @@ -190,6 +191,16 @@ export const AppRouter: React.FC = () => { } /> + + + + + + } + /> Tuple[List[QualityCheckTemplate], int]: + """Get quality check templates with filtering and pagination""" + + filters = [QualityCheckTemplate.tenant_id == tenant_id] + + if is_active is not None: + filters.append(QualityCheckTemplate.is_active == is_active) + + if check_type: + filters.append(QualityCheckTemplate.check_type == check_type) + + if stage: + filters.append( + or_( + func.json_contains( + QualityCheckTemplate.applicable_stages, + f'"{stage.value}"' + ), + QualityCheckTemplate.applicable_stages.is_(None) + ) + ) + + # Get total count with SQLAlchemy conditions + count_query = select(func.count(QualityCheckTemplate.id)).where(and_(*filters)) + count_result = await self.session.execute(count_query) + total = count_result.scalar() + + # Get templates with ordering + query = select(QualityCheckTemplate).where(and_(*filters)).order_by( + QualityCheckTemplate.is_critical.desc(), + QualityCheckTemplate.is_required.desc(), + QualityCheckTemplate.name + ).offset(skip).limit(limit) + + result = await self.session.execute(query) + templates = result.scalars().all() + + return templates, total + + async def get_by_tenant_and_id( + self, + tenant_id: str, + template_id: UUID + ) -> Optional[QualityCheckTemplate]: + """Get a specific quality check template by tenant and ID""" + + return await self.get_by_filters( + and_( + QualityCheckTemplate.tenant_id == tenant_id, + QualityCheckTemplate.id == template_id + ) + ) + + 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""" + + filters = [ + QualityCheckTemplate.tenant_id == tenant_id, + or_( + func.json_contains( + QualityCheckTemplate.applicable_stages, + f'"{stage.value}"' + ), + QualityCheckTemplate.applicable_stages.is_(None) + ) + ] + + if is_active is not None: + filters.append(QualityCheckTemplate.is_active == is_active) + + return await self.get_multi( + filters=and_(*filters), + order_by=[ + QualityCheckTemplate.is_critical.desc(), + QualityCheckTemplate.is_required.desc(), + QualityCheckTemplate.weight.desc(), + QualityCheckTemplate.name + ] + ) + + async def check_template_code_exists( + self, + tenant_id: str, + template_code: str, + exclude_id: Optional[UUID] = None + ) -> bool: + """Check if a template code already exists for the tenant""" + + filters = [ + QualityCheckTemplate.tenant_id == tenant_id, + QualityCheckTemplate.template_code == template_code + ] + + if exclude_id: + filters.append(QualityCheckTemplate.id != exclude_id) + + existing = await self.get_by_filters(and_(*filters)) + return existing is not None + + async def get_templates_by_ids( + self, + tenant_id: str, + template_ids: List[UUID] + ) -> List[QualityCheckTemplate]: + """Get quality check templates by list of IDs""" + + return await self.get_multi( + filters=and_( + QualityCheckTemplate.tenant_id == tenant_id, + QualityCheckTemplate.id.in_(template_ids) + ), + order_by=[ + QualityCheckTemplate.is_critical.desc(), + QualityCheckTemplate.is_required.desc(), + QualityCheckTemplate.weight.desc() + ] + ) \ No newline at end of file diff --git a/services/production/app/services/production_alert_service.py b/services/production/app/services/production_alert_service.py index 99de83cb..02652c6e 100644 --- a/services/production/app/services/production_alert_service.py +++ b/services/production/app/services/production_alert_service.py @@ -104,7 +104,7 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin): JOIN production_capacity pc ON pc.equipment_id = p.equipment_id WHERE p.planned_date >= CURRENT_DATE AND p.planned_date <= CURRENT_DATE + INTERVAL '3 days' - AND p.status IN ('planned', 'in_progress') + AND p.status IN ('PENDING', 'IN_PROGRESS') AND p.tenant_id = $1 GROUP BY p.tenant_id, p.planned_date ) @@ -226,10 +226,10 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin): COALESCE(pb.priority::text, 'medium') as priority_level, 1 as affected_orders -- Default to 1 since we can't count orders FROM production_batches pb - WHERE pb.status IN ('IN_PROGRESS', 'DELAYED') + WHERE pb.status IN ('IN_PROGRESS', 'ON_HOLD', 'QUALITY_CHECK') AND ( (pb.planned_end_time < NOW() AND pb.status = 'IN_PROGRESS') - OR pb.status = 'DELAYED' + OR pb.status IN ('ON_HOLD', 'QUALITY_CHECK') ) AND pb.planned_end_time > NOW() - INTERVAL '24 hours' ORDER BY @@ -831,7 +831,7 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin): FROM production_batches pb JOIN recipe_ingredients ri ON ri.recipe_id = pb.recipe_id WHERE ri.ingredient_id = $1 - AND pb.status IN ('planned', 'in_progress') + AND pb.status IN ('PENDING', 'IN_PROGRESS') AND pb.planned_completion_time > NOW() """ diff --git a/services/production/app/services/quality_template_service.py b/services/production/app/services/quality_template_service.py index 0c00ed88..2b3af6d2 100644 --- a/services/production/app/services/quality_template_service.py +++ b/services/production/app/services/quality_template_service.py @@ -19,7 +19,7 @@ class QualityTemplateService: def __init__(self, db: Session): self.db = db - async def create_template( + def create_template( self, tenant_id: str, template_data: QualityCheckTemplateCreate @@ -50,7 +50,7 @@ class QualityTemplateService: return template - async def get_templates( + def get_templates( self, tenant_id: str, stage: Optional[ProcessStage] = None, @@ -93,7 +93,7 @@ class QualityTemplateService: return templates, total - async def get_template( + def get_template( self, tenant_id: str, template_id: UUID @@ -107,7 +107,7 @@ class QualityTemplateService: ) ).first() - async def update_template( + def update_template( self, tenant_id: str, template_id: UUID, @@ -115,7 +115,7 @@ class QualityTemplateService: ) -> Optional[QualityCheckTemplate]: """Update a quality check template""" - template = await self.get_template(tenant_id, template_id) + template = self.get_template(tenant_id, template_id) if not template: return None @@ -143,14 +143,14 @@ class QualityTemplateService: return template - async def delete_template( + 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) + template = self.get_template(tenant_id, template_id) if not template: return False @@ -165,7 +165,7 @@ class QualityTemplateService: return True - async def get_templates_for_stage( + def get_templates_for_stage( self, tenant_id: str, stage: ProcessStage, @@ -198,14 +198,14 @@ class QualityTemplateService: QualityCheckTemplate.name ).all() - async def duplicate_template( + 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) + original = self.get_template(tenant_id, template_id) if not original: return None @@ -234,9 +234,9 @@ class QualityTemplateService: } create_data = QualityCheckTemplateCreate(**duplicate_data) - return await self.create_template(tenant_id, create_data) + return self.create_template(tenant_id, create_data) - async def get_templates_by_recipe_config( + def get_templates_by_recipe_config( self, tenant_id: str, stage: ProcessStage, @@ -268,7 +268,7 @@ class QualityTemplateService: return templates - async def validate_template_configuration( + def validate_template_configuration( self, tenant_id: str, template_data: dict