-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclient.js
460 lines (379 loc) · 15.7 KB
/
client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
// Specify the colors of the notifications
const errorRed = "#f44336";
const errorRedBorder = "#c13329";
const confirmationGreen = "#8BC34A";
const confirmationGreenBorder = "#6b9837";
// Press enter on the ID input to update it, no need for clicking the update button
document.getElementById("patientID").addEventListener("keydown", function(event) {
if (event.code === "Enter") {
showData(true);
}
});
let lastFetchedID; // Check what the last ID fetched & shown to the user
let lastUpdated; // Get the time the patient's data was last updated
// Fetch the data from the database
async function getData(id = 0) {
try {
const response = await fetch(`/data/${id}?lastUpdated=${lastUpdated}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
});
if (response.status == 304) {
console.log("304: Data not modified, no need to refresh the data.")
return;
} else if (!response.ok) { // Gives an error and returns null if the ID doesn't exist and the server responds with a status code of 404
if (response.status == 404) {
console.error(`Failed to fetch data: ${response.status} ${response.statusText}.\nID "${id}" is not in the database. `);
showNotification(`Error: ID "${id}" is not in the database.`, errorRed, errorRedBorder);
} else {
console.error(`Failed to fetch data: ${response.status} ${response.statusText}`);
}
return null;
} else {
const data = await response.json();
console.log(data);
return data;
}
} catch (error) {
console.error(error);
}
}
async function showData(forceUpdate = false) {
const idInput = document.getElementById("patientID").value;
// Check if the patient ID input is empty
if (idInput == "") {
console.error("Error: The ID input is empty. Please enter an ID");
showNotification("The ID input is empty. Please enter an ID", errorRed, errorRedBorder);
return; // To prevent the rest of the function from running
}
// Fetch the data
const data = await getData(+idInput);
// Don't re-display the same data if is was not updated
if (lastUpdated &&
forceUpdate == false &&
idInput == lastFetchedID &&
new Date(lastUpdated).getTime() === new Date(data?.lastUpdated).getTime()) {
return;
}
if ( data != null ) { // If the server didn't find the data, the getData function would return null
// so the rest of the function code only runs if the server found and returned the data
// Re-displays the data containers if they were hidden after trying to fetch an unavailable ID
document.getElementById("patientFieldSet").style.display = "block";
document.getElementById("graphsContainer").style.display = "block";
document.getElementById("patientDataNote").style.display = "none";
document.getElementById("graphNote").style.display = "none";
// const patientId = document.getElementById("patientId");
const patientName = document.getElementById("patientName");
const dateOfBirth = document.getElementById("dateOfBirth");
const nationalID = document.getElementById("nationalID");
const emailAddress = document.getElementById("emailAddress");
const phoneNumber = document.getElementById("phoneNumber");
// Get the age and how many months until the next birthday
const bd = untilNextBD(data.birthDate);
// patientId.innerText = `ID: ${data.id}`;
patientName.innerText = `Patient Name: ${data.fullName}`;
dateOfBirth.innerText = `Date Of Birth: ${formatDate(data.birthDate)}`;
currentAge.innerText = `Current Age: ${bd[0]} years old ( ${bd[1]} month(s) to ${bd[0] + 1} )`;
nationalID.innerText = "National ID: " + data.nationalID;
emailAddress.innerText = "Email Address: " + data.emailAddress;
phoneNumber.innerText = "Phone Number: " + data.phoneNumber;
// Graphs
// Check if the data for each graph is available.
// If not, hide the graph and replace it with an error text.
// Heart rate graph
if (data.heartData != undefined) {
// Display the graph container and set the background to white to make the graph visible
document.getElementById("heartGraphContainer").style.display = "block";
document.getElementById("heartGraphNote").style.display = "none";
document.getElementById("heartGraph").style.backgroundColor = "#fff";
// Get the data for the graphs
const heartGraphData = {
labels: data.heartData.map(entry => entry.id),
series: [ data.heartData.map(entry => entry.bpm) ]
};
// Render the graphs
new Chartist.Line('#heartGraph', heartGraphData, graphOptions(100, 50, '98%', 3, 'Heart Rate (BPM)'));
} else {
// If there's no heart rate data, show a note saying it's not found
document.getElementById("heartGraphContainer").style.display = "none";
document.getElementById("heartGraphNote").style.display = "block";
}
// Spo2 graph, same thing in this as the one above
if (data.oxygenData != undefined) {
document.getElementById("oxygenGraphContainer").style.display = "block";
document.getElementById("oxygenGraphNote").style.display = "none";
document.getElementById("oxygenGraph").style.backgroundColor = "#fff";
const oxygenGraphData = {
labels: data.oxygenData.map(entry => entry.id),
series: [ data.oxygenData.map(entry => entry.percentage) ]
};
new Chartist.Line('#oxygenGraph', oxygenGraphData, graphOptions(105, 80, '98%', 10, 'Percentage (%)')).on('draw', function (data) {
// The following sets the colors of the graph to blue because no other method is working
if (data.type === 'line') {
data.element._node.style.stroke = 'blue';
} else if (data.type === 'point') {
data.element._node.style.fill = 'blue';
data.element._node.style.stroke = 'blue';
} else if (data.type === 'area') {
data.element._node.style.fill = 'blue';
}
});
} else {
document.getElementById("oxygenGraphContainer").style.display = "none";
document.getElementById("oxygenGraphNote").style.display = "block";
}
} else if (data == undefined) {
return; // Do nothing if the data wasn't updated in the database
} else {
// Hide all graphs and data and display a note saying that the data is not available
document.getElementById("graphsContainer").style.display = "none";
document.getElementById("patientFieldSet").style.display = "none";
document.getElementById("patientDataNote").style.display = "block";
document.getElementById("graphNote").style.display = "block";
}
lastUpdated = data.lastUpdated;
lastFetchedID = idInput;
}
// Format the displayed birth date correctly
function formatDate(date) {
const dateObj = new Date(date);
const year = dateObj.getFullYear();
const month = (dateObj.getMonth() + 1).toString().padStart(2, '0'); // Add leading zero if needed
const day = dateObj.getDate().toString().padStart(2, '0');
return `${year}/${month}/${day}`;
}
// Find the age and remaining month until next birthday
function untilNextBD(birthDate) {
const today = new Date();
const birthDateObj = new Date(birthDate);
let age = today.getFullYear() - birthDateObj.getFullYear();
// Check if birthday has already passed this year
if (today.getMonth() < birthDateObj.getMonth() ||
(today.getMonth() === birthDateObj.getMonth() &&
today.getDate() < birthDateObj.getDate())) {
age--; // Decrement age if birthday hasn't passed yet
}
// Check if birthday has already passed this year
const hasPassedThisYear = today.getMonth() > birthDateObj.getMonth() ||
(today.getMonth() === birthDateObj.getMonth() &&
today.getDate() >= birthDateObj.getDate());
// Calculate months until birthday
const monthsUntilBirthday = (hasPassedThisYear ? 12 : 0) +
(birthDateObj.getMonth() - today.getMonth() + (hasPassedThisYear ? 0 : 1));
return [age, monthsUntilBirthday];
}
// Sets up the graphs' settings
const graphOptions = (high, low, width, leftPadding, yAxisTitle) => {
return {
high: high,
low: low,
width: width,
height: 450,
showArea: true,
// lineSmooth: false, // Makes the lines connecting each point straight lines (looks sharper but less friendly so it'll stay true for now until I work more on the UI)
axisX: {
// Only shows even numbered data to look less cluttered
labelInterpolationFnc: (value, index) => (index % 2 === 0 ? value : undefined)
},
chartPadding: {
top: 20,
bottom: 35,
right: 0,
left: leftPadding
},
plugins: [
Chartist.plugins.ctAxisTitle({
axisX: {
axisTitle: 'Time (mins)',
axisClass: 'ct-axis-title',
offset: {
x: 0,
y: 45
},
textAnchor: 'middle'
},
axisY: {
axisTitle: yAxisTitle,
axisClass: 'ct-axis-title',
offset: {
x: 0,
y: 0
},
textAnchor: 'middle',
flipTitle: false
}
}),
]
};
}
// Notification system
let isNotificationVisible = false;
let notificationTimer;
function showNotification(message, bgColor, borderColor) {
if (isNotificationVisible) {
return; // Do nothing if notification is already visible
}
const notification = document.getElementById("notification");
notification.style.backgroundColor = bgColor;
notification.style.borderColor = borderColor;
// Reset the notification's state
clearTimeout(notificationTimer);
notification.style.animation = "";
notification.style.display = "none";
notification.textContent = "";
// Set the message and display the notification
notification.textContent = message;
notification.style.display = "block";
isNotificationVisible = true;
// Start fade-out animation after the specified duration
notificationTimer = setTimeout(function() {
notification.style.animation = "fadeOut 1s forwards";
// Clear the notification state after the fade-out animation completes
setTimeout(function() {
notification.style.animation = "";
notification.style.display = "none";
notification.textContent = "";
isNotificationVisible = false; // Reset flag
}, 500); // Wait for fade-out animation to complete
}, 3000);
}
// Add a new patient to the database
async function submitData(event) {
// Prevents the default form submission behaviour so I can implement my own
event.preventDefault();
// Collect form data
const patientForm = document.getElementById("patientForm");
const formData = new FormData(patientForm);
const patientData = Object.fromEntries(formData.entries());
const heartCheckbox = document.getElementById("includeHeartData").checked;
const oxygenCheckbox = document.getElementById("includeOxygenData").checked;
// Check if the form is empty
let emptyValues = [];
for (const pair of formData.entries()) {
if (pair[1] == '') {
emptyValues.push(pair[0]);
}
}
// If the form is not empty, post the data to the server
if (emptyValues.length === 0) {
// To know when the patient was added to the database
const dateAdded = new Date().toISOString();
// Merge the data from the form with the randomly generated bpm and spo2 data
let mergedData = {};
if (heartCheckbox && oxygenCheckbox) {
const heartData = generateHeartData();
const oxygenData = generateOxygenData();
mergedData = {
...patientData,
heartData,
oxygenData,
dateAdded
};
} else if (heartCheckbox) {
const heartData = generateHeartData();
mergedData = {
...patientData,
heartData,
dateAdded
};
} else if (oxygenCheckbox) {
const oxygenData = generateOxygenData();
mergedData = {
...patientData,
oxygenData,
dateAdded
};
} else {
mergedData = {
...patientData,
dateAdded
};
}
// Send the data to the server to upload it into the database
console.log("Data sent to the server: ", mergedData);
fetch('/addPatient', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(mergedData)
})
.then(response => {
if (!response.ok) {
console.error(`Failed to post: ${response.status} ${response.statusText}`);
showNotification("Failed to upload patient data.", errorRed, errorRedBorder);
throw new Error('Network response was not ok');
}
return response.json();
})
.then(res => {
console.log('Patient added successfully! \nServer response: ', res);
showNotification(`Patient added successfully! ID: ${res.data.id}`, confirmationGreen, confirmationGreenBorder);
})
.catch(error => {
console.error('There was a problem with submitting the data:', error);
showNotification("There was an error during uploading the patient's data.", errorRed, errorRedBorder);
});
} else if (emptyValues.length === 5) {
console.error("Error: The form is empty. Please fill the form before submitting");
showNotification("The form is empty.", errorRed, errorRedBorder);
} else {
console.error("Error: The form is not complete. Complete the form to submit.");
showNotification("The form is not complete. Complete the form to submit.", errorRed, errorRedBorder);
}
}
// Generate random Heart rate & SpO2 data
// Generate IDs (doesn't need to be generated multiple times, once is enough)
const ids = [];
for (let i = 0; i < 60; i++) {
ids.push(i);
}
// Generate the date & time for each heart rate & SpO2 data element
function generateDatetimeArray() {
const datetimeArray = [];
const dateObject = new Date();
// Reset the second and millisecond count so they don't look weird.
dateObject.setSeconds(0);
dateObject.setMilliseconds(0);
for (let minute = 0; minute < 60; minute++) {
dateObject.setMinutes(minute);
datetimeArray.push(dateObject.toISOString());
}
return datetimeArray;
}
// Generate random integers between two numbers
// (to be used in generating the bpm and spo2 data)
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Generate the BPM data
function generateHeartData() {
const heartData = [];
// Get the date & time
const dt = generateDatetimeArray();
// Generate the random heart rate data in an array
const generatedBPM = [...Array(60)].map(() => getRandomInt(70, 95));
for (let i = 0; i < 60; i++) {
const date = new Date(dt[i]).toISOString();
heartData.push( { id: ids[i], bpm: generatedBPM[i], dt: date } );
}
return heartData;
}
// Generate the SpO2 data
function generateOxygenData() {
const oxygenData = [];
const dt = generateDatetimeArray();
const generatedPercentage = [...Array(60)].map(() => getRandomInt(90, 100));
for (let i = 0; i < 60; i++) {
const date = new Date(dt[i]).toISOString();
oxygenData.push( { id: ids[i], percentage: generatedPercentage[i], dt: date } );
}
return oxygenData;
}
// Display/refresh the data to the user
showData(true);
// Refresh the data every 30 seconds (30000 milliseconds)
// setInterval(async () => { await showData() }, 30000); // Does the same thing but using callbacks, not much different.
setInterval(showData, 30000);