From 7892c5a739a6cf5b2abc119a25780c48d4f4a657 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Tue, 23 Sep 2025 19:24:22 +0200 Subject: [PATCH] Add improved production UI 3 --- frontend/src/api/hooks/qualityTemplates.ts | 275 ++++++++ frontend/src/api/services/qualityTemplates.ts | 205 ++++++ frontend/src/api/types/qualityTemplates.ts | 173 +++++ .../domain/dashboard/ProductionPlansToday.tsx | 334 +++++++++- .../production/CompactProcessStageTracker.tsx | 303 +++++++++ .../production/CreateQualityTemplateModal.tsx | 406 ++++++++++++ .../production/EditQualityTemplateModal.tsx | 463 ++++++++++++++ .../domain/production/ProcessStageTracker.tsx | 377 +++++++++++ .../domain/production/QualityCheckModal.tsx | 87 ++- .../production/QualityTemplateManager.tsx | 467 ++++++++++++++ .../src/components/domain/production/index.ts | 5 + .../QualityCheckConfigurationModal.tsx | 372 +++++++++++ .../src/components/domain/recipes/index.ts | 3 +- .../src/components/layout/Sidebar/Sidebar.tsx | 1 + frontend/src/components/ui/Button/Button.tsx | 21 +- frontend/src/components/ui/Toggle/Toggle.tsx | 121 ++++ frontend/src/components/ui/Toggle/index.ts | 2 + frontend/src/components/ui/index.ts | 2 + frontend/src/locales/en/common.json | 1 + frontend/src/locales/en/equipment.json | 90 +++ frontend/src/locales/es/common.json | 1 + frontend/src/locales/es/equipment.json | 87 +++ frontend/src/locales/es/production.json | 4 + frontend/src/locales/eu/common.json | 17 +- frontend/src/locales/eu/equipment.json | 90 +++ frontend/src/locales/eu/production.json | 102 +-- frontend/src/locales/index.ts | 10 +- .../app/analytics/ProductionAnalyticsPage.tsx | 252 ++++++++ frontend/src/pages/app/operations/index.ts | 3 +- .../operations/maquinaria/MaquinariaPage.tsx | 604 ++++++++++++++++++ .../pages/app/operations/maquinaria/index.ts | 1 + .../procurement/ProcurementPage.tsx | 100 ++- .../operations/production/ProductionPage.tsx | 265 ++++++-- .../app/operations/recipes/RecipesPage.tsx | 85 ++- frontend/src/router/AppRouter.tsx | 28 +- frontend/src/router/routes.config.ts | 25 +- frontend/src/types/equipment.ts | 54 ++ services/production/app/api/production.py | 1 + .../production/app/api/quality_templates.py | 174 +++++ services/production/app/main.py | 2 + services/production/app/models/production.py | 212 +++++- .../app/schemas/quality_templates.py | 179 ++++++ .../app/services/production_alert_service.py | 111 +++- .../app/services/quality_template_service.py | 306 +++++++++ .../004_add_quality_check_configuration.py | 41 ++ services/recipes/app/models/recipes.py | 2 + shared/alerts/templates.py | 14 + 47 files changed, 6211 insertions(+), 267 deletions(-) create mode 100644 frontend/src/api/hooks/qualityTemplates.ts create mode 100644 frontend/src/api/services/qualityTemplates.ts create mode 100644 frontend/src/api/types/qualityTemplates.ts create mode 100644 frontend/src/components/domain/production/CompactProcessStageTracker.tsx create mode 100644 frontend/src/components/domain/production/CreateQualityTemplateModal.tsx create mode 100644 frontend/src/components/domain/production/EditQualityTemplateModal.tsx create mode 100644 frontend/src/components/domain/production/ProcessStageTracker.tsx create mode 100644 frontend/src/components/domain/production/QualityTemplateManager.tsx create mode 100644 frontend/src/components/domain/recipes/QualityCheckConfigurationModal.tsx create mode 100644 frontend/src/components/ui/Toggle/Toggle.tsx create mode 100644 frontend/src/components/ui/Toggle/index.ts create mode 100644 frontend/src/locales/en/equipment.json create mode 100644 frontend/src/locales/es/equipment.json create mode 100644 frontend/src/locales/eu/equipment.json create mode 100644 frontend/src/pages/app/analytics/ProductionAnalyticsPage.tsx create mode 100644 frontend/src/pages/app/operations/maquinaria/MaquinariaPage.tsx create mode 100644 frontend/src/pages/app/operations/maquinaria/index.ts create mode 100644 frontend/src/types/equipment.ts create mode 100644 services/production/app/api/quality_templates.py create mode 100644 services/production/app/schemas/quality_templates.py create mode 100644 services/production/app/services/quality_template_service.py create mode 100644 services/recipes/alembic/versions/004_add_quality_check_configuration.py diff --git a/frontend/src/api/hooks/qualityTemplates.ts b/frontend/src/api/hooks/qualityTemplates.ts new file mode 100644 index 00000000..1bcca37e --- /dev/null +++ b/frontend/src/api/hooks/qualityTemplates.ts @@ -0,0 +1,275 @@ +// frontend/src/api/hooks/qualityTemplates.ts +/** + * React hooks for Quality Check Template API integration + */ + +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'react-hot-toast'; +import { qualityTemplateService } from '../services/qualityTemplates'; +import type { + QualityCheckTemplate, + QualityCheckTemplateCreate, + QualityCheckTemplateUpdate, + QualityTemplateQueryParams, + ProcessStage, + QualityCheckExecutionRequest +} from '../types/qualityTemplates'; + +// Query Keys +export const qualityTemplateKeys = { + all: ['qualityTemplates'] as const, + lists: () => [...qualityTemplateKeys.all, 'list'] as const, + list: (tenantId: string, params?: QualityTemplateQueryParams) => + [...qualityTemplateKeys.lists(), tenantId, params] as const, + details: () => [...qualityTemplateKeys.all, 'detail'] as const, + detail: (tenantId: string, templateId: string) => + [...qualityTemplateKeys.details(), tenantId, templateId] as const, + forStage: (tenantId: string, stage: ProcessStage) => + [...qualityTemplateKeys.all, 'stage', tenantId, stage] as const, + forRecipe: (tenantId: string, recipeId: string) => + [...qualityTemplateKeys.all, 'recipe', tenantId, recipeId] as const, +}; + +/** + * Hook to fetch quality check templates + */ +export function useQualityTemplates( + tenantId: string, + params?: QualityTemplateQueryParams, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: qualityTemplateKeys.list(tenantId, params), + queryFn: () => qualityTemplateService.getTemplates(tenantId, params), + enabled: !!tenantId && (options?.enabled ?? true), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +} + +/** + * Hook to fetch a specific quality check template + */ +export function useQualityTemplate( + tenantId: string, + templateId: string, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: qualityTemplateKeys.detail(tenantId, templateId), + queryFn: () => qualityTemplateService.getTemplate(tenantId, templateId), + enabled: !!tenantId && !!templateId && (options?.enabled ?? true), + staleTime: 10 * 60 * 1000, // 10 minutes + }); +} + +/** + * Hook to fetch templates for a specific process stage + */ +export function useQualityTemplatesForStage( + tenantId: string, + stage: ProcessStage, + isActive: boolean = true, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: qualityTemplateKeys.forStage(tenantId, stage), + queryFn: () => qualityTemplateService.getTemplatesForStage(tenantId, stage, isActive), + enabled: !!tenantId && !!stage && (options?.enabled ?? true), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +} + +/** + * Hook to fetch templates organized by stages for recipe configuration + */ +export function useQualityTemplatesForRecipe( + tenantId: string, + recipeId: string, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: qualityTemplateKeys.forRecipe(tenantId, recipeId), + queryFn: () => qualityTemplateService.getTemplatesForRecipe(tenantId, recipeId), + enabled: !!tenantId && !!recipeId && (options?.enabled ?? true), + staleTime: 10 * 60 * 1000, // 10 minutes + }); +} + +/** + * Hook to create a quality check template + */ +export function useCreateQualityTemplate(tenantId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (templateData: QualityCheckTemplateCreate) => + qualityTemplateService.createTemplate(tenantId, templateData), + onSuccess: (newTemplate) => { + // Invalidate and refetch quality template lists + queryClient.invalidateQueries({ queryKey: qualityTemplateKeys.lists() }); + + // Add to cache + queryClient.setQueryData( + qualityTemplateKeys.detail(tenantId, newTemplate.id), + newTemplate + ); + + toast.success('Plantilla de calidad creada exitosamente'); + }, + onError: (error: any) => { + console.error('Error creating quality template:', error); + toast.error(error.response?.data?.detail || 'Error al crear la plantilla de calidad'); + }, + }); +} + +/** + * Hook to update a quality check template + */ +export function useUpdateQualityTemplate(tenantId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ templateId, templateData }: { + templateId: string; + templateData: QualityCheckTemplateUpdate; + }) => qualityTemplateService.updateTemplate(tenantId, templateId, templateData), + onSuccess: (updatedTemplate, { templateId }) => { + // Update cached data + queryClient.setQueryData( + qualityTemplateKeys.detail(tenantId, templateId), + updatedTemplate + ); + + // Invalidate lists to refresh + queryClient.invalidateQueries({ queryKey: qualityTemplateKeys.lists() }); + + toast.success('Plantilla de calidad actualizada exitosamente'); + }, + onError: (error: any) => { + console.error('Error updating quality template:', error); + toast.error(error.response?.data?.detail || 'Error al actualizar la plantilla de calidad'); + }, + }); +} + +/** + * Hook to delete a quality check template + */ +export function useDeleteQualityTemplate(tenantId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (templateId: string) => + qualityTemplateService.deleteTemplate(tenantId, templateId), + onSuccess: (_, templateId) => { + // Remove from cache + queryClient.removeQueries({ + queryKey: qualityTemplateKeys.detail(tenantId, templateId) + }); + + // Invalidate lists to refresh + queryClient.invalidateQueries({ queryKey: qualityTemplateKeys.lists() }); + + toast.success('Plantilla de calidad eliminada exitosamente'); + }, + onError: (error: any) => { + console.error('Error deleting quality template:', error); + toast.error(error.response?.data?.detail || 'Error al eliminar la plantilla de calidad'); + }, + }); +} + +/** + * Hook to duplicate a quality check template + */ +export function useDuplicateQualityTemplate(tenantId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (templateId: string) => + qualityTemplateService.duplicateTemplate(tenantId, templateId), + onSuccess: (duplicatedTemplate) => { + // Add to cache + queryClient.setQueryData( + qualityTemplateKeys.detail(tenantId, duplicatedTemplate.id), + duplicatedTemplate + ); + + // Invalidate lists to refresh + queryClient.invalidateQueries({ queryKey: qualityTemplateKeys.lists() }); + + toast.success('Plantilla de calidad duplicada exitosamente'); + }, + onError: (error: any) => { + console.error('Error duplicating quality template:', error); + toast.error(error.response?.data?.detail || 'Error al duplicar la plantilla de calidad'); + }, + }); +} + +/** + * Hook to execute a quality check + */ +export function useExecuteQualityCheck(tenantId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (executionData: QualityCheckExecutionRequest) => + qualityTemplateService.executeQualityCheck(tenantId, executionData), + onSuccess: (result, executionData) => { + // Invalidate production batch data to refresh status + queryClient.invalidateQueries({ + queryKey: ['production', 'batches', tenantId] + }); + + // Invalidate quality check history + queryClient.invalidateQueries({ + queryKey: ['qualityChecks', tenantId, executionData.batch_id] + }); + + const message = result.overall_pass + ? 'Control de calidad completado exitosamente' + : 'Control de calidad completado con observaciones'; + + if (result.overall_pass) { + toast.success(message); + } else { + toast.error(message); + } + }, + onError: (error: any) => { + console.error('Error executing quality check:', error); + toast.error(error.response?.data?.detail || 'Error al ejecutar el control de calidad'); + }, + }); +} + +/** + * Hook to get default templates for a product category + */ +export function useDefaultQualityTemplates( + tenantId: string, + productCategory: string, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: [...qualityTemplateKeys.all, 'defaults', tenantId, productCategory], + queryFn: () => qualityTemplateService.getDefaultTemplates(tenantId, productCategory), + enabled: !!tenantId && !!productCategory && (options?.enabled ?? true), + staleTime: 15 * 60 * 1000, // 15 minutes + }); +} + +/** + * Hook to validate template configuration + */ +export function useValidateQualityTemplate(tenantId: string) { + return useMutation({ + mutationFn: (templateData: Partial) => + qualityTemplateService.validateTemplate(tenantId, templateData), + onError: (error: any) => { + console.error('Error validating quality template:', error); + }, + }); +} \ No newline at end of file diff --git a/frontend/src/api/services/qualityTemplates.ts b/frontend/src/api/services/qualityTemplates.ts new file mode 100644 index 00000000..4442f582 --- /dev/null +++ b/frontend/src/api/services/qualityTemplates.ts @@ -0,0 +1,205 @@ +// frontend/src/api/services/qualityTemplates.ts +/** + * Quality Check Template API service + */ + +import { apiClient } from '../client'; +import type { + QualityCheckTemplate, + QualityCheckTemplateCreate, + QualityCheckTemplateUpdate, + QualityCheckTemplateList, + QualityTemplateQueryParams, + ProcessStage, + QualityCheckExecutionRequest, + QualityCheckExecutionResponse +} from '../types/qualityTemplates'; + +class QualityTemplateService { + private readonly baseURL = '/production/api/v1/quality-templates'; + + /** + * Create a new quality check template + */ + async createTemplate( + tenantId: string, + templateData: QualityCheckTemplateCreate + ): Promise { + const response = await apiClient.post(this.baseURL, templateData, { + headers: { 'X-Tenant-ID': tenantId } + }); + return response.data; + } + + /** + * Get quality check templates with filtering and pagination + */ + async getTemplates( + tenantId: string, + params?: QualityTemplateQueryParams + ): Promise { + const response = await apiClient.get(this.baseURL, { + params, + headers: { 'X-Tenant-ID': tenantId } + }); + return response.data; + } + + /** + * Get a specific quality check template + */ + async getTemplate( + tenantId: string, + templateId: string + ): Promise { + const response = await apiClient.get(`${this.baseURL}/${templateId}`, { + headers: { 'X-Tenant-ID': tenantId } + }); + return response.data; + } + + /** + * Update a quality check template + */ + async updateTemplate( + tenantId: string, + templateId: string, + templateData: QualityCheckTemplateUpdate + ): Promise { + const response = await apiClient.put(`${this.baseURL}/${templateId}`, templateData, { + headers: { 'X-Tenant-ID': tenantId } + }); + return response.data; + } + + /** + * Delete a quality check template + */ + async deleteTemplate(tenantId: string, templateId: string): Promise { + await apiClient.delete(`${this.baseURL}/${templateId}`, { + headers: { 'X-Tenant-ID': tenantId } + }); + } + + /** + * Get templates applicable to a specific process stage + */ + async getTemplatesForStage( + tenantId: string, + stage: ProcessStage, + isActive: boolean = true + ): Promise { + const response = await apiClient.get(`${this.baseURL}/stages/${stage}`, { + params: { is_active: isActive }, + headers: { 'X-Tenant-ID': tenantId } + }); + return response.data; + } + + /** + * Duplicate an existing quality check template + */ + async duplicateTemplate( + tenantId: string, + templateId: string + ): Promise { + const response = await apiClient.post(`${this.baseURL}/${templateId}/duplicate`, {}, { + headers: { 'X-Tenant-ID': tenantId } + }); + return response.data; + } + + /** + * Execute a quality check using a template + */ + async executeQualityCheck( + tenantId: string, + executionData: QualityCheckExecutionRequest + ): Promise { + const response = await apiClient.post('/production/api/v1/quality-checks/execute', executionData, { + headers: { 'X-Tenant-ID': tenantId } + }); + return response.data; + } + + /** + * Get quality check history for a batch + */ + async getQualityCheckHistory( + tenantId: string, + batchId: string, + stage?: ProcessStage + ): Promise { + const response = await apiClient.get('/production/api/v1/quality-checks', { + params: { batch_id: batchId, process_stage: stage }, + headers: { 'X-Tenant-ID': tenantId } + }); + return response.data; + } + + /** + * Get quality check templates for recipe configuration + */ + async getTemplatesForRecipe( + tenantId: string, + recipeId: string + ): Promise> { + const allTemplates = await this.getTemplates(tenantId, { is_active: true }); + + // Group templates by applicable stages + const templatesByStage: Record = {} as any; + + Object.values(ProcessStage).forEach(stage => { + templatesByStage[stage] = allTemplates.templates.filter(template => + !template.applicable_stages || + template.applicable_stages.length === 0 || + template.applicable_stages.includes(stage) + ); + }); + + return templatesByStage; + } + + /** + * Validate template configuration + */ + async validateTemplate( + tenantId: string, + templateData: Partial + ): Promise<{ valid: boolean; errors: string[] }> { + try { + const response = await apiClient.post(`${this.baseURL}/validate`, templateData, { + headers: { 'X-Tenant-ID': tenantId } + }); + return response.data; + } catch (error: any) { + if (error.response?.status === 400) { + return { + valid: false, + errors: [error.response.data.detail || 'Validation failed'] + }; + } + throw error; + } + } + + /** + * Get default template suggestions based on product type + */ + async getDefaultTemplates( + tenantId: string, + productCategory: string + ): Promise { + const templates = await this.getTemplates(tenantId, { + is_active: true, + category: productCategory + }); + + // Return commonly used templates for the product category + return templates.templates.filter(template => + template.is_required || template.weight > 5.0 + ).sort((a, b) => b.weight - a.weight); + } +} + +export const qualityTemplateService = new QualityTemplateService(); \ No newline at end of file diff --git a/frontend/src/api/types/qualityTemplates.ts b/frontend/src/api/types/qualityTemplates.ts new file mode 100644 index 00000000..9c6d181f --- /dev/null +++ b/frontend/src/api/types/qualityTemplates.ts @@ -0,0 +1,173 @@ +// frontend/src/api/types/qualityTemplates.ts +/** + * Quality Check Template types for API integration + */ + +export enum QualityCheckType { + VISUAL = 'visual', + MEASUREMENT = 'measurement', + TEMPERATURE = 'temperature', + WEIGHT = 'weight', + BOOLEAN = 'boolean', + TIMING = 'timing' +} + +export enum ProcessStage { + MIXING = 'mixing', + PROOFING = 'proofing', + SHAPING = 'shaping', + BAKING = 'baking', + COOLING = 'cooling', + PACKAGING = 'packaging', + FINISHING = 'finishing' +} + +export interface QualityCheckTemplate { + id: string; + tenant_id: string; + name: string; + template_code?: string; + check_type: QualityCheckType; + category?: string; + description?: string; + instructions?: string; + parameters?: Record; + thresholds?: Record; + scoring_criteria?: Record; + is_active: boolean; + is_required: boolean; + is_critical: boolean; + weight: number; + min_value?: number; + max_value?: number; + target_value?: number; + unit?: string; + tolerance_percentage?: number; + applicable_stages?: ProcessStage[]; + created_by: string; + created_at: string; + updated_at: string; +} + +export interface QualityCheckTemplateCreate { + name: string; + template_code?: string; + check_type: QualityCheckType; + category?: string; + description?: string; + instructions?: string; + parameters?: Record; + thresholds?: Record; + scoring_criteria?: Record; + is_active?: boolean; + is_required?: boolean; + is_critical?: boolean; + weight?: number; + min_value?: number; + max_value?: number; + target_value?: number; + unit?: string; + tolerance_percentage?: number; + applicable_stages?: ProcessStage[]; + created_by: string; +} + +export interface QualityCheckTemplateUpdate { + name?: string; + template_code?: string; + check_type?: QualityCheckType; + category?: string; + description?: string; + instructions?: string; + parameters?: Record; + thresholds?: Record; + scoring_criteria?: Record; + is_active?: boolean; + is_required?: boolean; + is_critical?: boolean; + weight?: number; + min_value?: number; + max_value?: number; + target_value?: number; + unit?: string; + tolerance_percentage?: number; + applicable_stages?: ProcessStage[]; +} + +export interface QualityCheckTemplateList { + templates: QualityCheckTemplate[]; + total: number; + skip: number; + limit: number; +} + +export interface QualityCheckCriterion { + id: string; + name: string; + description: string; + check_type: QualityCheckType; + required: boolean; + weight: number; + acceptable_criteria: string; + min_value?: number; + max_value?: number; + unit?: string; + is_critical: boolean; +} + +export interface QualityCheckResult { + criterion_id: string; + value: number | string | boolean; + score: number; + notes?: string; + photos?: string[]; + pass_check: boolean; + timestamp: string; +} + +export interface QualityCheckExecutionRequest { + template_id: string; + batch_id: string; + process_stage: ProcessStage; + checker_id?: string; + results: QualityCheckResult[]; + final_notes?: string; + photos?: string[]; +} + +export interface QualityCheckExecutionResponse { + check_id: string; + overall_score: number; + overall_pass: boolean; + critical_failures: string[]; + corrective_actions: string[]; + timestamp: string; +} + +export interface ProcessStageQualityConfig { + stage: ProcessStage; + template_ids: string[]; + custom_parameters?: Record; + is_required: boolean; + blocking: boolean; +} + +export interface RecipeQualityConfiguration { + stages: Record; + global_parameters?: Record; + default_templates?: string[]; +} + +// Filter and query types +export interface QualityTemplateFilters { + stage?: ProcessStage; + check_type?: QualityCheckType; + is_active?: boolean; + category?: string; + search?: string; +} + +export interface QualityTemplateQueryParams extends QualityTemplateFilters { + skip?: number; + limit?: number; +} \ No newline at end of file diff --git a/frontend/src/components/domain/dashboard/ProductionPlansToday.tsx b/frontend/src/components/domain/dashboard/ProductionPlansToday.tsx index 47b98416..3ae6a292 100644 --- a/frontend/src/components/domain/dashboard/ProductionPlansToday.tsx +++ b/frontend/src/components/domain/dashboard/ProductionPlansToday.tsx @@ -15,9 +15,43 @@ import { ChevronRight, Timer, Package, - Flame + Flame, + ChefHat, + Eye, + Scale, + FlaskRound, + CircleDot, + ArrowRight, + CheckSquare, + XSquare, + Zap, + Snowflake, + Box } from 'lucide-react'; +export interface QualityCheckRequirement { + id: string; + name: string; + stage: ProcessStage; + isRequired: boolean; + isCritical: boolean; + status: 'pending' | 'completed' | 'failed' | 'skipped'; + checkType: 'visual' | 'measurement' | 'temperature' | 'weight' | 'boolean'; +} + +export interface ProcessStageInfo { + current: ProcessStage; + history: Array<{ + stage: ProcessStage; + timestamp: string; + duration?: number; + }>; + pendingQualityChecks: QualityCheckRequirement[]; + completedQualityChecks: QualityCheckRequirement[]; +} + +export type ProcessStage = 'mixing' | 'proofing' | 'shaping' | 'baking' | 'cooling' | 'packaging' | 'finishing'; + export interface ProductionOrder { id: string; product: string; @@ -39,6 +73,7 @@ export interface ProductionOrder { unit: string; available: boolean; }>; + processStage?: ProcessStageInfo; } export interface ProductionPlansProps { @@ -50,6 +85,56 @@ export interface ProductionPlansProps { onViewAllPlans?: () => void; } +const getProcessStageIcon = (stage: ProcessStage) => { + switch (stage) { + case 'mixing': return ChefHat; + case 'proofing': return Timer; + case 'shaping': return Package; + case 'baking': return Flame; + case 'cooling': return Snowflake; + case 'packaging': return Box; + case 'finishing': return CheckCircle; + default: return CircleDot; + } +}; + +const getProcessStageColor = (stage: ProcessStage) => { + switch (stage) { + case 'mixing': return 'var(--color-info)'; + case 'proofing': return 'var(--color-warning)'; + case 'shaping': return 'var(--color-primary)'; + case 'baking': return 'var(--color-error)'; + case 'cooling': return 'var(--color-info)'; + case 'packaging': return 'var(--color-success)'; + case 'finishing': return 'var(--color-success)'; + default: return 'var(--color-gray)'; + } +}; + +const getProcessStageLabel = (stage: ProcessStage) => { + switch (stage) { + case 'mixing': return 'Mezclado'; + case 'proofing': return 'Fermentado'; + case 'shaping': return 'Formado'; + case 'baking': return 'Horneado'; + case 'cooling': return 'Enfriado'; + case 'packaging': return 'Empaquetado'; + case 'finishing': return 'Acabado'; + default: return 'Sin etapa'; + } +}; + +const getQualityCheckIcon = (checkType: string) => { + switch (checkType) { + case 'visual': return Eye; + case 'measurement': return Scale; + case 'temperature': return Thermometer; + case 'weight': return Scale; + case 'boolean': return CheckSquare; + default: return FlaskRound; + } +}; + const ProductionPlansToday: React.FC = ({ className, orders = [], @@ -78,7 +163,38 @@ const ProductionPlansToday: React.FC = ({ { name: 'Levadura', quantity: 0.5, unit: 'kg', available: true }, { name: 'Sal', quantity: 0.2, unit: 'kg', available: true }, { name: 'Agua', quantity: 3, unit: 'L', available: true } - ] + ], + processStage: { + current: 'baking', + history: [ + { stage: 'mixing', timestamp: '06:00', duration: 30 }, + { stage: 'proofing', timestamp: '06:30', duration: 90 }, + { stage: 'shaping', timestamp: '08:00', duration: 15 }, + { stage: 'baking', timestamp: '08:15' } + ], + pendingQualityChecks: [ + { + id: 'qc1', + name: 'Control de temperatura interna', + stage: 'baking', + isRequired: true, + isCritical: true, + status: 'pending', + checkType: 'temperature' + } + ], + completedQualityChecks: [ + { + id: 'qc2', + name: 'Inspección visual de masa', + stage: 'mixing', + isRequired: true, + isCritical: false, + status: 'completed', + checkType: 'visual' + } + ] + } }, { id: '2', @@ -99,7 +215,34 @@ const ProductionPlansToday: React.FC = ({ { name: 'Masa de croissant', quantity: 3, unit: 'kg', available: true }, { name: 'Mantequilla', quantity: 1, unit: 'kg', available: false }, { name: 'Huevo', quantity: 6, unit: 'unidades', available: true } - ] + ], + processStage: { + current: 'shaping', + history: [ + { stage: 'proofing', timestamp: '07:30', duration: 120 } + ], + pendingQualityChecks: [ + { + id: 'qc3', + name: 'Verificar formado de hojaldre', + stage: 'shaping', + isRequired: true, + isCritical: false, + status: 'pending', + checkType: 'visual' + }, + { + id: 'qc4', + name: 'Control de peso individual', + stage: 'shaping', + isRequired: false, + isCritical: false, + status: 'pending', + checkType: 'weight' + } + ], + completedQualityChecks: [] + } }, { id: '3', @@ -120,7 +263,39 @@ const ProductionPlansToday: React.FC = ({ { name: 'Levadura', quantity: 0.3, unit: 'kg', available: true }, { name: 'Sal', quantity: 0.15, unit: 'kg', available: true }, { name: 'Agua', quantity: 2.5, unit: 'L', available: true } - ] + ], + processStage: { + current: 'finishing', + history: [ + { stage: 'mixing', timestamp: '05:00', duration: 20 }, + { stage: 'proofing', timestamp: '05:20', duration: 120 }, + { stage: 'shaping', timestamp: '07:20', duration: 30 }, + { stage: 'baking', timestamp: '07:50', duration: 45 }, + { stage: 'cooling', timestamp: '08:35', duration: 30 }, + { stage: 'finishing', timestamp: '09:05' } + ], + pendingQualityChecks: [], + completedQualityChecks: [ + { + id: 'qc5', + name: 'Inspección visual final', + stage: 'finishing', + isRequired: true, + isCritical: false, + status: 'completed', + checkType: 'visual' + }, + { + id: 'qc6', + name: 'Control de temperatura de cocción', + stage: 'baking', + isRequired: true, + isCritical: true, + status: 'completed', + checkType: 'temperature' + } + ] + } }, { id: '4', @@ -143,7 +318,23 @@ const ProductionPlansToday: React.FC = ({ { name: 'Huevos', quantity: 24, unit: 'unidades', available: true }, { name: 'Mantequilla', quantity: 1, unit: 'kg', available: false }, { name: 'Vainilla', quantity: 50, unit: 'ml', available: true } - ] + ], + processStage: { + current: 'mixing', + history: [], + pendingQualityChecks: [ + { + id: 'qc7', + name: 'Verificar consistencia de masa', + stage: 'mixing', + isRequired: true, + isCritical: false, + status: 'pending', + checkType: 'visual' + } + ], + completedQualityChecks: [] + } } ]; @@ -214,6 +405,63 @@ const ProductionPlansToday: React.FC = ({ const completedOrders = displayOrders.filter(order => order.status === 'completed').length; const delayedOrders = displayOrders.filter(order => order.status === 'delayed').length; + // Cross-batch quality overview calculations + const totalPendingQualityChecks = displayOrders.reduce((total, order) => + total + (order.processStage?.pendingQualityChecks.length || 0), 0); + const criticalPendingQualityChecks = displayOrders.reduce((total, order) => + total + (order.processStage?.pendingQualityChecks.filter(qc => qc.isCritical).length || 0), 0); + const ordersBlockedByQuality = displayOrders.filter(order => + order.processStage?.pendingQualityChecks.some(qc => qc.isCritical && qc.isRequired) || false).length; + + // Helper function to create enhanced metadata with process stage info + const createEnhancedMetadata = (order: ProductionOrder) => { + const baseMetadata = [ + `⏰ Inicio: ${order.startTime}`, + `⏱️ Duración: ${formatDuration(order.estimatedDuration)}`, + ...(order.ovenNumber ? [`🔥 Horno ${order.ovenNumber} - ${order.temperature}°C`] : []) + ]; + + if (order.processStage) { + const { current, pendingQualityChecks, completedQualityChecks } = order.processStage; + const currentStageIcon = getProcessStageIcon(current); + const currentStageLabel = getProcessStageLabel(current); + + // Add current stage info + baseMetadata.push(`🔄 Etapa: ${currentStageLabel}`); + + // Add quality check info + if (pendingQualityChecks.length > 0) { + const criticalPending = pendingQualityChecks.filter(qc => qc.isCritical).length; + const requiredPending = pendingQualityChecks.filter(qc => qc.isRequired).length; + + if (criticalPending > 0) { + baseMetadata.push(`🚨 ${criticalPending} controles críticos pendientes`); + } else if (requiredPending > 0) { + baseMetadata.push(`✅ ${requiredPending} controles requeridos pendientes`); + } else { + baseMetadata.push(`📋 ${pendingQualityChecks.length} controles opcionales pendientes`); + } + } + + if (completedQualityChecks.length > 0) { + baseMetadata.push(`✅ ${completedQualityChecks.length} controles completados`); + } + } + + // Add ingredients info + const availableIngredients = order.ingredients.filter(ing => ing.available).length; + const totalIngredients = order.ingredients.length; + const ingredientsReady = availableIngredients === totalIngredients; + baseMetadata.push(`📋 Ingredientes: ${availableIngredients}/${totalIngredients} ${ingredientsReady ? '✅' : '❌'}`); + + // Add notes if any + if (order.notes) { + baseMetadata.push(`📝 ${order.notes}`); + } + + return baseMetadata; + }; + return ( @@ -236,6 +484,21 @@ const ProductionPlansToday: React.FC = ({
+ {ordersBlockedByQuality > 0 && ( + + 🚨 {ordersBlockedByQuality} bloqueadas por calidad + + )} + {criticalPendingQualityChecks > 0 && ordersBlockedByQuality === 0 && ( + + 🔍 {criticalPendingQualityChecks} controles críticos + + )} + {totalPendingQualityChecks > 0 && criticalPendingQualityChecks === 0 && ( + + 📋 {totalPendingQualityChecks} controles pendientes + + )} {delayedOrders > 0 && ( {delayedOrders} retrasadas @@ -273,23 +536,44 @@ const ProductionPlansToday: React.FC = ({
{displayOrders.map((order) => { const statusConfig = getOrderStatusConfig(order); - const availableIngredients = order.ingredients.filter(ing => ing.available).length; - const totalIngredients = order.ingredients.length; - const ingredientsReady = availableIngredients === totalIngredients; + const enhancedMetadata = createEnhancedMetadata(order); + + // Enhanced secondary info that includes stage information + const getSecondaryInfo = () => { + if (order.processStage) { + const currentStageLabel = getProcessStageLabel(order.processStage.current); + return { + label: 'Etapa actual', + value: `${currentStageLabel} • ${order.assignedBaker}` + }; + } + return { + label: 'Panadero asignado', + value: order.assignedBaker + }; + }; + + // Enhanced status indicator with process stage color for active orders + const getEnhancedStatusConfig = () => { + if (order.processStage && order.status === 'in_progress') { + return { + ...statusConfig, + color: getProcessStageColor(order.processStage.current) + }; + } + return statusConfig; + }; return ( = ({ order.progress > 70 ? 'var(--color-info)' : order.progress > 30 ? 'var(--color-warning)' : 'var(--color-error)' } : undefined} - metadata={[ - `⏰ Inicio: ${order.startTime}`, - `⏱️ Duración: ${formatDuration(order.estimatedDuration)}`, - ...(order.ovenNumber ? [`🔥 Horno ${order.ovenNumber} - ${order.temperature}°C`] : []), - `📋 Ingredientes: ${availableIngredients}/${totalIngredients} ${ingredientsReady ? '✅' : '❌'}`, - ...(order.notes ? [`📝 ${order.notes}`] : []) - ]} + metadata={enhancedMetadata} actions={[ ...(order.status === 'pending' ? [{ label: 'Iniciar', @@ -320,6 +598,22 @@ const ProductionPlansToday: React.FC = ({ priority: 'primary' as const, destructive: true }] : []), + // Add quality check action if there are pending quality checks + ...(order.processStage?.pendingQualityChecks.length > 0 ? [{ + label: `${order.processStage.pendingQualityChecks.filter(qc => qc.isCritical).length > 0 ? '🚨 ' : ''}Controles Calidad`, + icon: FlaskRound, + variant: order.processStage.pendingQualityChecks.filter(qc => qc.isCritical).length > 0 ? 'primary' as const : 'outline' as const, + onClick: () => onViewDetails?.(order.id), // This would open the quality check modal + priority: order.processStage.pendingQualityChecks.filter(qc => qc.isCritical).length > 0 ? 'primary' as const : 'secondary' as const + }] : []), + // Add next stage action for orders that can progress + ...(order.status === 'in_progress' && order.processStage && order.processStage.pendingQualityChecks.length === 0 ? [{ + label: 'Siguiente Etapa', + icon: ArrowRight, + variant: 'primary' as const, + onClick: () => console.log(`Advancing stage for order ${order.id}`), + priority: 'primary' as const + }] : []), { label: 'Ver Detalles', icon: ChevronRight, diff --git a/frontend/src/components/domain/production/CompactProcessStageTracker.tsx b/frontend/src/components/domain/production/CompactProcessStageTracker.tsx new file mode 100644 index 00000000..263bde5c --- /dev/null +++ b/frontend/src/components/domain/production/CompactProcessStageTracker.tsx @@ -0,0 +1,303 @@ +import React from 'react'; +import { Badge } from '../../ui/Badge'; +import { Button } from '../../ui/Button'; +import { + ChefHat, + Timer, + Package, + Flame, + Snowflake, + Box, + CheckCircle, + CircleDot, + Eye, + Scale, + Thermometer, + FlaskRound, + CheckSquare, + ArrowRight, + Clock, + AlertTriangle +} from 'lucide-react'; + +export type ProcessStage = 'mixing' | 'proofing' | 'shaping' | 'baking' | 'cooling' | 'packaging' | 'finishing'; + +export interface QualityCheckRequirement { + id: string; + name: string; + stage: ProcessStage; + isRequired: boolean; + isCritical: boolean; + status: 'pending' | 'completed' | 'failed' | 'skipped'; + checkType: 'visual' | 'measurement' | 'temperature' | 'weight' | 'boolean'; +} + +export interface ProcessStageInfo { + current: ProcessStage; + history: Array<{ + stage: ProcessStage; + timestamp: string; + duration?: number; + }>; + pendingQualityChecks: QualityCheckRequirement[]; + completedQualityChecks: QualityCheckRequirement[]; +} + +export interface CompactProcessStageTrackerProps { + processStage: ProcessStageInfo; + onAdvanceStage?: (currentStage: ProcessStage) => void; + onQualityCheck?: (checkId: string) => void; + className?: string; +} + +const getProcessStageIcon = (stage: ProcessStage) => { + switch (stage) { + case 'mixing': return ChefHat; + case 'proofing': return Timer; + case 'shaping': return Package; + case 'baking': return Flame; + case 'cooling': return Snowflake; + case 'packaging': return Box; + case 'finishing': return CheckCircle; + default: return CircleDot; + } +}; + +const getProcessStageColor = (stage: ProcessStage) => { + switch (stage) { + case 'mixing': return 'var(--color-info)'; + case 'proofing': return 'var(--color-warning)'; + case 'shaping': return 'var(--color-primary)'; + case 'baking': return 'var(--color-error)'; + case 'cooling': return 'var(--color-info)'; + case 'packaging': return 'var(--color-success)'; + case 'finishing': return 'var(--color-success)'; + default: return 'var(--color-gray)'; + } +}; + +const getProcessStageLabel = (stage: ProcessStage) => { + switch (stage) { + case 'mixing': return 'Mezclado'; + case 'proofing': return 'Fermentado'; + case 'shaping': return 'Formado'; + case 'baking': return 'Horneado'; + case 'cooling': return 'Enfriado'; + case 'packaging': return 'Empaquetado'; + case 'finishing': return 'Acabado'; + default: return 'Sin etapa'; + } +}; + +const getQualityCheckIcon = (checkType: string) => { + switch (checkType) { + case 'visual': return Eye; + case 'measurement': return Scale; + case 'temperature': return Thermometer; + case 'weight': return Scale; + case 'boolean': return CheckSquare; + default: return FlaskRound; + } +}; + +const formatTime = (timestamp: string) => { + return new Date(timestamp).toLocaleTimeString('es-ES', { + hour: '2-digit', + minute: '2-digit' + }); +}; + +const formatDuration = (minutes?: number) => { + if (!minutes) return ''; + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + if (hours > 0) { + return `${hours}h ${mins}m`; + } + return `${mins}m`; +}; + +const CompactProcessStageTracker: React.FC = ({ + processStage, + onAdvanceStage, + onQualityCheck, + className = '' +}) => { + const allStages: ProcessStage[] = ['mixing', 'proofing', 'shaping', 'baking', 'cooling', 'packaging', 'finishing']; + + const currentStageIndex = allStages.indexOf(processStage.current); + const completedStages = processStage.history.map(h => h.stage); + + const criticalPendingChecks = processStage.pendingQualityChecks.filter(qc => qc.isCritical); + const canAdvanceStage = processStage.pendingQualityChecks.length === 0 && currentStageIndex < allStages.length - 1; + + return ( +
+ {/* Current Stage Header */} +
+
+
+ {React.createElement(getProcessStageIcon(processStage.current), { className: 'w-4 h-4' })} +
+
+

+ {getProcessStageLabel(processStage.current)} +

+

+ Etapa actual +

+
+
+ + {canAdvanceStage && ( + + )} +
+ + {/* Process Timeline */} +
+
+ {allStages.map((stage, index) => { + const StageIcon = getProcessStageIcon(stage); + const isCompleted = completedStages.includes(stage); + const isCurrent = stage === processStage.current; + const stageHistory = processStage.history.find(h => h.stage === stage); + + return ( +
+ {/* Connection Line */} + {index < allStages.length - 1 && ( +
+ )} + + {/* Stage Icon */} +
+ +
+ + {/* Stage Label */} +
+
+ {getProcessStageLabel(stage).split(' ')[0]} +
+ {stageHistory && ( +
+ {formatTime(stageHistory.timestamp)} +
+ )} +
+
+ ); + })} +
+
+ + {/* Quality Checks Section */} + {processStage.pendingQualityChecks.length > 0 && ( +
+
+ +
+ Controles de Calidad Pendientes +
+ {criticalPendingChecks.length > 0 && ( + + {criticalPendingChecks.length} críticos + + )} +
+ +
+ {processStage.pendingQualityChecks.map((check) => { + const CheckIcon = getQualityCheckIcon(check.checkType); + return ( +
+
+ +
+
+ {check.name} +
+
+ {check.isCritical && } + {check.isRequired ? 'Obligatorio' : 'Opcional'} +
+
+
+ + +
+ ); + })} +
+
+ )} + + {/* Completed Quality Checks Summary */} + {processStage.completedQualityChecks.length > 0 && ( +
+
+ + {processStage.completedQualityChecks.length} controles de calidad completados +
+
+ )} + + {/* Stage History Summary */} + {processStage.history.length > 0 && ( +
+
Historial de etapas:
+
+ {processStage.history.map((historyItem, index) => ( +
+ {getProcessStageLabel(historyItem.stage)} + + {formatTime(historyItem.timestamp)} + {historyItem.duration && ` (${formatDuration(historyItem.duration)})`} + +
+ ))} +
+
+ )} +
+ ); +}; + +export default CompactProcessStageTracker; \ No newline at end of file diff --git a/frontend/src/components/domain/production/CreateQualityTemplateModal.tsx b/frontend/src/components/domain/production/CreateQualityTemplateModal.tsx new file mode 100644 index 00000000..50683bdd --- /dev/null +++ b/frontend/src/components/domain/production/CreateQualityTemplateModal.tsx @@ -0,0 +1,406 @@ +import React, { useState } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { + Modal, + Button, + Input, + Textarea, + Select, + Badge, + Card +} from '../../ui'; +import { + QualityCheckType, + ProcessStage, + type QualityCheckTemplateCreate +} from '../../../api/types/qualityTemplates'; +import { useCurrentTenant } from '../../../stores/tenant.store'; + +interface CreateQualityTemplateModalProps { + isOpen: boolean; + onClose: () => void; + onCreateTemplate: (templateData: QualityCheckTemplateCreate) => Promise; + isLoading?: boolean; +} + +const QUALITY_CHECK_TYPE_OPTIONS = [ + { value: QualityCheckType.VISUAL, label: 'Visual - Inspección visual' }, + { value: QualityCheckType.MEASUREMENT, label: 'Medición - Medidas precisas' }, + { value: QualityCheckType.TEMPERATURE, label: 'Temperatura - Control térmico' }, + { value: QualityCheckType.WEIGHT, label: 'Peso - Control de peso' }, + { value: QualityCheckType.BOOLEAN, label: 'Sí/No - Verificación binaria' }, + { value: QualityCheckType.TIMING, label: 'Tiempo - Control temporal' } +]; + +const PROCESS_STAGE_OPTIONS = [ + { value: ProcessStage.MIXING, label: 'Mezclado' }, + { value: ProcessStage.PROOFING, label: 'Fermentación' }, + { value: ProcessStage.SHAPING, label: 'Formado' }, + { value: ProcessStage.BAKING, label: 'Horneado' }, + { value: ProcessStage.COOLING, label: 'Enfriado' }, + { value: ProcessStage.PACKAGING, label: 'Empaquetado' }, + { value: ProcessStage.FINISHING, label: 'Acabado' } +]; + +const CATEGORY_OPTIONS = [ + { value: 'appearance', label: 'Apariencia' }, + { value: 'structure', label: 'Estructura' }, + { value: 'texture', label: 'Textura' }, + { value: 'flavor', label: 'Sabor' }, + { value: 'safety', label: 'Seguridad' }, + { value: 'packaging', label: 'Empaque' }, + { value: 'temperature', label: 'Temperatura' }, + { value: 'weight', label: 'Peso' }, + { value: 'dimensions', label: 'Dimensiones' } +]; + +export const CreateQualityTemplateModal: React.FC = ({ + isOpen, + onClose, + onCreateTemplate, + isLoading = false +}) => { + const currentTenant = useCurrentTenant(); + const [selectedStages, setSelectedStages] = useState([]); + + const { + register, + control, + handleSubmit, + watch, + reset, + formState: { errors, isValid } + } = useForm({ + defaultValues: { + name: '', + template_code: '', + check_type: QualityCheckType.VISUAL, + category: '', + description: '', + instructions: '', + is_active: true, + is_required: false, + is_critical: false, + weight: 1.0, + applicable_stages: [], + created_by: currentTenant?.id || '' + } + }); + + const checkType = watch('check_type'); + const showMeasurementFields = [ + QualityCheckType.MEASUREMENT, + QualityCheckType.TEMPERATURE, + QualityCheckType.WEIGHT + ].includes(checkType); + + const handleStageToggle = (stage: ProcessStage) => { + const newStages = selectedStages.includes(stage) + ? selectedStages.filter(s => s !== stage) + : [...selectedStages, stage]; + + setSelectedStages(newStages); + }; + + const onSubmit = async (data: QualityCheckTemplateCreate) => { + try { + await onCreateTemplate({ + ...data, + applicable_stages: selectedStages.length > 0 ? selectedStages : undefined, + created_by: currentTenant?.id || '' + }); + + // Reset form + reset(); + setSelectedStages([]); + } catch (error) { + console.error('Error creating template:', error); + } + }; + + const handleClose = () => { + reset(); + setSelectedStages([]); + onClose(); + }; + + return ( + +
+ {/* Basic Information */} + +

+ Información Básica +

+ +
+
+ + +
+ +
+ + +
+ +
+ + ( + + )} + /> +
+ +
+ + ( + + )} + /> +
+
+ +
+ +