From 474d7176bf858adda13e2878ea44446100b561c7 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Wed, 24 Sep 2025 15:58:18 +0200 Subject: [PATCH] Add modal to add equipment --- .../domain/equipment/EquipmentModal.tsx | 359 ++++++++++++++++++ frontend/src/locales/en/equipment.json | 27 +- frontend/src/locales/es/equipment.json | 31 +- frontend/src/locales/eu/equipment.json | 26 +- .../operations/maquinaria/MaquinariaPage.tsx | 70 +++- 5 files changed, 502 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/domain/equipment/EquipmentModal.tsx diff --git a/frontend/src/components/domain/equipment/EquipmentModal.tsx b/frontend/src/components/domain/equipment/EquipmentModal.tsx new file mode 100644 index 00000000..e754f9b6 --- /dev/null +++ b/frontend/src/components/domain/equipment/EquipmentModal.tsx @@ -0,0 +1,359 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Building2, Settings, Wrench, Thermometer, Activity, CheckCircle, AlertTriangle, Eye, Edit } from 'lucide-react'; +import { StatusModal, StatusModalSection } from '../../ui/StatusModal/StatusModal'; +import { Equipment } from '../../../types/equipment'; + +interface EquipmentModalProps { + isOpen: boolean; + onClose: () => void; + equipment?: Equipment | null; + onSave: (equipment: Equipment) => void; + mode: 'view' | 'edit' | 'create'; +} + +export const EquipmentModal: React.FC = ({ + isOpen, + onClose, + equipment: initialEquipment, + onSave, + mode +}) => { + const { t } = useTranslation(['equipment', 'common']); + const [currentMode, setCurrentMode] = useState<'view' | 'edit'>(mode === 'create' ? 'edit' : 'view'); + const [isCreating, setIsCreating] = useState(mode === 'create'); + const [equipment, setEquipment] = useState( + initialEquipment || { + id: '', + name: '', + type: 'other', + model: '', + serialNumber: '', + location: '', + status: 'operational', + installDate: new Date().toISOString().split('T')[0], + lastMaintenance: new Date().toISOString().split('T')[0], + nextMaintenance: new Date().toISOString().split('T')[0], + maintenanceInterval: 30, + efficiency: 100, + uptime: 100, + energyUsage: 0, + utilizationToday: 0, + alerts: [], + maintenanceHistory: [], + specifications: { + power: 0, + capacity: 0, + dimensions: { width: 0, height: 0, depth: 0 }, + weight: 0 + } + } as Equipment + ); + + const handleSave = async () => { + if (equipment) { + await onSave(equipment); + setCurrentMode('view'); + } + }; + + const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: string | number) => { + if (!equipment) return; + + const newEquipment = { ...equipment }; + const sections = getSections(); + const section = sections[sectionIndex]; + const field = section.fields[fieldIndex]; + + // Map field labels to equipment properties using translation keys + // This will work regardless of the current language + const fieldLabelKeyMap: { [key: string]: string } = { + [t('fields.name')]: 'name', + [t('fields.type')]: 'type', + [t('fields.model')]: 'model', + [t('fields.serial_number')]: 'serialNumber', + [t('fields.location')]: 'location', + [t('fields.status')]: 'status', + [t('fields.install_date')]: 'installDate', + [t('fields.last_maintenance')]: 'lastMaintenance', + [t('fields.next_maintenance')]: 'nextMaintenance', + [t('fields.maintenance_interval')]: 'maintenanceInterval', + [t('fields.efficiency')]: 'efficiency', + [t('fields.uptime')]: 'uptime', + [t('fields.energy_usage')]: 'energyUsage', + [t('fields.utilization_today')]: 'utilizationToday', + [t('fields.specifications.capacity')]: 'capacity', + [t('fields.specifications.power')]: 'power', + [t('fields.specifications.weight')]: 'weight', + [t('fields.specifications.width')]: 'width', + [t('fields.specifications.height')]: 'height', + [t('fields.specifications.depth')]: 'depth' + }; + + const propertyName = fieldLabelKeyMap[field.label]; + if (propertyName) { + // Handle nested specifications + if (propertyName === 'width' || propertyName === 'height' || propertyName === 'depth') { + newEquipment.specifications = { + ...newEquipment.specifications, + dimensions: { + ...newEquipment.specifications.dimensions, + [propertyName]: value + } + }; + } else if (propertyName in newEquipment.specifications && propertyName !== 'dimensions') { + newEquipment.specifications = { + ...newEquipment.specifications, + [propertyName]: value + }; + } else { + (newEquipment as any)[propertyName] = value; + } + setEquipment(newEquipment); + } + }; + + const getSections = (): StatusModalSection[] => { + if (!equipment) return []; + + const equipmentTypes = [ + { label: t('equipment_types.oven'), value: 'oven' }, + { label: t('equipment_types.mixer'), value: 'mixer' }, + { label: t('equipment_types.proofer'), value: 'proofer' }, + { label: t('equipment_types.freezer'), value: 'freezer' }, + { label: t('equipment_types.packaging'), value: 'packaging' }, + { label: t('equipment_types.other'), value: 'other' } + ]; + + const equipmentStatuses = [ + { label: t('equipment_status.operational'), value: 'operational' }, + { label: t('equipment_status.warning'), value: 'warning' }, + { label: t('equipment_status.maintenance'), value: 'maintenance' }, + { label: t('equipment_status.down'), value: 'down' } + ]; + + return [ + { + title: t('sections.equipment_info'), + icon: Settings, + fields: [ + { + label: t('fields.name'), + value: equipment.name || '', + type: 'text', + highlight: true, + editable: true, + required: true, + placeholder: t('placeholders.name') + }, + { + label: t('fields.type'), + value: equipment.type || 'other', + type: 'select', + editable: true, + options: equipmentTypes + }, + { + label: t('fields.model'), + value: equipment.model || '', + type: 'text', + editable: true, + placeholder: t('placeholders.model') + }, + { + label: t('fields.serial_number'), + value: equipment.serialNumber || '', + type: 'text', + editable: true, + placeholder: t('placeholders.serial_number') + }, + { + label: t('fields.location'), + value: equipment.location || '', + type: 'text', + editable: true, + placeholder: t('placeholders.location') + }, + { + label: t('fields.status'), + value: equipment.status || 'operational', + type: 'select', + editable: true, + options: equipmentStatuses + } + ] + }, + { + title: t('sections.performance'), + icon: CheckCircle, + fields: [ + { + label: t('fields.efficiency'), + value: equipment.efficiency || 0, + type: 'number', + editable: true, + placeholder: '100' + }, + { + label: t('fields.uptime'), + value: equipment.uptime || 0, + type: 'number', + editable: true, + placeholder: '100' + }, + { + label: t('fields.energy_usage'), + value: equipment.energyUsage || 0, + type: 'number', + editable: true, + placeholder: '0' + }, + { + label: t('fields.utilization_today'), + value: equipment.utilizationToday || 0, + type: 'number', + editable: true, + placeholder: '0' + } + ] + }, + { + title: t('sections.maintenance'), + icon: Wrench, + fields: [ + { + label: t('fields.install_date'), + value: equipment.installDate || new Date().toISOString().split('T')[0], + type: 'date', + editable: true + }, + { + label: t('fields.last_maintenance'), + value: equipment.lastMaintenance || new Date().toISOString().split('T')[0], + type: 'date', + editable: true + }, + { + label: t('fields.next_maintenance'), + value: equipment.nextMaintenance || new Date().toISOString().split('T')[0], + type: 'date', + editable: true + }, + { + label: t('fields.maintenance_interval'), + value: equipment.maintenanceInterval || 30, + type: 'number', + editable: true, + placeholder: '30' + } + ] + }, + { + title: t('sections.specifications'), + icon: Building2, + fields: [ + { + label: t('fields.specifications.power'), + value: equipment.specifications.power || 0, + type: 'number', + editable: true, + placeholder: '0' + }, + { + label: t('fields.specifications.capacity'), + value: equipment.specifications.capacity || 0, + type: 'number', + editable: true, + placeholder: '0' + }, + { + label: t('fields.specifications.width'), + value: equipment.specifications.dimensions.width || 0, + type: 'number', + editable: true, + placeholder: '0' + }, + { + label: t('fields.specifications.height'), + value: equipment.specifications.dimensions.height || 0, + type: 'number', + editable: true, + placeholder: '0' + }, + { + label: t('fields.specifications.depth'), + value: equipment.specifications.dimensions.depth || 0, + type: 'number', + editable: true, + placeholder: '0' + }, + { + label: t('fields.specifications.weight'), + value: equipment.specifications.weight || 0, + type: 'number', + editable: true, + placeholder: '0' + } + ] + } + ]; + }; + + const getEquipmentStatusConfig = () => { + if (!equipment) return undefined; + + const configs = { + operational: { + color: '#10B981', + text: t('equipment_status.operational'), + icon: CheckCircle, + isCritical: false, + isHighlight: false + }, + warning: { + color: '#F59E0B', + text: t('equipment_status.warning'), + icon: AlertTriangle, + isCritical: false, + isHighlight: false + }, + maintenance: { + color: '#3B82F6', + text: t('equipment_status.maintenance'), + icon: Wrench, + isCritical: false, + isHighlight: false + }, + down: { + color: '#EF4444', + text: t('equipment_status.down'), + icon: AlertTriangle, + isCritical: true, + isHighlight: false + } + }; + + return configs[equipment.status]; + }; + + return ( + { + onClose(); + setCurrentMode('view'); + setIsCreating(false); + }} + mode={currentMode} + onModeChange={setCurrentMode} + title={isCreating ? t('actions.add_equipment') : equipment?.name || t('common:forms.untitled')} + subtitle={isCreating ? t('sections.create_equipment_subtitle') : `${equipment?.model || ''} - ${equipment?.serialNumber || ''}`} + statusIndicator={getEquipmentStatusConfig()} + size="lg" + sections={getSections()} + showDefaultActions={true} + onSave={handleSave} + onFieldChange={handleFieldChange} + /> + ); +}; \ No newline at end of file diff --git a/frontend/src/locales/en/equipment.json b/frontend/src/locales/en/equipment.json index bae43921..6f2afdca 100644 --- a/frontend/src/locales/en/equipment.json +++ b/frontend/src/locales/en/equipment.json @@ -7,7 +7,7 @@ "down": "Out of Service", "warning": "Warning" }, - "equipment_type": { + "equipment_types": { "oven": "Oven", "mixer": "Mixer", "proofer": "Proofing Chamber", @@ -34,7 +34,17 @@ "power": "Power", "capacity": "Capacity", "weight": "Weight", - "parts": "Parts" + "parts": "Parts", + "utilization_today": "Utilization Today", + "edit": "Edit", + "specifications": { + "power": "Power", + "capacity": "Capacity", + "weight": "Weight", + "width": "Width", + "height": "Height", + "depth": "Depth" + } }, "actions": { "add_equipment": "Add Equipment", @@ -57,6 +67,19 @@ "overdue_maintenance": "Overdue Maintenance", "low_efficiency": "Low Efficiency" }, + "sections": { + "equipment_info": "Equipment Information", + "performance": "Performance", + "maintenance": "Maintenance Information", + "specifications": "Specifications", + "create_equipment_subtitle": "Fill in the details for the new equipment" + }, + "placeholders": { + "name": "Enter equipment name", + "model": "Enter equipment model", + "serial_number": "Enter serial number", + "location": "Enter location" + }, "descriptions": { "equipment_efficiency": "Current equipment efficiency percentage", "uptime_percentage": "Percentage of uptime", diff --git a/frontend/src/locales/es/equipment.json b/frontend/src/locales/es/equipment.json index 18c58be8..e4acdfc3 100644 --- a/frontend/src/locales/es/equipment.json +++ b/frontend/src/locales/es/equipment.json @@ -7,7 +7,7 @@ "down": "Fuera de Servicio", "warning": "Advertencia" }, - "equipment_type": { + "equipment_types": { "oven": "Horno", "mixer": "Batidora", "proofer": "Cámara de Fermentación", @@ -33,7 +33,17 @@ "target_temperature": "Temperatura Objetivo", "power": "Potencia", "capacity": "Capacidad", - "weight": "Peso" + "weight": "Peso", + "utilization_today": "Utilización Hoy", + "edit": "Editar", + "specifications": { + "power": "Potencia", + "capacity": "Capacidad", + "weight": "Peso", + "width": "Ancho", + "height": "Alto", + "depth": "Profundidad" + } }, "actions": { "add_equipment": "Agregar Equipo", @@ -43,7 +53,9 @@ "view_maintenance_history": "Ver Historial de Mantenimiento", "acknowledge_alert": "Reconocer Alerta", "view_details": "Ver Detalles", - "view_history": "Ver Historial" + "view_history": "Ver Historial", + "close": "Cerrar", + "cost": "Costo" }, "labels": { "total_equipment": "Total de Equipos", @@ -54,6 +66,19 @@ "overdue_maintenance": "Mantenimiento Atrasado", "low_efficiency": "Baja Eficiencia" }, + "sections": { + "equipment_info": "Información de Equipo", + "performance": "Rendimiento", + "maintenance": "Información de Mantenimiento", + "specifications": "Especificaciones", + "create_equipment_subtitle": "Completa los detalles del nuevo equipo" + }, + "placeholders": { + "name": "Introduce el nombre del equipo", + "model": "Introduce el modelo del equipo", + "serial_number": "Introduce el número de serie", + "location": "Introduce la ubicación" + }, "descriptions": { "equipment_efficiency": "Porcentaje de eficiencia actual de los equipos", "uptime_percentage": "Porcentaje de tiempo de funcionamiento", diff --git a/frontend/src/locales/eu/equipment.json b/frontend/src/locales/eu/equipment.json index 503265f4..e930d6a9 100644 --- a/frontend/src/locales/eu/equipment.json +++ b/frontend/src/locales/eu/equipment.json @@ -7,7 +7,7 @@ "down": "Zerbitzuetik kanpo", "warning": "Abisua" }, - "equipment_type": { + "equipment_types": { "oven": "Labean", "mixer": "Nahaste-makina", "proofer": "Igoera-gela", @@ -34,7 +34,16 @@ "power": "Potentzia", "capacity": "Edukiera", "weight": "Pisua", - "parts": "Piezak" + "utilization_today": "Gaurko erabilera", + "edit": "Editatu", + "specifications": { + "power": "Potentzia", + "capacity": "Edukiera", + "weight": "Pisua", + "width": "Zabalera", + "height": "Altuera", + "depth": "Sakonera" + } }, "actions": { "add_equipment": "Gehitu makina", @@ -57,6 +66,19 @@ "overdue_maintenance": "Mantentzea atzeratuta", "low_efficiency": "Eraginkortasun baxua" }, + "sections": { + "equipment_info": "Makinaren informazioa", + "performance": "Errendimendua", + "maintenance": "Mantentze informazioa", + "specifications": "Zehaztapenak", + "create_equipment_subtitle": "Bete makinaren xehetasunak" + }, + "placeholders": { + "name": "Sartu makinaren izena", + "model": "Sartu makinaren modeloa", + "serial_number": "Sartu serie-zenbakia", + "location": "Sartu kokapena" + }, "descriptions": { "equipment_efficiency": "Uneko makinaren eraginkortasun-ehunekoa", "uptime_percentage": "Funtzionamendu-denboraren ehunekoa", diff --git a/frontend/src/pages/app/operations/maquinaria/MaquinariaPage.tsx b/frontend/src/pages/app/operations/maquinaria/MaquinariaPage.tsx index 1e189bf7..0e3dd52b 100644 --- a/frontend/src/pages/app/operations/maquinaria/MaquinariaPage.tsx +++ b/frontend/src/pages/app/operations/maquinaria/MaquinariaPage.tsx @@ -7,6 +7,7 @@ import { LoadingSpinner } from '../../../../components/shared'; import { PageHeader } from '../../../../components/layout'; import { useCurrentTenant } from '../../../../stores/tenant.store'; import { Equipment } from '../../../../types/equipment'; +import { EquipmentModal } from '../../../../components/domain/equipment/EquipmentModal'; const MOCK_EQUIPMENT: Equipment[] = [ { @@ -152,19 +153,52 @@ const MaquinariaPage: React.FC = () => { const [statusFilter, setStatusFilter] = useState('all'); const [selectedItem, setSelectedItem] = useState(null); const [showMaintenanceModal, setShowMaintenanceModal] = useState(false); + const [showEquipmentModal, setShowEquipmentModal] = useState(false); + const [equipmentModalMode, setEquipmentModalMode] = useState<'view' | 'edit' | 'create'>('create'); + const [selectedEquipment, setSelectedEquipment] = useState(null); const currentTenant = useCurrentTenant(); const tenantId = currentTenant?.id || ''; // Mock functions for equipment actions - these would be replaced with actual API calls const handleCreateEquipment = () => { - console.log('Create new equipment'); - // Implementation would go here + setSelectedEquipment({ + id: '', + name: '', + type: 'other', + model: '', + serialNumber: '', + location: '', + status: 'operational', + installDate: new Date().toISOString().split('T')[0], + lastMaintenance: new Date().toISOString().split('T')[0], + nextMaintenance: new Date().toISOString().split('T')[0], + maintenanceInterval: 30, + efficiency: 100, + uptime: 100, + energyUsage: 0, + utilizationToday: 0, + alerts: [], + maintenanceHistory: [], + specifications: { + power: 0, + capacity: 0, + dimensions: { width: 0, height: 0, depth: 0 }, + weight: 0 + } + } as Equipment); + setEquipmentModalMode('create'); + setShowEquipmentModal(true); }; const handleEditEquipment = (equipmentId: string) => { - console.log('Edit equipment:', equipmentId); - // Implementation would go here + // Find the equipment to edit + const equipmentToEdit = MOCK_EQUIPMENT.find(eq => eq.id === equipmentId); + if (equipmentToEdit) { + setSelectedEquipment(equipmentToEdit); + setEquipmentModalMode('edit'); + setShowEquipmentModal(true); + } }; const handleScheduleMaintenance = (equipmentId: string) => { @@ -182,6 +216,14 @@ const MaquinariaPage: React.FC = () => { // Implementation would go here }; + const handleSaveEquipment = (equipment: Equipment) => { + console.log('Saving equipment:', equipment); + // In a real implementation, you would save to the API + // For now, just close the modal + setShowEquipmentModal(false); + // Refresh equipment list if needed + }; + const filteredEquipment = useMemo(() => { return MOCK_EQUIPMENT.filter(eq => { const matchesSearch = !searchTerm || @@ -363,6 +405,12 @@ const MaquinariaPage: React.FC = () => { priority: 'primary', onClick: () => handleShowMaintenanceDetails(equipment) }, + { + label: t('actions.edit'), + icon: Edit, + priority: 'secondary', + onClick: () => handleEditEquipment(equipment.id) + }, { label: t('actions.view_history'), icon: History, @@ -578,6 +626,20 @@ const MaquinariaPage: React.FC = () => { )} + + {/* Equipment Modal */} + {showEquipmentModal && ( + { + setShowEquipmentModal(false); + setSelectedEquipment(null); + }} + equipment={selectedEquipment} + onSave={handleSaveEquipment} + mode={equipmentModalMode} + /> + )} ); };