/** * Composable para gestión de registros externos y vinculaciones * * Registros externos: Ingresos, Carretas, Salidas, Rechazos * Estos registros vienen de Metabase (solo lectura) y se vinculan a lotes locales. */ // ===================================================== // TIPOS // ===================================================== export interface Ingreso { id: number created_at: string tipo: string calidad: string humedad: number | null estado: string observacion: string | null sacos_total: number peso_bruto: number peso_neto: number peso_seco: number cliente_id: number | null comercio_id: number | null cliente_nombre: string | null comercio_nombre: string | null vinculado: boolean } export interface Carreta { id: number created_at: string titulo: string estado: string libras_mojadas: number humedad: number qq_seco_estimado: number vinculado: boolean } export interface Salida { id: number created_at: string comprador: string observacion: string | null peso_bruto: number sacos_total: number peso_neto: number qq_seco: number vinculado: boolean } export interface Rechazo { id: number created_at: string tipo: string estado: string cantidad: number unidad: string precio_unidad: number | null observacion: string | null comprador_id: number | null comprador_nombre: string | null vinculado: boolean } export interface Vinculacion { id: string tipo_registro: 'ingreso' | 'carreta' | 'salida' | 'rechazo' registro_id: number lote_id: string fecha_vinculacion: string usuario_id: string | null observaciones: string | null datos_cache: Record | null periodo_cosecha: string } export interface EstadisticasVinculacion { ingresos: { total: number; vinculados: number; sinVincular: number; porcentaje: number } carretas: { total: number; vinculados: number; sinVincular: number; porcentaje: number } salidas: { total: number; vinculados: number; sinVincular: number; porcentaje: number } rechazos: { total: number; vinculados: number; sinVincular: number; porcentaje: number } resumen: { total: number; vinculados: number; sinVincular: number; porcentaje: number } periodo: string } export type TipoRegistro = 'ingreso' | 'carreta' | 'salida' | 'rechazo' // ===================================================== // COMPOSABLE // ===================================================== export const useRegistrosExternos = () => { const toast = process.client ? useToast() : null // ===================================================== // FETCH DE REGISTROS EXTERNOS // ===================================================== /** * Obtiene ingresos del período */ const fetchIngresos = async (opts?: { periodo?: string; sinVincular?: boolean }) => { try { const query = new URLSearchParams() if (opts?.periodo) query.append('periodo', opts.periodo) if (opts?.sinVincular) query.append('sinVincular', 'true') const { data, error } = await useFetch<{ success: boolean data: Ingreso[] meta: { total: number; vinculados: number; sinVincular: number } }>(`/api/externos/ingresos?${query.toString()}`) if (error.value) { throw new Error(error.value.message || 'Error obteniendo ingresos') } return { data: data.value?.data || [], meta: data.value?.meta, } } catch (err: any) { console.error('Error fetching ingresos:', err) toast?.add({ title: 'Error', description: err.message || 'Error obteniendo ingresos', color: 'red', }) return { data: [], meta: null } } } /** * Obtiene carretas del período */ const fetchCarretas = async (opts?: { periodo?: string; sinVincular?: boolean }) => { try { const query = new URLSearchParams() if (opts?.periodo) query.append('periodo', opts.periodo) if (opts?.sinVincular) query.append('sinVincular', 'true') const { data, error } = await useFetch<{ success: boolean data: Carreta[] meta: { total: number; vinculados: number; sinVincular: number } }>(`/api/externos/carretas?${query.toString()}`) if (error.value) { throw new Error(error.value.message || 'Error obteniendo carretas') } return { data: data.value?.data || [], meta: data.value?.meta, } } catch (err: any) { console.error('Error fetching carretas:', err) toast?.add({ title: 'Error', description: err.message || 'Error obteniendo carretas', color: 'red', }) return { data: [], meta: null } } } /** * Obtiene salidas del período */ const fetchSalidas = async (opts?: { periodo?: string; sinVincular?: boolean }) => { try { const query = new URLSearchParams() if (opts?.periodo) query.append('periodo', opts.periodo) if (opts?.sinVincular) query.append('sinVincular', 'true') const { data, error } = await useFetch<{ success: boolean data: Salida[] meta: { total: number; vinculados: number; sinVincular: number } }>(`/api/externos/salidas?${query.toString()}`) if (error.value) { throw new Error(error.value.message || 'Error obteniendo salidas') } return { data: data.value?.data || [], meta: data.value?.meta, } } catch (err: any) { console.error('Error fetching salidas:', err) toast?.add({ title: 'Error', description: err.message || 'Error obteniendo salidas', color: 'red', }) return { data: [], meta: null } } } /** * Obtiene rechazos del período */ const fetchRechazos = async (opts?: { periodo?: string; sinVincular?: boolean }) => { try { const query = new URLSearchParams() if (opts?.periodo) query.append('periodo', opts.periodo) if (opts?.sinVincular) query.append('sinVincular', 'true') const { data, error } = await useFetch<{ success: boolean data: Rechazo[] meta: { total: number; vinculados: number; sinVincular: number } }>(`/api/externos/rechazos?${query.toString()}`) if (error.value) { throw new Error(error.value.message || 'Error obteniendo rechazos') } return { data: data.value?.data || [], meta: data.value?.meta, } } catch (err: any) { console.error('Error fetching rechazos:', err) toast?.add({ title: 'Error', description: err.message || 'Error obteniendo rechazos', color: 'red', }) return { data: [], meta: null } } } /** * Obtiene estadísticas de vinculación */ const fetchEstadisticas = async (periodo?: string) => { try { const query = periodo ? `?periodo=${periodo}` : '' const { data, error } = await useFetch<{ success: boolean data: EstadisticasVinculacion }>(`/api/externos/stats${query}`) if (error.value) { throw new Error(error.value.message || 'Error obteniendo estadísticas') } return data.value?.data || null } catch (err: any) { console.error('Error fetching estadisticas:', err) toast?.add({ title: 'Error', description: err.message || 'Error obteniendo estadísticas', color: 'red', }) return null } } // ===================================================== // GESTIÓN DE VINCULACIONES // ===================================================== /** * Vincula un registro externo a un lote */ const vincular = async ( tipo: TipoRegistro, registroId: number, loteId: string, opts?: { observaciones?: string; datosCache?: Record } ) => { try { const { data, error } = await useFetch<{ success: boolean data: Vinculacion message: string }>('/api/vinculaciones', { method: 'POST', body: { tipo_registro: tipo, registro_id: registroId, lote_id: loteId, observaciones: opts?.observaciones, datos_cache: opts?.datosCache, }, }) if (error.value) { throw new Error(error.value.message || 'Error creando vinculación') } toast?.add({ title: 'Vinculado', description: 'Registro vinculado correctamente al lote', color: 'green', }) return data.value?.data || null } catch (err: any) { console.error('Error vinculando:', err) toast?.add({ title: 'Error', description: err.message || 'Error creando vinculación', color: 'red', }) return null } } /** * Vincula múltiples registros a un lote */ const vincularMasivo = async ( vinculaciones: Array<{ tipo: TipoRegistro registroId: number loteId: string observaciones?: string datosCache?: Record }> ) => { try { const { data, error } = await useFetch<{ success: boolean data: Vinculacion[] message: string }>('/api/vinculaciones', { method: 'POST', body: { masivo: true, items: vinculaciones.map((v) => ({ tipo_registro: v.tipo, registro_id: v.registroId, lote_id: v.loteId, observaciones: v.observaciones, datos_cache: v.datosCache, })), }, }) if (error.value) { throw new Error(error.value.message || 'Error creando vinculaciones') } toast?.add({ title: 'Vinculados', description: `${data.value?.data?.length || 0} registros vinculados correctamente`, color: 'green', }) return data.value?.data || [] } catch (err: any) { console.error('Error vinculando masivo:', err) toast?.add({ title: 'Error', description: err.message || 'Error creando vinculaciones', color: 'red', }) return [] } } /** * Elimina una vinculación */ const desvincular = async (vinculacionId: string) => { try { const { error } = await useFetch(`/api/vinculaciones/${vinculacionId}`, { method: 'DELETE', }) if (error.value) { throw new Error(error.value.message || 'Error eliminando vinculación') } toast?.add({ title: 'Desvinculado', description: 'Vinculación eliminada correctamente', color: 'green', }) return true } catch (err: any) { console.error('Error desvinculando:', err) toast?.add({ title: 'Error', description: err.message || 'Error eliminando vinculación', color: 'red', }) return false } } /** * Obtiene vinculaciones de un lote */ const fetchVinculacionesByLote = async (loteId: string) => { try { const { data, error } = await useFetch<{ success: boolean data: Vinculacion[] agrupados: { ingresos: Vinculacion[] carretas: Vinculacion[] salidas: Vinculacion[] rechazos: Vinculacion[] } meta: { lote_id: string total: number por_tipo: { ingresos: number carretas: number salidas: number rechazos: number } } }>(`/api/vinculaciones/por-lote/${loteId}`) if (error.value) { throw new Error(error.value.message || 'Error obteniendo vinculaciones') } return { data: data.value?.data || [], agrupados: data.value?.agrupados, meta: data.value?.meta, } } catch (err: any) { console.error('Error fetching vinculaciones:', err) toast?.add({ title: 'Error', description: err.message || 'Error obteniendo vinculaciones', color: 'red', }) return { data: [], agrupados: null, meta: null } } } // ===================================================== // CONSTANTES // ===================================================== const TIPOS_REGISTRO = [ { value: 'ingreso', label: 'Ingresos', labelSingular: 'Ingreso', icon: 'i-heroicons-inbox-arrow-down', color: 'purple' }, { value: 'carreta', label: 'Carretas', labelSingular: 'Carreta', icon: 'i-heroicons-truck', color: 'blue' }, { value: 'salida', label: 'Salidas', labelSingular: 'Salida', icon: 'i-heroicons-arrow-up-tray', color: 'green' }, { value: 'rechazo', label: 'Rechazos', labelSingular: 'Rechazo', icon: 'i-heroicons-x-circle', color: 'red' }, ] as const const PERIODOS_COSECHA = [ { value: '25-26', label: 'Cosecha 2025-2026 (Sep 2025 - Sep 2026)' }, { value: '24-25', label: 'Cosecha 2024-2025 (Sep 2024 - Sep 2025)' }, ] as const // ===================================================== // HELPERS // ===================================================== /** * Obtiene el label de un tipo de registro */ const getTipoLabel = (tipo: TipoRegistro, plural = true) => { const found = TIPOS_REGISTRO.find((t) => t.value === tipo) return plural ? found?.label || tipo : found?.labelSingular || tipo } /** * Obtiene el icono de un tipo de registro */ const getTipoIcon = (tipo: TipoRegistro) => { const found = TIPOS_REGISTRO.find((t) => t.value === tipo) return found?.icon || 'i-heroicons-document' } /** * Obtiene el color de un tipo de registro */ const getTipoColor = (tipo: TipoRegistro) => { const found = TIPOS_REGISTRO.find((t) => t.value === tipo) return found?.color || 'gray' } /** * Formatea una fecha para mostrar */ const formatFecha = (fecha: string) => { return new Date(fecha).toLocaleDateString('es-HN', { year: 'numeric', month: 'short', day: 'numeric', }) } /** * Formatea un número como peso */ const formatPeso = (peso: number | null, unidad = 'qq') => { if (peso === null || peso === undefined) return '-' return `${peso.toFixed(2)} ${unidad}` } return { // Fetch registros externos fetchIngresos, fetchCarretas, fetchSalidas, fetchRechazos, fetchEstadisticas, // Vinculaciones vincular, vincularMasivo, desvincular, fetchVinculacionesByLote, // Constantes TIPOS_REGISTRO, PERIODOS_COSECHA, // Helpers getTipoLabel, getTipoIcon, getTipoColor, formatFecha, formatPeso, } }