Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2d53b4fab |
@ -1,13 +1,15 @@
|
|||||||
const { startAuthentication } = require('./auth');
|
const { startAuthentication } = require('./auth');
|
||||||
const { fetchClosedTickets } = require('./tickets');
|
const { fetchClosedTickets, fetchUnassignedCount } = require('./tickets');
|
||||||
|
|
||||||
function createBoardService({ clientId, clientSecret, technicians, statusSet, windowMinutes, checkMinutes }) {
|
function createBoardService({ clientId, clientSecret, technicians, statusSet, windowMinutes, checkMinutes, unassignedStatusSet, unassignedSiteSet, unassignedGroupSet }) {
|
||||||
const state = {
|
const state = {
|
||||||
auth: 'disconnected',
|
auth: 'disconnected',
|
||||||
verificationUrl: null,
|
verificationUrl: null,
|
||||||
userCode: null,
|
userCode: null,
|
||||||
lastCheck: null,
|
lastCheck: null,
|
||||||
lastError: null
|
lastError: null,
|
||||||
|
unassignedCount: null,
|
||||||
|
unassignedError: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const tickets = new Map();
|
const tickets = new Map();
|
||||||
@ -25,6 +27,14 @@ function createBoardService({ clientId, clientSecret, technicians, statusSet, wi
|
|||||||
statusSet,
|
statusSet,
|
||||||
windowMinutes
|
windowMinutes
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await fetchUnassignedCount({
|
||||||
|
accessToken,
|
||||||
|
state,
|
||||||
|
statusSet: unassignedStatusSet,
|
||||||
|
siteSet: unassignedSiteSet,
|
||||||
|
groupSet: unassignedGroupSet
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function startPolling() {
|
function startPolling() {
|
||||||
|
|||||||
@ -2,3 +2,13 @@ CLIENT_ID=
|
|||||||
CLIENT_SECRET=
|
CLIENT_SECRET=
|
||||||
TECHNICIAN_NAMES=First Last, First Last
|
TECHNICIAN_NAMES=First Last, First Last
|
||||||
STATUS_NAMES=Closed,Resolved
|
STATUS_NAMES=Closed,Resolved
|
||||||
|
# Unassigned queue number: counts tickets with no technician assigned,
|
||||||
|
# matching all of the statuses/sites below and this support group.
|
||||||
|
# Note the exact spelling "Need Information" (not "Needs Information") -
|
||||||
|
# that's the literal status name in this SDP instance.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
UNASSIGNED_STATUS_NAMES=Open,In Progress,Need Information
|
||||||
|
UNASSIGNED_SITES=KVCC,CMCC,EMCC,CMCC-Damariscotta,KVCC-Alfond,MCCS
|
||||||
|
UNASSIGNED_GROUP=Customer Support
|
||||||
@ -91,6 +91,31 @@ h1 {
|
|||||||
}
|
}
|
||||||
.status[data-auth="error"] .dot { background: var(--alert); }
|
.status[data-auth="error"] .dot { background: var(--alert); }
|
||||||
|
|
||||||
|
.unassigned {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unassigned[hidden] { display: none; }
|
||||||
|
|
||||||
|
.unassignedNum {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--signal);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unassignedNum[data-error] { color: var(--alert); }
|
||||||
|
|
||||||
|
.unassignedLabel {
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%, 100% { opacity: 1; }
|
||||||
50% { opacity: 0.25; }
|
50% { opacity: 0.25; }
|
||||||
@ -302,6 +327,10 @@ body > footer {
|
|||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1>Closed ticket board</h1>
|
<h1>Closed ticket board</h1>
|
||||||
|
<div class="unassigned" id="unassigned" hidden>
|
||||||
|
<span class="unassignedNum" id="unassignedNum">–</span>
|
||||||
|
<span class="unassignedLabel">Unassigned</span>
|
||||||
|
</div>
|
||||||
<div class="status" id="status" data-auth="disconnected">
|
<div class="status" id="status" data-auth="disconnected">
|
||||||
<span class="dot"></span><span id="statusText">Not connected</span>
|
<span class="dot"></span><span id="statusText">Not connected</span>
|
||||||
</div>
|
</div>
|
||||||
@ -375,6 +404,23 @@ function render() {
|
|||||||
$('status').dataset.auth = data.auth;
|
$('status').dataset.auth = data.auth;
|
||||||
$('statusText').textContent = STATUS_TEXT[data.auth] || data.auth;
|
$('statusText').textContent = STATUS_TEXT[data.auth] || data.auth;
|
||||||
|
|
||||||
|
const unassignedEl = $('unassigned');
|
||||||
|
const unassignedNumEl = $('unassignedNum');
|
||||||
|
if (data.auth === 'connected' && (data.unassignedCount !== null || data.unassignedError)) {
|
||||||
|
unassignedEl.hidden = false;
|
||||||
|
if (data.unassignedError) {
|
||||||
|
unassignedNumEl.textContent = '—';
|
||||||
|
unassignedNumEl.dataset.error = 'true';
|
||||||
|
unassignedEl.title = 'Unassigned check failed: ' + data.unassignedError;
|
||||||
|
} else {
|
||||||
|
unassignedNumEl.textContent = data.unassignedCount;
|
||||||
|
delete unassignedNumEl.dataset.error;
|
||||||
|
unassignedEl.title = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unassignedEl.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
$('meta').innerHTML = [
|
$('meta').innerHTML = [
|
||||||
'Checks every ' + data.checkMinutes + ' min',
|
'Checks every ' + data.checkMinutes + ' min',
|
||||||
'Window ' + data.windowMinutes + ' min',
|
'Window ' + data.windowMinutes + ' min',
|
||||||
|
|||||||
@ -6,7 +6,7 @@ const express = require('express');
|
|||||||
const { createBoardService } = require('./boardService');
|
const { createBoardService } = require('./boardService');
|
||||||
const { createRoutes } = require('./routes');
|
const { createRoutes } = require('./routes');
|
||||||
|
|
||||||
const { CLIENT_ID, CLIENT_SECRET, TECHNICIAN_NAMES, STATUS_NAMES } = process.env;
|
const { CLIENT_ID, CLIENT_SECRET, TECHNICIAN_NAMES, STATUS_NAMES, UNASSIGNED_STATUS_NAMES, UNASSIGNED_SITES, UNASSIGNED_GROUP } = process.env;
|
||||||
const PORT = process.env.PORT || 3000; // default 3000
|
const PORT = process.env.PORT || 3000; // default 3000
|
||||||
|
|
||||||
if (!CLIENT_ID || !CLIENT_SECRET) {
|
if (!CLIENT_ID || !CLIENT_SECRET) {
|
||||||
@ -18,6 +18,15 @@ const technicians = (TECHNICIAN_NAMES ? TECHNICIAN_NAMES.split(',') : []).map(t
|
|||||||
const statuses = (STATUS_NAMES ? STATUS_NAMES.split(',') : []).map(s => s.trim()).filter(Boolean);
|
const statuses = (STATUS_NAMES ? STATUS_NAMES.split(',') : []).map(s => s.trim()).filter(Boolean);
|
||||||
const statusSet = new Set(statuses.map(s => s.toLowerCase()));
|
const statusSet = new Set(statuses.map(s => s.toLowerCase()));
|
||||||
|
|
||||||
|
const unassignedStatuses = (UNASSIGNED_STATUS_NAMES ? UNASSIGNED_STATUS_NAMES.split(',') : ['Open', 'In Progress', 'Need Information']).map(s => s.trim()).filter(Boolean);
|
||||||
|
const unassignedStatusSet = new Set(unassignedStatuses.map(s => s.toLowerCase()));
|
||||||
|
|
||||||
|
const unassignedSites = (UNASSIGNED_SITES ? UNASSIGNED_SITES.split(',') : ['KVCC', 'CMCC', 'EMCC']).map(s => s.trim()).filter(Boolean);
|
||||||
|
const unassignedSiteSet = new Set(unassignedSites.map(s => s.toLowerCase()));
|
||||||
|
|
||||||
|
const unassignedGroups = (UNASSIGNED_GROUP ? UNASSIGNED_GROUP.split(',') : ['Customer Support']).map(g => g.trim()).filter(Boolean);
|
||||||
|
const unassignedGroupSet = new Set(unassignedGroups.map(g => g.toLowerCase()));
|
||||||
|
|
||||||
const WINDOW_MINUTES = 10; // how far back the server looks in minutes
|
const WINDOW_MINUTES = 10; // how far back the server looks in minutes
|
||||||
const CHECK_MINUTES = 1; // how often the server checks for new tickets in minutes
|
const CHECK_MINUTES = 1; // how often the server checks for new tickets in minutes
|
||||||
|
|
||||||
@ -30,7 +39,10 @@ const boardService = createBoardService({
|
|||||||
technicians,
|
technicians,
|
||||||
statusSet,
|
statusSet,
|
||||||
windowMinutes: WINDOW_MINUTES,
|
windowMinutes: WINDOW_MINUTES,
|
||||||
checkMinutes: CHECK_MINUTES
|
checkMinutes: CHECK_MINUTES,
|
||||||
|
unassignedStatusSet,
|
||||||
|
unassignedSiteSet,
|
||||||
|
unassignedGroupSet
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|||||||
@ -99,6 +99,78 @@ async function fetchClosedTickets({ accessToken, state, tickets, technicians, st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = {
|
module.exports = {
|
||||||
fetchClosedTickets
|
fetchClosedTickets,
|
||||||
|
fetchUnassignedCount
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue
Block a user