type CommissionEvent = { sessionUID: string actionType: string actionTime: string durationMs: number adUserId?: number | null mInoutId?: number | null cOrderId?: number | null mProductId?: number | null cBPartnerId?: number | null foreignAdOrgId?: number | null qty?: number | null shippingService?: string | null description?: string | null } type TrackInput = Partial> const ENDPOINT = '/api/commission/activity' const FLUSH_MS = 4000 export const useCommissionActivity = () => { const sessionUID = ref(null) const startedAt = ref(0) const queue: CommissionEvent[] = [] // Bound FK context for the active session — copied onto every track() call // so each cust_commissionactivity row carries the same m_inout / c_order / // c_bpartner / foreign_ad_org keys and can be joined back to the shipment. const sessionContext = ref({}) let timer: any = null let visibilityHandler: (() => void) | null = null const adUserIdCookie = useCookie('logship_user_id') const adUserId = Number(adUserIdCookie.value) || null const requestHeaders = useRequestHeaders(['cookie']) const flush = () => { if (!queue.length) return const batch = queue.splice(0, queue.length) $fetch(ENDPOINT, { method: 'POST', headers: requestHeaders, body: { events: batch }, }).catch(() => {}) } const flushBeacon = () => { if (!import.meta.client || !queue.length) return const batch = queue.splice(0, queue.length) try { const blob = new Blob([JSON.stringify({ events: batch })], { type: 'application/json' }) if (!navigator.sendBeacon?.(ENDPOINT, blob)) { $fetch(ENDPOINT, { method: 'POST', headers: requestHeaders, body: { events: batch }, keepalive: true, } as any).catch(() => {}) } } catch {} } const attachVisibility = () => { if (!import.meta.client || visibilityHandler) return visibilityHandler = () => { if (document.visibilityState === 'hidden') flushBeacon() } window.addEventListener('visibilitychange', visibilityHandler) } const detachVisibility = () => { if (!import.meta.client || !visibilityHandler) return window.removeEventListener('visibilitychange', visibilityHandler) visibilityHandler = null } const track = (actionType: string, data: TrackInput = {}) => { if (!sessionUID.value) return queue.push({ sessionUID: sessionUID.value, actionType, actionTime: new Date().toISOString(), durationMs: Date.now() - startedAt.value, adUserId, mInoutId: sessionContext.value.mInoutId ?? null, cOrderId: sessionContext.value.cOrderId ?? null, cBPartnerId: sessionContext.value.cBPartnerId ?? null, foreignAdOrgId: sessionContext.value.foreignAdOrgId ?? null, ...data, }) } const start = (ctx: TrackInput = {}) => { if (sessionUID.value) return sessionUID.value = (import.meta.client && crypto?.randomUUID) ? crypto.randomUUID() : `sess-${Date.now()}-${Math.random().toString(36).slice(2, 10)}` startedAt.value = Date.now() sessionContext.value = { mInoutId: ctx.mInoutId ?? null, cOrderId: ctx.cOrderId ?? null, cBPartnerId: ctx.cBPartnerId ?? null, foreignAdOrgId: ctx.foreignAdOrgId ?? null, } track('session_start', ctx) if (!timer) timer = setInterval(flush, FLUSH_MS) attachVisibility() } const stop = () => { if (!sessionUID.value) return track('session_complete') flushBeacon() sessionUID.value = null sessionContext.value = {} if (timer) { clearInterval(timer); timer = null } detachVisibility() } const abandon = () => { if (!sessionUID.value) return const sid = sessionUID.value queue.length = 0 sessionUID.value = null sessionContext.value = {} if (timer) { clearInterval(timer); timer = null } detachVisibility() // Fire DELETE immediately with keepalive so it survives tab close / // navigation. The server runs a second sweep ~3s later to catch any // in-flight POST from the last flush that lands after this DELETE. $fetch(ENDPOINT, { method: 'DELETE', headers: requestHeaders, body: { sessionUID: sid }, keepalive: true, } as any).catch(() => {}) } if (import.meta.client) { onScopeDispose(() => { if (sessionUID.value) { flushBeacon() } if (timer) { clearInterval(timer); timer = null } detachVisibility() }) } return { start, track, stop, abandon, sessionUID: readonly(sessionUID), } }