Registro y elevación de componentes base (ADN, Scripts, Servicios)

This commit is contained in:
Ricardo Monla
2026-02-15 20:37:53 -03:00
parent 9e9bf62c68
commit c865a7772b
25279 changed files with 3354766 additions and 0 deletions
@@ -0,0 +1,199 @@
import { useState } from 'react'
import toast from 'react-hot-toast'
export interface EntityManagementState<T> {
showEditForm: boolean
setShowEditForm: (show: boolean) => void
editingEntity: T | null
setEditingEntity: (entity: T | null) => void
showCreateForm: boolean
setShowCreateForm: (show: boolean) => void
}
export interface EntityManagementActions<T> {
handleCreate: (data: Partial<T>) => Promise<void>
handleEdit: (id: number) => Promise<void>
handleUpdate: (data: Partial<T>) => Promise<void>
handleDelete: (id: number) => Promise<void>
handleViewProfile: (id: number) => void
}
export interface EntityStore<T> {
entities?: T[]
loading: boolean
error: string | null
pagination?: {
page: number
limit: number
total: number
pages: number
}
filters: Record<string, any>
fetchEntities: (page?: number) => Promise<void>
createEntity: (data: Partial<T>) => Promise<void>
updateEntity: (id: number, data: Partial<T>) => Promise<void>
deleteEntity: (id: number) => Promise<void>
setFilters: (filters: Record<string, any>) => void
clearFilters: () => void
}
export const useEntityManagement = <T extends { id: number; name?: string; full_name?: string }>(
store: EntityStore<T> & { [key: string]: any },
entityName: string,
entityNamePlural: string = entityName + 's',
entitiesKey: string = 'entities'
): EntityManagementState<T> & EntityManagementActions<T> => {
const [showEditForm, setShowEditForm] = useState(false)
const [editingEntity, setEditingEntity] = useState<T | null>(null)
const [showCreateForm, setShowCreateForm] = useState(false)
const handleCreate = async (data: Partial<T>) => {
try {
await store.createEntity(data)
toast.success(`${entityName} creado exitosamente`)
setShowCreateForm(false)
} catch (error) {
// Error handled by store
}
}
const handleEdit = async (id: number) => {
console.log('[DEBUG] handleEdit called with id:', id)
console.log('[DEBUG] entitiesKey:', entitiesKey)
console.log('[DEBUG] store[entitiesKey]:', store[entitiesKey])
console.log('[DEBUG] store[entitiesKey] type:', typeof store[entitiesKey])
console.log('[DEBUG] store[entitiesKey] length:', store[entitiesKey]?.length)
console.log('[DEBUG] entityName:', entityName)
const entities = store[entitiesKey]
if (!entities || !Array.isArray(entities)) {
console.error('[DEBUG] store[entitiesKey] is not an array or is undefined')
return
}
const entity = entities.find(e => e.id === id)
console.log('[DEBUG] Found entity:', entity)
console.log('[DEBUG] Entity id type:', typeof entity?.id, 'Entity id value:', entity?.id)
if (entity) {
console.log('[DEBUG] Setting editingEntity and showEditForm')
setEditingEntity(entity)
setShowEditForm(true)
// Scroll suave al formulario de edición con animación mejorada
setTimeout(() => {
const editForm = document.getElementById('editFormSection')
console.log('[DEBUG] Looking for editFormSection element:', editForm)
if (editForm) {
console.log('[DEBUG] Scrolling to edit form')
editForm.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
// Agregar clase para animación adicional
editForm.classList.add('highlight-form')
setTimeout(() => {
editForm.classList.remove('highlight-form')
}, 2000)
} else {
console.warn('[DEBUG] editFormSection element not found')
}
}, 150)
} else {
console.warn(`Entity with id ${id} not found in store. Available entities:`, entities.map(e => e.id))
}
}
const handleUpdate = async (data: Partial<T>) => {
console.log('[DEBUG] handleUpdate called with data:', data)
console.log('[DEBUG] editingEntity:', editingEntity)
console.log('[DEBUG] store.updateEntity available:', typeof store.updateEntity)
if (!editingEntity) {
console.warn('[DEBUG] No editingEntity found, returning early')
return
}
try {
// Filtrar solo los campos que realmente cambiaron y son válidos para actualizar
const updateData: Partial<T> = {}
// Para tareas, solo enviar campos específicos que pueden actualizarse
if (entityName === 'tareas') {
const allowedFields = ['title', 'description', 'technician_id', 'status', 'priority', 'due_date']
allowedFields.forEach(key => {
const currentValue = editingEntity[key as keyof T]
const newValue = data[key as keyof T]
// Comparar valores y solo incluir si son diferentes
if (newValue !== undefined && newValue !== currentValue) {
updateData[key as keyof T] = newValue
}
})
} else {
// Para otras entidades, copiar todos los datos
Object.assign(updateData, data)
}
console.log('[DEBUG] Filtered update data:', updateData)
if (Object.keys(updateData).length === 0) {
console.log('[DEBUG] No changes detected, closing form')
setShowEditForm(false)
setEditingEntity(null)
return
}
console.log('[DEBUG] Calling store.updateEntity with id:', editingEntity.id)
console.log('[DEBUG] Data to send:', updateData)
await store.updateEntity(editingEntity.id, updateData)
console.log('[DEBUG] Update successful, showing success toast')
toast.success(`${entityName} actualizado exitosamente`)
console.log('[DEBUG] Setting showEditForm to false and editingEntity to null')
setShowEditForm(false)
setEditingEntity(null)
console.log('[DEBUG] Form should be closed now')
} catch (error) {
console.error('[DEBUG] Error in handleUpdate:', error)
console.error('[DEBUG] Error details:', error instanceof Error ? error.message : error)
toast.error(`Error al actualizar ${entityName.toLowerCase()}: ${error instanceof Error ? error.message : 'Error desconocido'}`)
}
}
const handleDelete = async (id: number) => {
if (window.confirm(`¿Está seguro de eliminar este ${entityName.toLowerCase()}?`)) {
try {
await store.deleteEntity(id)
toast.success(`${entityName} eliminado exitosamente`)
} catch (error) {
// Error handled by store
}
}
}
const handleViewProfile = (id: number) => {
const entities = store[entitiesKey]
const entity = entities?.find(e => e.id === id)
if (entity) {
toast.info(`Ver detalles del ${entityName.toLowerCase()}: ${entity.full_name || entity.name || 'Sin nombre'}`)
}
}
return {
// State
showEditForm,
setShowEditForm,
editingEntity,
setEditingEntity,
showCreateForm,
setShowCreateForm,
// Actions
handleCreate,
handleEdit,
handleUpdate,
handleDelete,
handleViewProfile
}
}
@@ -0,0 +1,352 @@
import { useState, useCallback, useEffect } from 'react'
import toast from 'react-hot-toast'
export interface AssignedResource {
id: number
name: string
category?: string
status?: string
location?: string
}
export interface ResourceOption {
id: number
name: string
category?: string
status?: string
location?: string
}
export interface ResourceAssignmentData {
entity_type: 'tarea' | 'usuario' | 'tecnico' | 'recurso'
entity_id: number
resource_id: number
}
export interface UseResourceAssignmentState {
assignedResources: AssignedResource[]
availableResources: ResourceOption[]
loading: boolean
error: string | null
}
export interface UseResourceAssignmentActions {
assignResource: (resourceId: number) => Promise<boolean>
unassignResource: (resourceId: number) => Promise<boolean>
loadAssignedResources: () => Promise<void>
loadAvailableResources: () => Promise<void>
refreshAssignments: () => Promise<void>
}
export type UseResourceAssignment = UseResourceAssignmentState & UseResourceAssignmentActions
export const useResourceAssignment = (
entityType: 'tarea' | 'usuario' | 'tecnico' | 'recurso',
entityId: number
): UseResourceAssignment => {
// Use relative paths for Vite proxy
const API_BASE = '/api'
const [state, setState] = useState<UseResourceAssignmentState>({
assignedResources: [],
availableResources: [],
loading: false,
error: null
})
const setLoading = useCallback((loading: boolean) => {
setState(prev => ({ ...prev, loading }))
}, [])
const setError = useCallback((error: string | null) => {
setState(prev => ({ ...prev, error }))
}, [])
// Load currently assigned resources
const loadAssignedResources = useCallback(async () => {
setLoading(true)
setError(null)
try {
let endpoint = ''
let queryParams = ''
// Build endpoint based on entity type
switch (entityType) {
case 'tarea':
endpoint = `${API_BASE}/tarea-recursos/tareas/${entityId}/recursos`
break
case 'usuario':
endpoint = `${API_BASE}/usuarios_relacionados/usuarios/${entityId}/recursos`
break
case 'tecnico':
endpoint = `${API_BASE}/tecnico-recursos/tecnicos/${entityId}/recursos`
break
case 'recurso':
endpoint = `${API_BASE}/recursos-asignados/recursos/${entityId}/asignaciones`
break
default:
endpoint = `${API_BASE}/tarea-recursos/tareas/${entityId}/recursos`
}
const response = await fetch(endpoint)
if (!response.ok) {
throw new Error(`Error al cargar recursos asignados: ${response.status}`)
}
const data = await response.json()
if (data.success && data.data) {
// Handle different response structures
let resources = []
if (data.data.assignments) {
// Backend returns { assignments: [...] }
resources = data.data.assignments
} else if (Array.isArray(data.data)) {
// Direct array response
resources = data.data
} else {
resources = []
}
// Map resources to expected format
const mappedResources = resources.map((resource: any) => {
// Handle different naming patterns from backend
const name = resource.recurso_name || resource.name || resource.resource_name || 'Recurso desconocido'
const category = resource.recurso_category || resource.category
const status = resource.recurso_status || resource.status
const location = resource.recurso_location || resource.location
const id = resource.recurso_id || resource.id || resource.resource_id
return {
id: id || 0,
name: name,
category: category,
status: status,
location: location
}
}).filter(resource => resource.id) // Filter out invalid resources
setState(prev => ({
...prev,
assignedResources: mappedResources
}))
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error desconocido'
setError(errorMessage)
console.error('Error loading assigned resources:', error)
// Don't show toast error for missing assignments, it's normal for new entities
} finally {
setLoading(false)
}
}, [entityType, entityId, setLoading, setError])
// Load available resources for assignment
const loadAvailableResources = useCallback(async () => {
try {
const response = await fetch(`${API_BASE}/recursos?status=available`)
if (!response.ok) {
throw new Error(`Error al cargar recursos disponibles: ${response.status}`)
}
const data = await response.json()
if (data.success && data.data) {
const resources = Array.isArray(data.data) ? data.data : data.data.recursos || []
setState(prev => ({
...prev,
availableResources: resources.map((resource: any) => ({
id: resource.id,
name: resource.name,
category: resource.category,
status: resource.status,
location: resource.location
}))
}))
}
} catch (error) {
console.error('Error loading available resources:', error)
// Don't show error toast for available resources as it's not critical
}
}, [])
// Assign a resource to the entity
const assignResource = useCallback(async (resourceId: number): Promise<boolean> => {
setLoading(true)
setError(null)
try {
// Build endpoint based on entity type
let endpoint = ''
let requestBody = {}
switch (entityType) {
case 'tarea':
endpoint = `${API_BASE}/tarea-recursos/tareas/${entityId}/recursos`
requestBody = { recurso_id: resourceId }
break
case 'usuario':
endpoint = `${API_BASE}/usuarios_relacionados/usuarios/${entityId}/recursos`
requestBody = { resource_id: resourceId }
break
case 'tecnico':
endpoint = `${API_BASE}/tecnico-recursos/tecnicos/${entityId}/recursos`
requestBody = { resource_id: resourceId }
break
case 'recurso':
endpoint = `${API_BASE}/recursos-asignados/recursos/${entityId}/asignaciones`
requestBody = { entity_type: 'tarea', entity_id: entityId, resource_id: resourceId }
break
default:
throw new Error('Tipo de entidad no soportado')
}
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
if (!response.ok) {
if (response.status === 409) {
const errorData = await response.json().catch(() => ({}))
const message = errorData.message || 'Error de conflicto al asignar recurso'
throw new Error(message)
} else {
throw new Error(`Error al asignar recurso: ${response.status}`)
}
}
const data = await response.json()
if (data.success) {
toast.success('Recurso relacionado exitosamente')
await refreshAssignments()
return true
} else {
throw new Error(data.message || 'Error al asignar recurso')
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error desconocido'
setError(errorMessage)
console.error('Error assigning resource:', error)
// Don't show toast for now due to missing backend endpoints
return false
} finally {
setLoading(false)
}
}, [entityType, entityId, setLoading, setError])
// Unassign a resource from the entity
const unassignResource = useCallback(async (resourceId: number): Promise<boolean> => {
setLoading(true)
setError(null)
try {
// Build endpoint based on entity type
let endpoint = ''
let requestBody = {}
switch (entityType) {
case 'tarea':
endpoint = `${API_BASE}/tarea-recursos/tareas/${entityId}/recursos/${resourceId}`
requestBody = {}
break
case 'usuario':
endpoint = `${API_BASE}/usuarios_relacionados/usuarios/${entityId}/recursos/${resourceId}`
requestBody = {}
break
case 'tecnico':
endpoint = `${API_BASE}/tecnico-recursos/tecnicos/${entityId}/recursos/${resourceId}`
requestBody = {}
break
case 'recurso':
endpoint = `${API_BASE}/recursos-asignados/recursos/${resourceId}/asignaciones`
requestBody = { entity_type: entityType, entity_id: entityId }
break
default:
throw new Error('Tipo de entidad no soportado')
}
const response = await fetch(endpoint, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
if (!response.ok) {
throw new Error(`Error al desasignar recurso: ${response.status}`)
}
const data = await response.json()
if (data.success) {
// Don't show toast for now due to missing backend endpoints
await refreshAssignments()
return true
} else {
throw new Error(data.message || 'Error al desasignar recurso')
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error desconocido'
setError(errorMessage)
console.error('Error unassigning resource:', error)
return false
} finally {
setLoading(false)
}
}, [entityType, entityId, setLoading, setError])
// Refresh both assigned and available resources
const refreshAssignments = useCallback(async () => {
await Promise.all([
loadAssignedResources(),
loadAvailableResources()
])
}, [loadAssignedResources, loadAvailableResources])
// Helper function to get the correct endpoint for assignments
const getAssignmentEndpoint = (type: string): string => {
switch (type) {
case 'tarea':
return '/api/tarea-recursos'
case 'usuario':
return '/api/usuarios_relacionados'
case 'tecnico':
return '/api/tecnico-recursos'
case 'recurso':
return '/api/recursos-asignados'
default:
return '/api/tarea-recursos'
}
}
// Auto-load data when hook is initialized
useEffect(() => {
refreshAssignments()
}, [refreshAssignments])
return {
// State
assignedResources: state.assignedResources,
availableResources: state.availableResources,
loading: state.loading,
error: state.error,
// Actions
assignResource,
unassignResource,
loadAssignedResources,
loadAvailableResources,
refreshAssignments
}
}
export default useResourceAssignment