const axios = require('axios'); function toMillis(completedTime) { if (!completedTime) return null; const raw = typeof completedTime === 'object' ? completedTime.value : completedTime; const ms = Number(raw); return Number.isFinite(ms) ? ms : null; } function getRelevantTicketTime(ticket) { return toMillis(ticket.completed_time) || toMillis(ticket.resolved_time) || toMillis(ticket.last_updated_time) || null; } async function fetchClosedTickets({ accessToken, state, tickets, technicians, statusSet, windowMinutes }) { if (!accessToken) return; try { const windowStart = Date.now() - windowMinutes * 60 * 1000; const listInfo = { list_info: { row_count: 200, sort_field: 'last_updated_time', sort_order: 'desc', fields_required: [ 'display_id', 'subject', 'status', 'completed_time', 'resolved_time', 'last_updated_time', 'technician' ], search_criteria: [ { field: 'last_updated_time', condition: 'greater_than', value: windowStart.toString() } ] } }; const response = await axios.get('https://ithelp.mainecc.edu/api/v3/requests', { params: { input_data: JSON.stringify(listInfo) }, headers: { Authorization: `Zoho-oauthtoken ${accessToken}`, Accept: 'application/vnd.manageengine.sdp.v3+json' } }); const found = response.data?.requests || []; for (const ticket of found) { const statusName = ticket.status?.name || ''; const statusInternalName = ticket.status?.internal_name || ''; const statusMatches = !statusSet.size || statusSet.has(statusName.toLowerCase()) || statusSet.has(statusInternalName.toLowerCase()); if (!statusMatches) continue; const closedAt = getRelevantTicketTime(ticket); if (!closedAt || closedAt <= windowStart) continue; const technician = ticket.technician ? ticket.technician.name : null; if (!technician) continue; if (technicians.length && !technicians.includes(technician)) continue; tickets.set(String(ticket.display_id), { id: ticket.display_id, subject: ticket.subject, technician, status: statusName || null, closedAt, closedDisplay: ticket.completed_time?.display_value || ticket.resolved_time?.display_value || ticket.last_updated_time?.display_value || null }); } const cutoff = Date.now() - 12 * 60 * 60 * 1000; for (const [key, ticket] of tickets) { if (ticket.closedAt < cutoff) tickets.delete(key); } state.lastCheck = Date.now(); state.lastError = null; } catch (err) { const detail = err.response?.data; state.lastError = detail?.response_status?.messages?.[0]?.message || (typeof detail === 'string' ? detail : null) || err.message; state.lastCheck = Date.now(); console.error('Error: ', detail || err.message); } } async function fetchUnassignedCount({ accessToken, state, statusSet, siteSet, groupSet }) { if (!accessToken) return; try { let count = 0; let startIndex = 1; const rowCount = 100; const maxPages = 50; // safety cap: 5000 unassigned tickets // The SDP instance ignores search_criteria arrays with more than one // condition, so only "technician is unassigned" is filtered server-side; // status/site/group are matched client-side per ticket below. for (let page = 0; page < maxPages; page++) { const listInfo = { list_info: { row_count: rowCount, start_index: startIndex, search_criteria: { field: 'technician', condition: 'is', value: null }, fields_required: ['status', 'technician', 'group', 'requester'] } }; const response = await axios.get('https://ithelp.mainecc.edu/api/v3/requests', { params: { input_data: JSON.stringify(listInfo) }, headers: { Authorization: `Zoho-oauthtoken ${accessToken}`, Accept: 'application/vnd.manageengine.sdp.v3+json' } }); const found = response.data?.requests || []; for (const ticket of found) { if (ticket.technician) continue; const statusName = (ticket.status?.name || '').toLowerCase(); const statusInternalName = (ticket.status?.internal_name || '').toLowerCase(); const statusMatches = !statusSet.size || statusSet.has(statusName) || statusSet.has(statusInternalName); if (!statusMatches) continue; const groupName = (ticket.group?.name || '').toLowerCase(); if (groupSet.size && !groupSet.has(groupName)) continue; const siteName = (ticket.requester?.site?.name || '').toLowerCase(); if (siteSet.size && !siteSet.has(siteName)) continue; count++; } if (!response.data?.list_info?.has_more_rows || found.length === 0) break; startIndex += rowCount; } state.unassignedCount = count; state.unassignedError = null; } catch (err) { const detail = err.response?.data; state.unassignedError = detail?.response_status?.messages?.[0]?.message || (typeof detail === 'string' ? detail : null) || err.message; state.unassignedCount = null; console.error('Error fetching unassigned count: ', detail || err.message); } } module.exports = { fetchClosedTickets, fetchUnassignedCount };