423 lines
12 KiB
JavaScript
423 lines
12 KiB
JavaScript
// Composable para gestionar los resultados de búsqueda y las sesiones de búsqueda.
|
|
|
|
import { computed, ref, watch } from 'vue'
|
|
import { useWsMonitor } from './useWsMonitor'
|
|
import { adaptSearchPayload } from '../services/resultAdapters'
|
|
|
|
const sessions = ref([])
|
|
const activeSessionId = ref('')
|
|
const pendingRequests = ref({})
|
|
const selectedCardIds = ref([])
|
|
|
|
let bridgeInitialized = false
|
|
let sequence = 0
|
|
const processedLogIds = new Set()
|
|
|
|
const uid = (prefix) => {
|
|
sequence += 1
|
|
return `${prefix}_${Date.now()}_${sequence}`
|
|
}
|
|
|
|
const hashString = (value) => {
|
|
const raw = String(value || '')
|
|
let hash = 5381
|
|
for (let i = 0; i < raw.length; i += 1) {
|
|
hash = ((hash << 5) + hash) + raw.charCodeAt(i)
|
|
hash &= 0xffffffff
|
|
}
|
|
return `h${Math.abs(hash)}`
|
|
}
|
|
|
|
const getActiveSession = () => sessions.value.find((item) => item.id === activeSessionId.value) || null
|
|
|
|
const activeSession = computed(() => getActiveSession())
|
|
|
|
const selectedSet = computed(() => new Set(selectedCardIds.value))
|
|
|
|
const findSessionById = (sessionId) => sessions.value.find((item) => item.id === sessionId) || null
|
|
|
|
const findCardById = (cardId) => {
|
|
for (const session of sessions.value) {
|
|
const card = session.cardsById[cardId]
|
|
if (card) {
|
|
return { session, card }
|
|
}
|
|
}
|
|
return { session: null, card: null }
|
|
}
|
|
|
|
const ensureSourceGroup = (session, sourceKey, sourceLabel) => {
|
|
if (session.groupMap[sourceKey]) {
|
|
return session.groupMap[sourceKey]
|
|
}
|
|
|
|
const group = {
|
|
id: uid('group'),
|
|
sourceKey,
|
|
sourceLabel,
|
|
status: 'ok',
|
|
error: '',
|
|
cards: [],
|
|
}
|
|
|
|
session.groupMap[sourceKey] = group
|
|
session.groups.push(group)
|
|
return group
|
|
}
|
|
|
|
const createSession = (rootRequestId, rootQuery, rootContext) => {
|
|
const session = {
|
|
id: uid('session'),
|
|
createdAt: new Date().toISOString(),
|
|
rootRequestId,
|
|
rootQuery,
|
|
rootContext,
|
|
groups: [],
|
|
groupMap: {},
|
|
cardsById: {},
|
|
}
|
|
|
|
sessions.value.unshift(session)
|
|
activeSessionId.value = session.id
|
|
return session
|
|
}
|
|
|
|
const pushChildRequestPlaceholder = (parentCardId, requestId, query, context) => {
|
|
if (!parentCardId) {
|
|
return
|
|
}
|
|
|
|
const { card } = findCardById(parentCardId)
|
|
if (!card) {
|
|
return
|
|
}
|
|
|
|
card.childRequests.unshift({
|
|
requestId,
|
|
query,
|
|
context,
|
|
status: 'loading',
|
|
error: '',
|
|
resultCardIds: [],
|
|
createdAt: new Date().toISOString(),
|
|
})
|
|
}
|
|
// Actualiza el estado de una solicitud de búsqueda secundaria asociada a un card padre, incluyendo el estado, los IDs de las cards resultantes y cualquier error.
|
|
const setChildRequestResult = (parentCardId, requestId, status, resultCardIds = [], error = '') => {
|
|
if (!parentCardId) {
|
|
return
|
|
}
|
|
|
|
const { card } = findCardById(parentCardId)
|
|
if (!card) {
|
|
return
|
|
}
|
|
|
|
const target = card.childRequests.find((item) => item.requestId === requestId)
|
|
if (!target) {
|
|
return
|
|
}
|
|
|
|
target.status = status
|
|
target.error = error
|
|
target.resultCardIds = resultCardIds
|
|
}
|
|
|
|
|
|
// Ejecuta una busqueda enviando la query y el context al backend a través del WebSocket
|
|
const dispatchSearch = ({ query, context, parentCardId = '', sessionId = '' }) => {
|
|
// Genera un ID único para la solicitud de búsqueda
|
|
const requestId = uid('req')
|
|
// Obtiene la función sendCommand del composable useWsMonitor para enviar comandos al backend
|
|
const { sendCommand } = useWsMonitor()
|
|
// Busca la sesión correspondiente al sessionId proporcionado, si no se encuentra, intenta buscarla a través del parentCardId. Si aún no se encuentra, crea una nueva sesión con la query y el context proporcionados.
|
|
let targetSession = sessionId ? findSessionById(sessionId) : null
|
|
if (!targetSession && parentCardId) {
|
|
const found = findCardById(parentCardId)
|
|
targetSession = found.session
|
|
}
|
|
// Si no se encuentra ninguna sesión existente, crea una nueva sesión con la query y el context proporcionados.
|
|
if (!targetSession) {
|
|
targetSession = createSession(requestId, query, context)
|
|
}
|
|
// Registra la solicitud de búsqueda pendiente en el objeto pendingRequests, asociando el requestId con la sesión, el parentCardId, la query y el context.
|
|
pendingRequests.value[requestId] = {
|
|
requestId,
|
|
sessionId: targetSession.id,
|
|
parentCardId,
|
|
query,
|
|
context,
|
|
status: 'pending',
|
|
error: '',
|
|
createdAt: new Date().toISOString(),
|
|
}
|
|
// Si se proporciona un parentCardId, agrega un marcador de posición para la solicitud de búsqueda secundaria en el card correspondiente.
|
|
pushChildRequestPlaceholder(parentCardId, requestId, query, context)
|
|
// Envía el comando 'busqueda' al backend a través del WebSocket, incluyendo la query y el context en el payload.
|
|
|
|
sendCommand('busqueda', {
|
|
busqueda: {
|
|
query,
|
|
context,
|
|
},
|
|
})
|
|
|
|
return { requestId, sessionId: targetSession.id }
|
|
}
|
|
|
|
const normalizeMessagePayload = (payload) => {
|
|
if (!payload) {
|
|
return null
|
|
}
|
|
if (typeof payload === 'string') {
|
|
try {
|
|
return JSON.parse(payload)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
if (typeof payload === 'object') {
|
|
return payload
|
|
}
|
|
return null
|
|
}
|
|
|
|
const extractRequestId = (payload) => {
|
|
return (
|
|
payload?.requestId
|
|
|| payload?.context?.requestId
|
|
|| payload?.meta?.requestId
|
|
|| payload?.source?.requestId
|
|
|| ''
|
|
)
|
|
}
|
|
|
|
const pickLatestRequest = () => {
|
|
const entries = Object.values(pendingRequests.value)
|
|
.filter((item) => item.status !== 'error')
|
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
|
|
return entries[0] || null
|
|
}
|
|
|
|
// Agrega nuevas cards a un grupo de resultados de búsqueda dentro de una sesión, asegurando que no se dupliquen y devolviendo los IDs de las cards agregadas.
|
|
const addCardsToGroup = ({ session, group, cards, parentCardId, requestId }) => {
|
|
const appendedIds = []
|
|
|
|
for (const card of cards) {
|
|
const stableSeed = `${group.sourceKey}:${card.stableKey}`
|
|
const stableId = `card_${hashString(stableSeed)}`
|
|
|
|
if (session.cardsById[stableId]) {
|
|
appendedIds.push(stableId)
|
|
continue
|
|
}
|
|
|
|
const normalized = {
|
|
id: stableId,
|
|
stableKey: card.stableKey,
|
|
sourceKey: group.sourceKey,
|
|
sourceLabel: group.sourceLabel,
|
|
title: card.title,
|
|
fields: Array.isArray(card.fields) ? card.fields : [],
|
|
raw: card.raw,
|
|
actions: Array.isArray(card.actions) ? card.actions : [],
|
|
parentCardId: parentCardId || '',
|
|
createdByRequestId: requestId,
|
|
childRequests: [],
|
|
}
|
|
|
|
session.cardsById[stableId] = normalized
|
|
group.cards.push(stableId)
|
|
appendedIds.push(stableId)
|
|
}
|
|
|
|
return appendedIds
|
|
}
|
|
|
|
const handleIncomingSearchPayload = (payload) => {
|
|
const requestId = extractRequestId(payload)
|
|
const pending = requestId ? pendingRequests.value[requestId] : pickLatestRequest()
|
|
|
|
if (!pending) {
|
|
return
|
|
}
|
|
|
|
const session = findSessionById(pending.sessionId)
|
|
if (!session) {
|
|
return
|
|
}
|
|
|
|
if (payload?.ok === false) {
|
|
const normalized = adaptSearchPayload(payload)
|
|
const group = ensureSourceGroup(session, normalized.sourceKey, normalized.sourceLabel)
|
|
group.status = 'error'
|
|
group.error = String(
|
|
payload?.error?.message
|
|
|| payload?.error?.detail
|
|
|| payload?.message
|
|
|| 'Error en respuesta'
|
|
)
|
|
|
|
// NOT_FOUND por fuente es esperado en búsquedas multi-base: no cancelar toda la solicitud.
|
|
const errorCode = String(payload?.error?.code || '').toUpperCase()
|
|
if (errorCode !== 'NOT_FOUND') {
|
|
pending.error = group.error
|
|
setChildRequestResult(pending.parentCardId, pending.requestId, 'error', [], pending.error)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
const normalized = adaptSearchPayload(payload)
|
|
const group = ensureSourceGroup(session, normalized.sourceKey, normalized.sourceLabel)
|
|
|
|
const cardIds = addCardsToGroup({
|
|
session,
|
|
group,
|
|
cards: normalized.cards,
|
|
parentCardId: pending.parentCardId,
|
|
requestId: pending.requestId,
|
|
})
|
|
|
|
pending.status = 'done'
|
|
setChildRequestResult(pending.parentCardId, pending.requestId, 'done', cardIds)
|
|
}
|
|
|
|
// Inicializa el puente WebSocket para recibir los resultados de búsqueda en tiempo real
|
|
const initWsBridge = () => {
|
|
if (bridgeInitialized) {
|
|
return
|
|
}
|
|
|
|
bridgeInitialized = true
|
|
const { logs } = useWsMonitor()
|
|
// Observa los cambios en los logs del WebSocket para procesar los mensajes entrantes
|
|
watch(
|
|
logs,
|
|
(items) => {
|
|
for (const item of items) {
|
|
if (item.type !== 'incoming') {
|
|
continue
|
|
}
|
|
if (processedLogIds.has(item.id)) {
|
|
continue
|
|
}
|
|
|
|
processedLogIds.add(item.id)
|
|
const payload = normalizeMessagePayload(item.payload)
|
|
if (!payload) {
|
|
continue
|
|
}
|
|
|
|
const isSearchPayload = Object.prototype.hasOwnProperty.call(payload, 'ok')
|
|
&& Object.prototype.hasOwnProperty.call(payload, 'source')
|
|
&& Object.prototype.hasOwnProperty.call(payload, 'data')
|
|
|
|
if (isSearchPayload) {
|
|
handleIncomingSearchPayload(payload)
|
|
}
|
|
}
|
|
},
|
|
{ deep: true }
|
|
)
|
|
}
|
|
|
|
const toggleCardSelection = (cardId) => {
|
|
if (selectedSet.value.has(cardId)) {
|
|
selectedCardIds.value = selectedCardIds.value.filter((id) => id !== cardId)
|
|
} else {
|
|
selectedCardIds.value = [...selectedCardIds.value, cardId]
|
|
}
|
|
}
|
|
|
|
const isCardSelected = (cardId) => selectedSet.value.has(cardId)
|
|
|
|
const runCardAction = ({ sessionId, parentCardId, action }) => {
|
|
if (!action?.query || !action?.context) {
|
|
return
|
|
}
|
|
dispatchSearch({
|
|
query: action.query,
|
|
context: action.context,
|
|
parentCardId,
|
|
sessionId,
|
|
})
|
|
}
|
|
|
|
const selectedCards = computed(() => {
|
|
const session = activeSession.value
|
|
if (!session) {
|
|
return []
|
|
}
|
|
return selectedCardIds.value.map((id) => session.cardsById[id]).filter(Boolean)
|
|
})
|
|
|
|
const exportSelectedAsJson = () => {
|
|
const data = {
|
|
generatedAt: new Date().toISOString(),
|
|
sessionId: activeSessionId.value,
|
|
cards: selectedCards.value,
|
|
}
|
|
|
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
|
const url = URL.createObjectURL(blob)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = `reporte_resultados_${Date.now()}.json`
|
|
link.click()
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
|
|
const escapeHtml = (value) => String(value ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
|
|
const exportSelectedAsPdf = () => {
|
|
if (!selectedCards.value.length) {
|
|
return
|
|
}
|
|
|
|
const rows = selectedCards.value
|
|
.map((card, index) => {
|
|
const fields = card.fields
|
|
.map((field) => `<li><strong>${escapeHtml(field.label)}:</strong> ${escapeHtml(field.value)}</li>`)
|
|
.join('')
|
|
|
|
return `<section class="card"><h2>${index + 1}. ${escapeHtml(card.title)}</h2><p class="meta">Base: ${escapeHtml(card.sourceLabel || card.sourceKey)}</p><ul>${fields}</ul></section>`
|
|
})
|
|
.join('')
|
|
|
|
const html = `<!DOCTYPE html><html lang="es"><head><meta charset="UTF-8"/><title>Reporte</title><style>body{font-family:Segoe UI,Arial,sans-serif;margin:24px;color:#0f172a}.header{border-bottom:2px solid #1e293b;margin-bottom:16px;padding-bottom:10px}.card{border:1px solid #cbd5e1;padding:12px;margin-bottom:10px}.meta{color:#475569;font-size:12px;margin:0 0 8px}ul{margin:0;padding-left:16px}li{margin-bottom:4px}</style></head><body><header class="header"><h1>Reporte de resultados</h1><p>Generado: ${escapeHtml(new Date().toLocaleString())}</p><p>Total de cards: ${selectedCards.value.length}</p></header>${rows}</body></html>`
|
|
|
|
const win = window.open('', '_blank', 'noopener,noreferrer,width=1100,height=800')
|
|
if (!win) {
|
|
return
|
|
}
|
|
|
|
win.document.open()
|
|
win.document.write(html)
|
|
win.document.close()
|
|
win.focus()
|
|
win.print()
|
|
}
|
|
|
|
export const useSearchResultsStore = () => ({
|
|
sessions,
|
|
activeSession,
|
|
activeSessionId,
|
|
pendingRequests,
|
|
selectedCardIds,
|
|
selectedCards,
|
|
initWsBridge,
|
|
dispatchSearch,
|
|
toggleCardSelection,
|
|
isCardSelected,
|
|
runCardAction,
|
|
exportSelectedAsJson,
|
|
exportSelectedAsPdf,
|
|
})
|