77 Commits

Author SHA1 Message Date
Pritimay Sarkar
048bfcd0cb add enter support 2024-02-29 22:21:38 +05:30
Pritimay Sarkar
565cb2259a add BM2 column in samples 2024-02-29 22:20:56 +05:30
Pritimay Sarkar
644e06f78f add iocl setup 2024-02-19 12:17:42 +05:30
Pritimay Sarkar
41aebcec7b EDA plots 2024-02-19 12:17:28 +05:30
Pritimay Sarkar
8e659d8fed nats test 2024-02-19 12:17:06 +05:30
Pritimay Sarkar
0ed5e0725b known issue in llm chat 2024-02-19 12:16:49 +05:30
Pritimay Sarkar
aa2c04f853 force install in ci 2024-02-11 14:17:28 +05:30
Pritimay Sarkar
47579b0796 get chats count 2024-02-11 14:14:26 +05:30
Pritimay Sarkar
ff8c62ef46 add plots 2024-02-11 14:14:10 +05:30
Pritimay Sarkar
f40a78b843 nats script 2024-02-10 10:07:52 +05:30
Pritimay Sarkar
79980e34c8 add kit check 2024-02-10 10:05:37 +05:30
Pritimay Sarkar
d9fe2fdf25 add changes to llm chat 2024-02-10 09:52:34 +05:30
Pritimay Sarkar
0c5b13fc67 decrease side bar width 2024-02-10 07:46:26 +05:30
Pritimay Sarkar
83dc9f068f fix device run time 2024-02-08 20:42:14 +05:30
Pritimay Sarkar
7e3226c6d2 add device data page 2024-02-08 16:33:36 +05:30
Pritimay Sarkar
870c3a4dcb show cr and clean samples page 2024-02-07 10:20:49 +05:30
Pritimay Sarkar
f7071a2ff5 add remark gfm 2024-01-30 21:00:03 +05:30
Pritimay Sarkar
ed8dd0d4d6 disable reference file input 2024-01-27 10:22:17 +05:30
Pritimay Sarkar
dd92c78104 add new environments 2024-01-27 01:40:06 +05:30
Pritimay Sarkar
6872d260b6 fix test time display 2024-01-27 01:39:16 +05:30
Pritimay Sarkar
3e4f08b90e add experimental chat 2024-01-26 23:02:43 +05:30
Pritimay Sarkar
b69791c246 add test ci script 2024-01-21 11:38:34 +05:30
Pritimay Sarkar
d327fecf8c Merge branch 'dev' into preprod 2023-12-25 13:51:16 +05:30
Pritimay Sarkar
43375556bf add environment variables and point api based on them 2023-12-25 13:46:54 +05:30
Pritimay Sarkar
92feb4081c add preprod stie 2023-12-18 09:16:57 +05:30
Pritimay Sarkar
bb968b4781 ad dtarget 2023-12-14 16:45:53 +05:30
Pritimay Sarkar
a751b55647 add new class column 2023-12-14 15:31:43 +05:30
Pritimay Sarkar
099b3f08fa add dev web app 2023-12-12 22:12:26 +05:30
Pritimay Sarkar
1f55978521 add reference file input and loader 2023-12-11 22:12:56 +05:30
Pritimay Sarkar
84740e846f add loading icon and fix cors 2023-12-10 23:39:27 +05:30
Pritimay Sarkar
09d18106b8 add new helps 2023-12-08 16:36:46 +05:30
Pritimay Sarkar
5dbff78d79 add new multi site support 2023-12-08 15:36:10 +05:30
Pritimay Sarkar
75ad2aba82 add remaining files 2023-12-08 15:35:46 +05:30
Pritimay Sarkar
3a355ffe57 add auto analysis functionality 2023-12-08 15:35:17 +05:30
Pritimay Sarkar
4da937673c add new samples columns 2023-12-08 15:34:46 +05:30
Pritimay Sarkar
48c9bacb4f add supported formats for samples 2023-12-08 15:34:30 +05:30
Pritimay Sarkar
4dd9a31a75 add accordion in help for automatic 2023-12-08 15:33:59 +05:30
Pritimay Sarkar
5d5325d51d add curve fit image api 2023-12-03 16:28:36 +05:30
Pritimay Sarkar
f857e57173 add device quality components 2023-12-03 11:51:12 +05:30
Pritimay Sarkar
c20197c40a add log in ignore 2023-11-17 10:55:22 +05:30
Pritimay Sarkar
ca5106d613 resize card for test counts 2023-11-16 16:33:38 +05:30
Pritimay Sarkar
9c782b7cd1 add css 2023-11-16 16:21:32 +05:30
Pritimay Sarkar
707e338d3a new columns 2023-11-16 14:28:37 +05:30
Pritimay Sarkar
367c36e3f8 revert to old method of cls 2023-11-11 15:35:03 +05:30
Pritimay Sarkar
fc31aea95b new prd classification 2023-11-11 10:53:38 +05:30
Pritimay Sarkar
eaf87791bd new columns 2023-11-10 16:17:25 +05:30
Pritimay Sarkar
89090ff86b install steps 2023-11-10 16:16:53 +05:30
Pritimay Sarkar
515ad41329 change topbar text 2023-11-10 16:16:33 +05:30
Pritimay Sarkar
920858a523 add config 2023-11-09 08:49:00 +05:30
Pritimay Sarkar
b43aba97f2 add qa firebase config 2023-11-09 08:02:59 +05:30
Pritimay Sarkar
16aa545862 add qa firebase config 2023-11-09 08:01:53 +05:30
Pritimay Sarkar
a836335ed3 add samples page 2023-11-09 07:49:36 +05:30
Pritimay Sarkar
65723718c6 add app installation notes 2023-11-09 07:48:36 +05:30
Pritimay Sarkar
fb3d715831 add env name on page title 2023-11-09 07:48:20 +05:30
Pritimay Sarkar
683c6049af add clear button 2023-11-09 07:47:47 +05:30
Pritimay Sarkar
34a770ceed add samples page 2023-11-09 07:46:45 +05:30
Pritimay Sarkar
67a7414d5c hide clear users button in preprod 2023-11-07 10:53:39 +05:30
Pritimay Sarkar
260e6ec734 change apis to qa links 2023-11-07 10:52:33 +05:30
Pritimay Sarkar
eb975ce11a update app links 2023-11-07 08:26:24 +05:30
Pritimay Sarkar
a3d2bea0cc add incubation time 2023-11-07 05:55:03 +05:30
Pritimay Sarkar
420171d24a add link in qc code 2023-09-30 11:56:08 +05:30
Pritimay Sarkar
dd4ee6d964 add qr code instructions 2023-09-27 02:07:24 +05:30
Pritimay Sarkar
62810fe81b add app installtion page 2023-09-25 01:47:27 +05:30
Pritimay Sarkar
aa08336f5f button css 2023-09-25 01:26:36 +05:30
Pritimay Sarkar
66838b6360 add clearusers api 2023-09-25 01:16:33 +05:30
Pritimay Sarkar
3774972839 add clear button 2023-09-25 00:20:42 +05:30
Pritimay Sarkar
924e48c1f0 add alerts commponents 2023-09-19 15:39:43 +05:30
Pritimay Sarkar
36879b6326 registration name 2023-09-10 09:50:35 +05:30
Pritimay Sarkar
327aa07735 add preprod setup 2023-09-07 11:22:05 +05:30
Pritimay Sarkar
f269b12da1 fix error for hemocube device in select 2023-09-07 11:21:52 +05:30
Pritimay Sarkar
cb0063e286 html tag 2023-09-05 18:44:32 +05:30
Pritimay Sarkar
3e01c94e46 add pdf generation 2023-08-29 16:29:57 +05:30
Pritimay Sarkar
cde2485a60 show user id after user registration 2023-08-28 18:23:39 +05:30
Pritimay Sarkar
9893161684 add user registration 2023-08-28 17:54:20 +05:30
Pritimay Sarkar
eecc6501df add data download page 2023-08-23 21:15:32 +05:30
Pritimay Sarkar
47d348d791 merge 2023-08-23 20:57:33 +05:30
Pritimay Sarkar
16b3f521d4 add sidebar with redux
- add store
	- add routes
	- add page wrapper, mainlayout
	- add nav folder
2023-08-01 15:38:22 +05:30
89 changed files with 13637 additions and 5623 deletions

View File

@@ -1,5 +1,30 @@
{
"projects": {
"default": "hpos-af3cc"
}
}
},
"targets": {
"hpos-af3cc": {
"hosting": {
"dev1": [
"hpos-dev1"
],
"qa": [
"hpos-qa"
],
"dev": [
"hpos-dev"
],
"preprod": [
"hpos-preprod"
],
"uat": [
"hpos-uat"
],
"iocl": [
"iocl-iisc"
]
}
}
},
"etags": {}
}

14
.gitignore vendored
View File

@@ -13,6 +13,7 @@
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
@@ -27,3 +28,16 @@ yarn-error.log*
src/components/testData.json
public/world-110m.json
public/india-states.json
*.apk
scripts/reports
scripts/data
firebase-debug.log
.env.preprod
.env.prod
.env.qa
.env.dev
.env.uat
.env.iocl
certificate_path/

18
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,18 @@
stages:
- test
unit-test:
image: node:latest
stage: test
before_script:
- npm install --force
script:
- npm run test-ci
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
artifacts:
paths:
- coverage/
when: always
reports:
junit:
- junit.xml

View File

@@ -14,4 +14,26 @@
http://localhost:3000
### add target (multi site), if not already defined for that site
firebase target:apply hosting dev1 hpos-dev1
### add or change to firebase.json
"target": "dev1",
### run build
npm run build
### deploy
firebase deploy --only hosting:dev
pyinstaller --noconfirm --noconsole -i "smi.ico" --windowed --clean hemocube.py
### known issues
gemini starts giving error after 70 odd messages in a chat discussion

1
__mocks__/styleMock.js Normal file
View File

@@ -0,0 +1 @@
module.exports = {};

1
__mocks__/svgMock.js Normal file
View File

@@ -0,0 +1 @@
module.exports = 'test-file-stub';

4
babel.config.json Normal file
View File

@@ -0,0 +1,4 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"]
}

View File

@@ -1,5 +1,6 @@
{
"hosting": {
"target": "iocl",
"public": "build",
"ignore": [
"firebase.json",

View File

@@ -1,45 +0,0 @@
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
}
void loop() {
// put your main code here, to run repeatedly:
Serial.println("SN:182829");
// while (Serial.available() == 0) {} //wait for data available
// String bufferCommand = Serial.readString(); //read until timeout
// bufferCommand.trim(); // remove any \r \n whitespace at the end of the String
// if (bufferCommand == "B") {
// Serial.println("#Start Buffer");
// delay(2000);
// Serial.println("#Buffer Completed");
// }
// while (Serial.available() == 0) {} //wait for data available
// String SampleCommand = Serial.readString(); //read until timeout
// SampleCommand.trim(); // remove any \r \n whitespace at the end of the String
// if (SampleCommand == "S") {
// Serial.println("#Sample Started");
// delay(2000);
// Serial.println("#Sample Completed");
// }
// while (Serial.available() == 0) {} //wait for data available
// String resultCommand = Serial.readString(); //read until timeout
// resultCommand.trim(); // remove any \r \n whitespace at the end of the String
// if (resultCommand == "R") {
// delay(3000);
// Serial.println("RESULT SN 18291 1937.23 2828.11 28211.20 121829.12 REND");
// delay(15000);
// }
while (Serial.available() == 0) {} //wait for data available
String resultCommand = Serial.readString(); //read until timeout
resultCommand.trim(); // remove any \r \n whitespace at the end of the String
if (resultCommand == "B") {
delay(3000);
Serial.println("ERROR 500 REND");
delay(15000);
}
}

19
jest.config.js Normal file
View File

@@ -0,0 +1,19 @@
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
transform: {
"^.+\\.jsx?$": "babel-jest",
'^.+\\.mjs': 'babel-jest',
},
transformIgnorePatterns: [
'/node_modules/',
'node_modules/(?!(d3|other-module-to-transform)/)',
'\\.css$',
],
moduleNameMapper: {
'\\.(svg)$': '<rootDir>/__mocks__/svgMock.js',
'\\.(css|less|sass|scss)$': '<rootDir>/__mocks__/styleMock.js',
'^d3$': '<rootDir>/node_modules/d3/src/index.js',
},
};

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

74
nats.js Normal file
View File

@@ -0,0 +1,74 @@
// const nats = require('nats');
// const fs = require('fs');
// const privateKey = fs.readFileSync('clientCertificate/client-key.pem');
// const certificate = fs.readFileSync('clientCertificate/client-cert.pem');
// const nc = nats.connect({
// servers: ['nats://nanodgx.in:4222'],
// token: 'SUADA3R4HRQG5ZEGR7E65P5P2JUQ2LMVLEOGJWEIKLEYOZEPQES2WHW2KU',
// tls: {
// key: privateKey,
// cert: certificate,
// },
// connect: function() {
// console.log('Connected to NATS server');
// // Subscribe to a subject
// const subscription = nc.subscribe('your-subject');
// // Listen for incoming messages
// subscription.on('message', function(msg) {
// console.log(`Received message: ${msg.getData()}`);
// });
// // Publish a message
// nc.publish('your-subject', 'Hello NATS!');
// },
// error: function(err) {
// console.error(`Error connecting to NATS server: ${err}`);
// },
// close: function() {
// console.log('Connection to NATS server closed');
// },
// });
const NATS = require('nats');
const sc = NATS.StringCodec();
const seed = new TextEncoder().encode(
"SUACFBL4SJNFOYVB6F4AHK7A7VCHDENCKVLKJTTKAH3I47BOTSOE3Z2K7M",
);
const servers = [
{
servers: "nats://nanodgx.in:4222",
authenticator: NATS.nkeyAuthenticator(seed),
tls: {
keyFile: "certificate_path/client-key.pem",
certFile: "certificate_path/client-cert.pem",
},
},
];
servers.forEach(async (v) => {
try {
// console.log(v);
const nc = await NATS.connect(v);
// Subscribe to a subject
const subscription = await nc.subscribe("server.hpos.HCV-000-3998.ping");
/*
(async () => {
for await (const msg of subscription) {
console.log(`Received message: ${sc.decode(msg.data)}`);
// Your custom logic for handling the incoming message goes here
}
})();
*/
nc.publish("server.hpos.HCV-000-3998.ping", "ALIVE");
} catch (err) {
console.log(`error connecting to ${JSON.stringify(v)} ${err}`);
}
});

16278
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,11 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.1",
"@mui/material": "^5.14.2",
"@reduxjs/toolkit": "^1.9.5",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -10,17 +15,26 @@
"d3": "^7.8.5",
"d3-selection": "^3.0.0",
"firebase": "^9.23.0",
"katex": "^0.16.9",
"lodash": "^4.17.21",
"ml-savitzky-golay-generalized": "^4.0.1",
"moment": "^2.29.4",
"nats": "^2.19.0",
"react": "^18.2.0",
"react-datepicker": "^4.16.0",
"react-dom": "^18.2.0",
"react-dropdown": "^1.11.0",
"react-flip-numbers": "^3.0.8",
"react-icons": "^4.10.1",
"react-katex": "^3.0.1",
"react-markdown": "^9.0.1",
"react-mathjax": "^1.0.1",
"react-redux": "^8.1.2",
"react-router-dom": "^6.14.2",
"react-scripts": "5.0.1",
"react-select": "^5.7.4",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"topojson-client": "^3.1.0",
"web-vitals": "^2.1.4"
},
@@ -30,7 +44,8 @@
"test": "react-scripts test",
"eject": "react-scripts eject",
"deploy": "npm run build && firebase deploy",
"coverage": "react-scripts test --coverage"
"coverage": "react-scripts test --coverage",
"test-ci": "jest --testPathIgnorePatterns=src/tests/ --passWithNoTests --ci --bail"
},
"eslintConfig": {
"extends": [
@@ -49,5 +64,15 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.23.7",
"@babel/preset-env": "^7.23.8",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"csv-parser": "^3.0.0",
"html-pdf-node": "^1.0.8",
"jest-environment-jsdom": "^29.7.0",
"ts-jest": "^29.1.1"
}
}

View File

@@ -25,6 +25,11 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>HPOS Sickle Cell Test Dashboard</title>
<script
type="text/javascript"
async
src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS_HTML-full"
></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

Binary file not shown.

View File

@@ -1,64 +0,0 @@
import os
import firebase_admin
from IPython.core.display import Image
from IPython.core.display_functions import display
from firebase_admin import credentials
from firebase_admin import firestore
import pandas as pd
import datetime
print("pritimay")
# Initialize Firebase Admin SDK
cred = credentials.Certificate(r'C:\Users\smila\PycharmProjects\pythonProject\hpos-prod-firebase-adminsdk-bionp-5486f7becd.json') # Replace with your own service account key path
firebase_admin.initialize_app(cred)
# Get a reference to the Firestore database
db = firestore.client()
# Specify the collections
patient_collection = db.collection("patientData")
test_collection = db.collection("testData")
start_date = input("Please enter the start date (yyyy-mm-dd): ")
end_date = input("Please enter the end date (yyyy-mm-dd): ")
query = patient_collection.where("createdAt", ">=", start_date).where("createdAt", "<", end_date)
patient_docs = query.stream()
# Prepare data to store in CSV
data = []
for patient_doc in patient_docs:
patient_data = patient_doc.to_dict()
patient_id = patient_data["_id"]
# Query the document from testData collection based on the common _id
test_docs = test_collection.where("_id", "==", patient_id).stream()
for test_doc in test_docs:
test_data = test_doc.to_dict()
# Combine the data from both collections into a single dictionary
combined_data = {**patient_data, **test_data}
# Skip if the csvPath is not a valid URL
data.append(combined_data)
# Convert the data to a DataFrame
df = pd.DataFrame(data)
print(df.size)
# Save the DataFrame to a CSV file
output_filename = "data.csv"
df.to_csv(output_filename, index=False)
downloads_dir = os.path.join(os.path.expanduser("~"), "Downloads")
output_path = os.path.join(downloads_dir, output_filename)
df.to_csv(output_path, index=False)
print(f"Data saved to '{output_path}'")
# Close the Firebase Admin SDK
firebase_admin.delete_app(firebase_admin.get_app())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

281
scripts/pdf.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,235 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<style>
@media print {
body {
-webkit-print-color-adjust: exact;
}
}
.person-details-row {
display: flex;
flex-direction: row;
}
.person-details-col {
display: flex;
flex-direction: column;
height: 90px;
width: 50%;
margin: 1px;
border: 0;
}
.test-details-row {
border: 0;
}
.test-details-col {
height: 100px;
}
.test-cell-div {
/* border: 1px solid black; */
border-collapse: collapse;
height: 20px;
padding: 10px;
}
.table-header {
border: 1px solid black;
margin: 0 0 -10px 10px;
background-color: rgb(191, 191, 191);
text-align: center;
justify-content: center;
align-items: center;
display: flex;
font-weight: 600;
height: 50px;
width: 98%;
}
@media print {
.table-header {
background-color: rgb(191, 191, 191) !important;
print-color-adjust: exact;
}
}
@media print {
.vendorListHeading th {
color: white !important;
}
}
.test-method {
font-weight: 200 !important;
color: rgb(191, 191, 191);
}
.result-value {
justify-content: center;
align-items: center;
display: flex;
}
.end-of-report {
display: flex;
align-items: center;
justify-content: center;
}
.logo {
display: flex;
justify-content: center;
align-items: center;
}
table,
th,
td {
border: 1px solid black;
border-collapse: collapse;
}
ul {
list-style-type: none;
/* margin: 0; */
/* padding: 0; */
}
</style>
</head>
<body>
<div>
<div class="logo">
<img src="./logo_nobg.png" width="128" height="128" alt="sickle cell logo" />
</div>
<table style="border: 1px solid black; margin: 10px; width: 98%;">
<tr class="person-details-row">
<td class="person-details-col" style="border-right: 2; width: 70%;">
<div><strong>Name:</strong></div>
<div><strong>Age / Gender:</strong></div>
<div><strong>Sample type:</strong></div>
<div><strong>Family History of Sickle Cell Anemia:</strong></div>
</td>
<td class="person-details-col" style="width: 30%;">
<div><strong>Marital Status:</strong></div>
<div><strong>Test Date:</strong></div>
<div><strong>ABHA ID:</strong></div>
<div><strong>Sample ID:</strong></div>
</td>
</tr>
</table>
</div>
<div style="height: 2px;width: 98%; background-color: rgb(56, 105, 166); margin: 30px 10px 40px 10px;"></div>
<div>
<div class="table-header">POINT OF CARE SICKLE CELL ANEMIA TEST</div>
<table class="test-details-table" style="border: 1px solid black; margin: 10px; width: 98%;">
<tr style="height: 50px;">
<th>
Test Description
</th>
<th>
RESULT
</th>
<th>
REFERENCE RANGES
</th>
</tr>
<tr class="test-details-row">
<td class="test-details-col" style="width: 20%; margin: 10px;">
<div style="margin: 10px;">Sickle Cell
Anemia
<div class="test-method">(Method: HPOS)</div>
</div>
</td>
<td style="width: 30%;">
<div class="result-value">Ra = 0.16</div>
<!-- <div class="test-cell-div">Normal</div>
<div class="test-cell-div">Sickle Cell Trait</div>
<div class="test-cell-div">Sickle Cell Disease</div>
<div class="test-cell-div">Negative Borderline</div>
<div class="test-cell-div">Positive Borderline</div> -->
</td>
<td style="width: 50%;">
<div class="test-cell-div">
< 0.16: Normal (HbA)</div>
<div class="test-cell-div">0.165 0.235: Sickle-cell Trait (HbAS)</div>
<div class="test-cell-div">> 0.24: Sickle-cell Disease (HbSS)</div>
<div class="test-cell-div">0.16-0.165: Inconclusive (Negative Borderline)</div>
<div class="test-cell-div">0.235 0.24: Inconclusive (Positive Borderline)</div>
</td>
<!-- <td style="width: 25%;">
<div class="test-cell-div">Normal</div>
<div class="test-cell-div">Sickle Cell Trait</div>
<div class="test-cell-div">Sickle Cell Disease</div>
<div class="test-cell-div">Recommended for HPLC or Electrophoresis Tests</div>
<div class="test-cell-div">Recommended for HPLC or Electrophore</div>
</td> -->
</tr>
</table>
</div>
<div>
<!-- <div style="font-weight: 600; margin: 10px;">INTERPRETATION:</div> -->
<div style="margin: 10px;">
<strong>Test Principle:</strong> This point of care quantitative diagnostic test for sickle-cell anemia
works on the principle of absorption
spectroscopy. The test helps in differentiating heterozygous/homozygous hemoglobin from normal hemoglobin.
</div>
<div style="margin: 10px;">
<strong>Method:</strong> High Performance Optical Spectroscopy (HPOS) for detection of Sickle cell trait and
sickle cell disease in whole blood capillary blood samples.
</div>
<div style="margin: 10px;">
<strong> Note:</strong>
<!-- <ul>
<li>a. Blood transfusion may have an impact on the test results.</li>
<li>
b. Patients already on sickle-cell medications may impact test results.
</li>
</ul> -->
Borderline cases are reported as inconclusive. It may occur due to several factors such as medication,
transfusion, field conditions and assay process. Further clinical tests are recommended in these cases for
diagnosis.
</div>
<div
style="margin: 80px 10px 20px 50px; width: 88%; display: flex; flex-direction: row; justify-content: space-between; align-items: center;">
<div style="margin: 5px;">DATE:</div>
<div style="margin: 5px;">Hematologist</div>
</div>
<!-- <div style="margin: 30px;">
This report is for the perusal of doctor only. Not for medico legal cases. Clinical correlation is
essential.
Please contact us in case of unexpected result.
</div> -->
<div style="height: 3px;width: 98%; background-color: black;"></div>
<div class="end-of-report">
*** END OF REPORT ***</div>
<div style="display: flex; justify-content: center; align-items: center; flex-direction: column;">
<div style="font-style: italic;">
This is an electronically generated report. Generated at HH:MM hrs on DD-MMM-YYYY.
</div>
<div>
Note: Assay results should be correlated clinically with other clinical findings
</div>
</div>
</div>
</body>
</html>

View File

@@ -50,7 +50,7 @@ body {
display: inline-flex;
flex-direction: row;
font-size: xx-large;
background-color: rgb(207, 235, 250);
background-color: rgb(215, 192, 252);
}
.last-update {
@@ -146,7 +146,7 @@ body {
text-align: center;
align-self: center;
font-size: xx-large;
background-color: rgb(207, 235, 250);
background-color: rgb(215, 192, 252);
}
.nscm-logo {
@@ -206,7 +206,7 @@ body {
text-align: center;
align-self: center;
font-size: xx-large;
background-color: rgb(207, 235, 250);
background-color: rgb(215, 192, 252);
}
}
@@ -253,7 +253,7 @@ body {
text-align: center;
align-self: center;
font-size: xx-large;
background-color: rgb(207, 235, 250);
background-color: rgb(215, 192, 252);
}
.title-text {
@@ -289,7 +289,7 @@ body {
}
.cards-container {
height: 90vh;
height: 80vh;
justify-content: center;
align-items: center;
display: flex;
@@ -307,7 +307,7 @@ body {
text-align: center;
align-self: center;
font-size: xx-large;
background-color: rgb(207, 235, 250);
background-color: rgb(215, 192, 252);
}
.title-text {
@@ -356,7 +356,7 @@ body {
}
.cards-container {
height: 90vh;
height: 80vh;
justify-content: center;
align-items: center;
display: flex;
@@ -372,7 +372,7 @@ body {
.dashboard-title {
text-align: center;
align-self: center;
background-color: rgb(207, 235, 250);
background-color: rgb(215, 192, 252);
}
.title-text {

View File

@@ -2,11 +2,27 @@ import './App.css';
import React, { useEffect, useState } from 'react';
import moment from 'moment';
import { BrowserRouter, Route, Routes } from "react-router-dom";
import MainLayout from "./components/MainLayout";
import { routes } from "./routes";
import { collection, onSnapshot, query } from "firebase/firestore";
import Card from './components/Card';
import db from './firebase';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<MainLayout />}>
{routes}
</Route>
</Routes>
</BrowserRouter>
);
}
function App1() {
const [totalScreened, setTotalScreened] = useState(0);
const [males, setMales] = useState(0);
const [females, setFemales] = useState(0);

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,63 @@
.button {
background-color: #4CAF50;
/* Green */
border: none;
color: white;
/* padding: 15px 32px; */
padding: 5px 10px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 15px;
}
.file-row {
flex-direction: row;
display: flex;
justify-content: center;
align-items: center;
margin: 1.5rem;
}
.file-label {
margin: 10px;
}
.dates {
flex-direction: row;
display: flex;
justify-content: center;
align-items: center;
margin: 1.5rem;
}
.start-date {
margin: 10px;
}
.end-date {
margin: 10px;
}
.download-row {
flex-direction: row;
display: flex;
justify-content: center;
align-items: center;
margin: 1.5rem;
}
.loading-icon {
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

107
src/components/Accuracy.js Normal file
View File

@@ -0,0 +1,107 @@
import React, { useState } from 'react';
import { FaInfoCircle } from "react-icons/fa";
import { AiOutlineLoading } from "react-icons/ai";
import DatePicker from "react-datepicker";
import moment from 'moment';
import axios from 'axios';
import "react-datepicker/dist/react-datepicker.css";
import './Accuracy.css';
function Accuracy({ show }) {
const [showCardInfo, setShowCardInfo] = useState(false);
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [selectedFile, setSelectedFile] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null); // New state for handling errors
const handleFileChange = (event) => {
setSelectedFile(event.target.files[0]);
};
const fetchFile = async (start_date, end_date) => {
try {
setLoading(true);
setError(null); // Reset error state before making a new request
if (!selectedFile) {
const formData = new FormData();
// formData.append('file', selectedFile);
const queryString = `start_date=${moment(startDate).format(
'YYYY-MM-DD'
)}&end_date=${moment(endDate).format('YYYY-MM-DD')}`;
const url = `https://asia-south1-${process.env.REACT_APP_FIREBASE_PROJECT_ID}.cloudfunctions.net/performance?${queryString}`;
// Make a POST request using the Fetch API
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
// Add any necessary headers
// 'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
},
});
// Check if the request was successful (status code 2xx)
if (response.ok) {
// Create a blob from the response data
const blob = await response.blob();
// Create a download link and trigger a click to download the file
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'autoanlysis.xlsx'; // Set the desired file name
document.body.appendChild(link);
link.click();
// Clean up
document.body.removeChild(link);
} else {
console.error('Error downloading file:', response.statusText);
setError('Error downloading file. Please try again.'); // Set error message
}
} else {
setError("Select file from reference device");
}
} catch (error) {
console.error('Error downloading file:', error);
setError('Error downloading file. Please try again.'); // Set error message
} finally {
setLoading(false); // Set loading back to false when the request completes
}
};
return (
<div>
<h1>Auto Analysis</h1>
{/* <div className='file-row'>
<div className='file-label'>Reference file</div>
<input type="file" onChange={handleFileChange} />
</div> */}
<div className='dates'>
<div className='start-date'>
<DatePicker selected={startDate} onChange={(date) => setStartDate(date)} placeholderText='start date' />
</div>
<div className='end-date'>
<DatePicker selected={endDate} onChange={(date) => setEndDate(date)} placeholderText='end date' />
</div>
</div>
<div className='download-row'>
<div className='button' onClick={() => fetchFile(startDate, endDate)}>
{loading ? (
<AiOutlineLoading className="loading-icon" />
) : (
'Download Excel'
)}
</div>
</div>
{error && <div className="error-message">{error}</div>} {/* Display error message if there's an error */}
</div>
);
}
export default Accuracy;

View File

39
src/components/Alerts.js Normal file
View File

@@ -0,0 +1,39 @@
import React, { useEffect, useState, useRef } from 'react';
import { query, collection, onSnapshot } from 'firebase/firestore';
import './Alerts.css';
import db from '../firebase';
const Alerts = () => {
const [alerts, setAlerts] = useState([]);
useEffect(() => {
if (!allTests) {
const q = query(collection(db, "alerts"));
const unsub = onSnapshot(q, (querySnapshot) => {
const testDevices = new Set();
const hposTests = [];
querySnapshot.forEach((document) => {
const hposTestData = document.data();
hposTests.push(hposTestData);
const { deviceSerialNumber } = hposTestData;
testDevices.add(deviceSerialNumber);
});
const groups = [...testDevices];
// setDevices(groups);
setAlerts(hposTests);
});
return () => unsub;
}
}, []);
return (
<div>
{alerts.length}
</div>
)
}
export default Alerts;

View File

@@ -0,0 +1,4 @@
.app-qrcode {
height: 250px;
width: 250px;
}

View File

@@ -0,0 +1,38 @@
import React, { useEffect, useState, useRef } from 'react';
import { query, collection, onSnapshot } from 'firebase/firestore';
import './AppInstallation.css';
import db from '../firebase';
const AppInstallation = () => {
const [alerts, setAlerts] = useState([]);
useEffect(() => {
}, []);
return (
<div>
<h1>App Installation</h1>
<h3>First try the firebase app distribution which is email linked. After adding the email from backend. Check the email to get started.</h3>
{/* <div>
<div>Testing 2.1.27 (Preprod - 07-11-2023)</div>
<a href="https://www.dropbox.com/scl/fi/v0hux8qrby54rjns8mhum/hpos-testing_preprod_2.1.27.apk?rlkey=ias0fe0zq72avy01rk4cvzmvp&dl=1">
<img className="app-qrcode" src={require('../assets/hpos-testing_preprod.png')} alt='icon'></img>
</a>
</div>
<div>
<p>Steps to install:</p>
<p>
<ul>
<li>use an QR code scanner app to download the app</li>
<li>if the phone browser prompts an warning, select download anyway</li>
<li>install the downloaded apk and give permissions</li>
</ul>
</p>
</div> */}
</div>
)
}
export default AppInstallation;

View File

@@ -1,9 +0,0 @@
.page-title {
/* width: 100vw; */
padding: 10px;
text-align: center;
background: rgb(207, 235, 250);
color: black;
font-size: 30px;
font-family: sans-serif;
}

View File

@@ -1,78 +0,0 @@
import React, { useEffect, useState } from 'react';
import { query, collection, onSnapshot } from 'firebase/firestore';
import Dropdown from 'react-dropdown';
import 'react-dropdown/style.css';
import moment from 'moment';
import db from '../firebase';
import './Calibrations.css';
const Calibrations = () => {
const [device, setDevice] = useState("Select a device");
const [devices, setDevices] = useState([]);
const [curDeviceData, setCurDeviceData] = useState(null);
const changeDevice = ({ value }) => {
setDevice(value);
}
const toggleShowDevice = () => {
console.log(devices.filter(x => x.deviceId === device))
setCurDeviceData(devices.find(x => x.deviceId === device));
// if (!device) {
// setCurDeviceData(devices.filter(x => x.deviceId === device));
// } else {
// setDevice(null);
// }
}
useEffect(() => {
if (!devices?.length) {
const q = query(collection(db, "devices"));
const unsub = onSnapshot(q, (querySnapshot) => {
const testDevices = [];
querySnapshot.forEach((document) => {
const deviceData = document.data();
testDevices.push(deviceData);
});
setDevices(testDevices);
});
return () => unsub;
}
}, [device, curDeviceData]);
return (
<div className="calibrations-page">
<div className='page-title'>Calibrations</div>
<div className='dropdown-group'>
<Dropdown className='device-dropdown' options={devices?.map(x => x.deviceId)} onChange={changeDevice} value={device} placeholder="Select an device" />
<button className='clear-button' onClick={toggleShowDevice}><img className="graph-icon" src={require('../assets/graph_icon.png')} alt='icon'></img>{device ? 'Clear' : 'Show'}</button>
</div>
<div className='graph-person-conatiner'>
{curDeviceData && <div className='test-details'>
<table>
<tr>
<th>key</th>
<th>value</th>
</tr>
<tr>
<td>device</td> <td>{curDeviceData?.deviceId} </td>
</tr>
<tr>
<td>calibratedAt</td> <td>{curDeviceData?.calibratedAt} </td>
</tr>
<tr>
<td>coefficients</td> <td>{curDeviceData?.coefficients?.toString()} </td>
</tr>
</table>
</div>}
</div>
</div>
)
}
export default Calibrations;

View File

View File

@@ -0,0 +1,65 @@
import React, { useEffect, useState } from 'react';
import { FaInfoCircle } from "react-icons/fa";
import axios from 'axios';
import './CurveFit.css';
import ImageDownloader from './ImageDownloader';
function CurveFit({ show }) {
const [showCardInfo, setShowCardInfo] = useState(false);
const [svgData, setSvgData] = useState(null);
const [loading, setLoading] = useState(true);
const fetchSvg = () => {
axios({
method: 'get',
url: "https://asia-south1-hpos-af3cc.cloudfunctions.net/crdboxplot",
data: {
"_id": "id",
"underMedication": false,
"underTransfusion": false,
"userImageURL": "https://hpos-prod.web.app/logo.png"
},
withCredentials: false,
secure: false,
headers: {
"Content-Type": "application/json",
// "x-client": "e16a1bd15af2ee640f5a7a18c8d8333f5f01f7c66e003bbc21ee52870f1bce27",
},
})
.then(response => response.data)
.then((data) => {
setSvgData(data?.plot?.replace('\"', '').replace('\n', '').replace('"', "'"));
setLoading(false); // Set loading to false once data is fetched
})
.catch((err) => {
console.log(err);
setLoading(false); // Set loading to false in case of an error
});
}
useEffect(() => {
if (!svgData) {
fetchSvg();
}
}, []);
return (
<div>
<h1>Plots</h1>
{loading ? (
<div className="spinner-container">
{/* Add your spinner component here or use a simple text */}
<p>Loading...</p>
</div>
) : (
<img src={`https://asia-south1-${process.env.REACT_APP_FIREBASE_PROJECT_ID}.cloudfunctions.net/crdboxplot`} width="450" height="340" />
)}
{/* <ImageDownloader /> */}
</div>
);
}
export default CurveFit;

View File

@@ -2,14 +2,12 @@
display: none;
}
.insights-title {
/* width: 100vw; */
.title {
padding: 10px;
text-align: center;
background: rgb(207, 235, 250);
background: rgb(215, 192, 252);
color: black;
font-size: 30px;
font-family: sans-serif;
}
.device-dropdown {
@@ -81,7 +79,7 @@
background-color: rgb(207, 235, 250);
width: 10vw;
height: 5vh;
/* margin: 5px; */
margin: 5px;
border-radius: 10px;
font-size: medium;
font-weight: 600;

View File

@@ -1,9 +1,8 @@
import React, { useEffect, useState, useRef } from 'react';
import React, { useEffect, useState } from 'react';
import { query, collection, onSnapshot } from 'firebase/firestore';
import Dropdown from 'react-dropdown';
import 'react-dropdown/style.css';
import moment from 'moment';
import Select from 'react-select';
import DeviceGraph from "./DeviceGraph";
import db from '../firebase';
@@ -19,17 +18,13 @@ const Devices = () => {
const [tests, setTests] = useState([]);
const [allTests, setAllTests] = useState(null);
const [showGraph, setShowGraph] = useState(false);
const selectDateDropdownRef = useRef();
const selectTestDropdownRef = useRef();
const changeDevice = ({ value }) => {
setDevice(value);
setTestDate(null);
setCurTest(null);
selectDateDropdownRef.current?.setValue("");
selectTestDropdownRef.current?.setValue("");
setDays([...new Set(allTests
.filter((x) => x.deviceSerialNumber === value)
.filter((x) => x.deviceSerialNumber === value || x.deviceId === value)
.map(x => moment(x.testTime).format("YYYY-MM-DD")))]
.sort()
.reverse());
@@ -37,12 +32,11 @@ const Devices = () => {
const changeTestDate = ({ value }) => {
setTestDate(value);
selectTestDropdownRef.current?.setValue("");
setCurTest(null);
setCurTestData(null);
setShowGraph(false);
setTests(allTests
.filter((x) => x.deviceSerialNumber === device && moment(x.testTime).isBetween(moment(value).startOf('day'), moment(value).endOf('day')))
.filter((x) => (x.deviceSerialNumber === device || x.deviceId === device) && moment(x.testTime).isBetween(moment(value).startOf('day'), moment(value).endOf('day')))
.map(x => x._id)
.sort());
}
@@ -76,8 +70,13 @@ const Devices = () => {
querySnapshot.forEach((document) => {
const hposTestData = document.data();
hposTests.push(hposTestData);
const { deviceSerialNumber } = hposTestData;
testDevices.add(deviceSerialNumber);
const { deviceType, deviceId, deviceSerialNumber } = hposTestData;
if (deviceType === 'HEMOCUBE' && deviceId) {
testDevices.add(deviceId);
}
if (deviceType !== 'HEMOCUBE' && deviceSerialNumber) {
testDevices.add(deviceSerialNumber);
}
});
const groups = [...testDevices];
setDevices(groups);
@@ -90,45 +89,22 @@ const Devices = () => {
return (
<div className="devices-page">
<div className='insights-title'>Insights</div>
<div className='title'>Insights</div>
<div className='dropdown-group'>
{/* <Dropdown className='device-dropdown' options={devices} onChange={changeDevice} value={device} placeholder="Select an device" />
<Dropdown className='device-dropdown' options={devices} onChange={changeDevice} value={device} placeholder="Select an device" />
<Dropdown className='day-dropdown' options={days} onChange={changeTestDate} value={testDate} placeholder="Select an test date" />
<Dropdown className='test-dropdown' options={tests} onChange={changeTest} value={curTest} placeholder="Select an test" /> */}
<Dropdown className='test-dropdown' options={tests} onChange={changeTest} value={curTest} placeholder="Select an test" />
<Select
className='device-dropdown'
defaultValue={{ label: "Select a device", value: "Select a device" }}
options={devices.map(x => { return { value: x, label: x }; })}
menuPlacement='auto'
maxMenuHeight={250}
menuShouldScrollIntoView={true}
onChange={changeDevice} />
<Select
ref={selectDateDropdownRef}
className='day-dropdown'
defaultValue={{ label: "Select a test date", value: "Select a test date" }}
options={days.map(x => { return { value: x, label: x }; })}
menuPlacement='auto'
maxMenuHeight={250}
menuShouldScrollIntoView={true}
onChange={changeTestDate} />
<Select
ref={selectTestDropdownRef}
className='test-dropdown'
defaultValue={{ label: "Select a test", value: "Select a test" }}
options={tests.map(x => { return { value: x, label: x }; })}
menuPlacement='auto'
maxMenuHeight={220}
menuShouldScrollIntoView={true}
onChange={changeTest} />
{/* <Select className='device-dropdown' options={devices.map(x => { return { value: x, label: x }; })} menuPlacement='auto' maxMenuHeight={250} menuShouldScrollIntoView={true} onChange={changeDevice} />
<Select className='day-dropdown' options={days.map(x => { return { value: x, label: x }; })} menuPlacement='auto' maxMenuHeight={250} menuShouldScrollIntoView={true} onChange={changeTestDate} />
<Select className='test-dropdown' options={tests.map(x => { return { value: x, label: x }; })} menuPlacement='auto' maxMenuHeight={220} menuShouldScrollIntoView={true} onChange={changeTestDate} /> */}
<button className='clear-button' onClick={toggleShowGraph}><img className="graph-icon" src={require('../assets/graph_icon.png')} alt='icon'></img>{curTestData ? 'Clear' : 'Show'}</button>
</div>
<div className='graph-person-conatiner'>
{!curTestData && <div className='refresh-graph-message'>Select test device, date and person to get the detailed graph. Then, click show button to render graph.</div>}
{curTestData?.csvPath && curTestData?.testType !== 'HEMOCUBE' && <DeviceGraph url={curTestData?.csvPath} show={showGraph} />}
{curTestData?.csvPath && <DeviceGraph url={curTestData?.csvPath} show={showGraph} />}
{curTestData && <div className='test-details'>
<table>
@@ -151,26 +127,6 @@ const Devices = () => {
<tr>
<td>result</td> <td>{curTestData?.result}</td>
</tr>
{curTestData?.testType === 'HEMOCUBE' && <tr>
<td>calculatedRatio</td>
<td>{curTestData?.calculatedRatio}</td>
</tr>}
{curTestData?.testType === 'HEMOCUBE' && <tr>
<td>deviceRatio</td>
<td>{curTestData?.deviceRatio}</td>
</tr>}
{curTestData?.testType === 'HEMOCUBE' && <tr>
<td>green led Average</td>
<td>{curTestData?.led1Average}</td>
</tr>}
{curTestData?.testType === 'HEMOCUBE' && <tr>
<td>blue led Average</td>
<td>{curTestData?.led2Average}</td>
</tr>}
{curTestData?.testType === 'HEMOCUBE' && <tr>
<td>blue led Average</td>
<td>{curTestData?.led2Average}</td>
</tr>}
</table>
{/* <img src={curTestData?.userImageURL} width={100} height={150} /> */}
</div>}

View File

View File

@@ -0,0 +1,68 @@
import React, { useEffect, useState, useRef } from 'react';
import { query, collection, onSnapshot, where, orderBy, limit } from 'firebase/firestore';
import moment from 'moment';
import db from '../firebase';
import './Diagnostics.css';
const Diagnostics = () => {
const [samples, setSamples] = useState([]);
useEffect(() => {
if (!samples.length) {
const q = query(collection(db, "diagnostics"), where("runTime", ">=", moment().format("YYYY-MM-DD").toString()), orderBy("runTime", "desc"));
const unsub = onSnapshot(q, (querySnapshot) => {
const samples = [];
querySnapshot.forEach((document) => {
console.log(document.id);
const data = document.data();
const id = document.id;
samples.push({id, ...data});
});
setSamples(samples);
});
return () => unsub;
}
}, []);
return (
<div>
{/* <div className='samples-heading'><h3>Samples page for denovix useage</h3></div>
{samples.length ? (
samples.map((alert, index) => (
<div key={index} className='samples-card'>
<div tabIndex={index}><strong>{alert._id}</strong></div>
<p className='sample-time'>{alert.testTime}</p>
<p className='sample-time-ago'>{moment(alert.testTime).fromNow()}</p>
</div>
))
) : (
<div className='no-samples-message'>Samples will appear as in when a test is recorded</div>
)} */}
<h1>Device Data</h1>
<div className='samples-container'>
<table id="samples-table">
<tr>
<th>Device</th>
<th>Raw Data</th>
<th>Time</th>
</tr>
{samples.map((sample, index) => (
<tr key={index} className='samples-table-tr'>
<td>{sample.deviceId}</td>
<td>{sample.deviceData || 'NA'}</td>
<td>{moment(sample.runTime).format('HH:mm:ss')}</td>
</tr>
))}
</table>
</div>
</div>
)
}
export default Diagnostics;

View File

@@ -0,0 +1,29 @@
.button {
background-color: #4CAF50;
/* Green */
border: none;
color: white;
/* padding: 15px 32px; */
padding: 5px 10px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 15px;
}
.dates {
flex-direction: row;
display: flex;
justify-content: center;
align-items: center;
margin: 1.5rem;
}
.start-date {
margin: 10px;
}
.end-date {
margin: 10px;
}

View File

@@ -0,0 +1,32 @@
import React, { useState } from 'react';
import DatePicker from "react-datepicker";
import moment from 'moment';
import "react-datepicker/dist/react-datepicker.css";
import './ExportData.css';
function ExportData({ url, show }) {
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const fetchFile = (start_date, end_date) => {
window.location.href = `https://asia-south1-${process.env.REACT_APP_FIREBASE_PROJECT_ID}.cloudfunctions.net/consolidation?start_date=${moment(startDate).format("YYYY-MM-DD")}&end_date=${moment(endDate).format("YYYY-MM-DD")}`;
}
return (
<div>
<div className='dates'>
<div className='start-date'>
<DatePicker selected={startDate} onChange={(date) => setStartDate(date)} placeholderText='start date' />
</div>
<div className='end-date'>
<DatePicker selected={endDate} onChange={(date) => setEndDate(date)} placeholderText='end date' />
</div>
<div className='button' onClick={() => fetchFile(startDate, endDate)}>Download Excel</div>
</div>
</div>
);
}
export default ExportData;

103
src/components/Gemini.css Normal file
View File

@@ -0,0 +1,103 @@
.user-chat-input {
margin: 10px;
display: flex;
flex-direction: row;
align-items: start;
justify-content: start;
}
.user-message {
background-color: #aaffaa;
padding: 5px;
margin: 5px;
border-radius: 5px;
}
.other-message {
background-color: #aaaaff;
padding: 5px;
margin: 5px;
border-radius: 5px;
}
.typing-animation {
overflow: hidden; /* Hide overflowing text */
border-right: 0.1em solid #000; /* Add a border to simulate the cursor */
white-space: nowrap; /* Ensure the text stays in one line */
animation: typing 1s steps(40, end), blink-caret 0.5s step-end infinite; /* Typing animation */
}
@keyframes typing {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes blink-caret {
from, to {
border-color: transparent; /* Blinking cursor effect */
}
50% {
border-color: #000;
}
}
/* Apply basic styles to the table */
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
/* Add styling to the table header */
th, td {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
}
/* Style the table header */
th {
background-color: #f2f2f2;
}
/* Add some hover effect to the table rows */
tr:hover {
background-color: #f5f5f5;
}
.loading-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 200px;
}
.loading-icon {
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
/* background-color: #f5f5f5; */
/* border: 1px solid #ddd; */
border-radius: 4px;
margin-bottom: 10px;
width: 100px;
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

239
src/components/Gemini.js Normal file
View File

@@ -0,0 +1,239 @@
import React, { useState, useEffect, useRef } from 'react';
import { query, collection, onSnapshot, addDoc, where, getDocs, orderBy, getCountFromServer } from 'firebase/firestore';
import Markdown from 'react-markdown';
import MathJax from 'react-mathjax';
import RemarkMathPlugin from 'remark-math';
import remarkGfm from 'remark-gfm';
import { debounce } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { FaSpinner } from 'react-icons/fa';
// import { MathJax, MathJaxContext } from "better-react-mathjax";
import './Gemini.css'; // Make sure to have your CSS styles for the typewriter effect
import db from '../firebase';
import 'katex/dist/katex.min.css';
function Gemini(props) {
const [messages, setMessages] = useState([]);
const [newMessage, setNewMessage] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [discussionId, setDiscussionId] = useState(uuidv4());
const [isLoading, setIsLoading] = useState(false);
const [chatCount, setChatCount] = useState(0);
const scrollRef = useRef(null);
const newProps = {
...props,
options: {
...props.options,
inlineMath: [['$', '$'], ['\\(', '\\)']], // Adjust delimiters if needed
displayMath: [['$$', '$$'], ['\\[', '\\]']], // Adjust delimiters if needed
},
remarkPlugins: [
RemarkMathPlugin,
remarkGfm,
],
// renderers: {
// ...props.renderers,
// math: (props) =>
// <MathJax.Node formula={props.value} />,
// inlineMath: (props) =>
// <MathJax.Node inline formula={props.value} />
// }
};
const newComponents = {
...props.components,
math: (props) => (
<MathJax.Node formula={props.value} />
),
inlineMath: (props) => (
<MathJax.Node inline formula={props.value} />
)
};
const getDocumentCount = async (uid) => {
const collectionPath = `users/uid/discussions`;
try {
const querySnapshot = await getCountFromServer(collection(db, collectionPath));
// const documentCount = querySnapshot.size;
// setChatCount(querySnapshot);
console.log(`Document count in the collection "${collectionPath}": ${JSON.stringify(querySnapshot._data.count.integerValue)}`);
} catch (error) {
console.error("Error getting document count:", error);
}
};
useEffect(() => {
const loadChatHistory = async () => {
const uid = 'uid'; // Replace with your user ID
// const discussionId = uuidv4();
console.log(discussionId)
getDocumentCount(uid);
const collectionPath = `users/${uid}/discussions/${discussionId}/messages`;
const q = query(collection(db, collectionPath), orderBy('createTime'));
try {
const querySnapshot = await getDocs(q);
const loadedMessages = [];
querySnapshot.forEach((doc) => {
const data = doc.data();
loadedMessages.push({ text: data.prompt, sender: 'user' });
if (data.response) {
// console.log(data.response)
// Simulate delay for typewriter effect
// setTimeout(() => {
// // Update the state with the Gemini's response
// setMessages((prevMessages) => [
// ...prevMessages,
// { text: data.response, sender: 'gemini' },
// ]);
// // if (scrollRef.current) {
// // // alert(scrollRef.current.scrollHeight)
// // scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
// // }
// }, 10); // You can adjust the delay as needed
loadedMessages.push({ text: data.response, sender: 'gemini' });
}
});
// Sort the messages by createTime before updating the state
// loadedMessages.sort((a, b) => a.createTime - b.createTime);
setMessages(loadedMessages);
} catch (error) {
console.error('Error loading chat history:', error);
}
setTimeout(() => {
if (scrollRef.current) {
// alert(scrollRef.current.scrollHeight)
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, 300);
};
loadChatHistory();
}, []); // Empty dependency array ensures the effect runs only once on component mount
const handleSendMessage = async () => {
if (newMessage.trim() === '') return;
setIsLoading(true);
const uid = 'uid'; // Replace with your user ID
const collectionPath = `users/${uid}/discussions/${discussionId}/messages`;
const collectionRef = collection(db, collectionPath);
// Add user's message to the local state first
setMessages([...messages, { text: newMessage, sender: 'user' }]);
setNewMessage('');
// Then, add the user's message to the Firestore collection
// Your existing code for adding user's message remains unchanged
const docRef = await addDoc(collectionRef, {
prompt: newMessage,
});
// Listen for changes in the Firestore document
const unsub = onSnapshot(docRef, (doc) => {
const data = doc.data();
if (data && data.response) {
// console.log(data.response)
// Start typewriter animation
// setIsTyping(true);
// Simulate delay for typewriter effect
setTimeout(() => {
// Update the state with the Gemini's response
setMessages((prevMessages) => [...prevMessages, { text: data.response, sender: 'gemini' }]);
// Stop typewriter animation
// setIsTyping(false);
setIsLoading(false);
}, 200); // You can adjust the delay as needed
}
// Scroll to the bottom
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
});
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
return unsub;
};
const handleInputChange = debounce((value) => {
setNewMessage(value);
}, 30); // Adjust the delay as needed
return (
<div>
{/* <div>Chats: {chatCount}</div> */}
<div style={{ height: '500px', overflowY: 'scroll', border: '1px solid #ccc' }} ref={scrollRef}>
{messages.map((message, index) => (
<div key={index} className={message.sender === 'user' ? 'user-message' : 'other-message'}>
{message.sender === 'gemini' && isTyping ? (
<div className="typing-animation">Gemini is typing...</div>
) : (
<MathJax.Provider input="tex">
<Markdown components={newComponents} {...newProps}>{message.text}</Markdown>
</MathJax.Provider>
)}
</div>
))}
</div>
<div className="user-chat-input">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSendMessage();
}
}}
placeholder="Type your message..."
style={{ marginRight: '8px', padding: '8px', fontSize: '16px', width: '50vw' }}
/>
<button
onClick={handleSendMessage}
style={{
backgroundColor: '#4CAF50',
color: 'white',
padding: '10px 20px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '16px',
}}
>
Send
</button>
{isLoading && <div className='loading-container'>
<div className="loading-icon">
<FaSpinner className="spin" />
</div>
</div>}
</div>
</div>
);
}
export default Gemini;

19
src/components/Help.css Normal file
View File

@@ -0,0 +1,19 @@
.step-title {
font-weight: 600;
margin: 10px;
margin-top: 50px;
}
.help-image {
margin: 10px;
}
.step-description {
margin: 10px;
}
.help-feature {
font-weight: 900;
font-size: 1.5rem;
margin: 10px;
}

128
src/components/Help.js Normal file
View File

@@ -0,0 +1,128 @@
import React, { useState } from 'react';
import Accordion from '@mui/material/Accordion';
import AccordionDetails from '@mui/material/AccordionDetails';
import AccordionSummary from '@mui/material/AccordionSummary';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import './Help.css';
function Help({ url, show }) {
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [expanded, setExpanded] = useState(false);
const handleChange =
(panel) => (event, isExpanded) => {
setExpanded(isExpanded ? panel : false);
};
return (
<div>
<Accordion expanded={expanded === 'panel1'} onChange={handleChange('panel1')}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1bh-content"
id="panel1bh-header"
>
<Typography sx={{ width: '33%', flexShrink: 0 }}>
Auto Analysis
</Typography>
<Typography sx={{ color: 'text.secondary' }}>Auto Analysis of device data with dye and HB</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
<div className='step-title'>Step 1:</div>
<div className='step-description'>Click on the start date tab and select the necessary date. Repeat the same process on the end date tab as well.</div>
<img className="help-image" src={require('../assets/auto_analysis_help-1.png')} alt='icon'></img>
<div className='step-title'>Step 2:</div>
<div className='step-description'>Select an end date, it has to be at least one date post the start date.</div>
<img className="help-image" src={require('../assets/auto_analysis_help-2.png')} alt='icon'></img>
<div className='step-title'>Step 3:</div>
<div className='step-description'>Click on the Download Excel button and wait for the document to download.</div>
<img className="help-image" src={require('../assets/auto_analysis_help-3.png')} alt='icon'></img>
</Typography>
</AccordionDetails>
</Accordion>
<Accordion expanded={expanded === 'panel2'} onChange={handleChange('panel2')}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel2bh-content"
id="panel2bh-header"
>
<Typography sx={{ width: '33%', flexShrink: 0 }}>Raw Data</Typography>
<Typography sx={{ color: 'text.secondary' }}>
Download raw data
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
<div className='step-title'>Step 1:</div>
<div className='step-description'>Click on the <strong>start date</strong> tab and select the necessary date. Repeat the same process on the <strong>end date</strong> tab as well.</div>
<img className="help-image" src={require('../assets/raw_data_help_1.png')} alt='icon'></img>
<img className="help-image" src={require('../assets/raw_data_help_2.png')} alt='icon'></img>
<div className='step-title'>Step 2:</div>
<div className='step-description'>Click on the <strong>Download Excel</strong> button and wait for the document to download.</div>
<img className="help-image" src={require('../assets/raw_data_help_3.png')} alt='icon'></img>
</Typography>
</AccordionDetails>
</Accordion>
<Accordion expanded={expanded === 'panel3'} onChange={handleChange('panel3')}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel3bh-content"
id="panel3bh-header"
>
<Typography sx={{ width: '33%', flexShrink: 0 }}>
Registration
</Typography>
<Typography sx={{ color: 'text.secondary' }}>
Create and clear entries for taking readings in devices
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
<div className='step-title'>Step 1:</div>
<div className='step-description'>Enter the sample or solution name in the <strong>name</strong> tab with appropriate format given below.</div>
<img className="help-image" src={require('../assets/registration_help_1.png')} alt='icon'></img>
<div className='step-title'>Step 2:</div>
<div className='step-description'>Click on the create button to get an ID and you will receive a confirmation message.
If you wish to clear the generated IDs click on the clear button and you will receive a confirmation message.</div>
<img className="help-image" src={require('../assets/raw_data_help_2.png')} alt='icon'></img>
<img className="help-image" src={require('../assets/raw_data_help_3.png')} alt='icon'></img>
</Typography>
</AccordionDetails>
</Accordion>
{/* <Accordion expanded={expanded === 'panel4'} onChange={handleChange('panel4')}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel4bh-content"
id="panel4bh-header"
>
<Typography sx={{ width: '33%', flexShrink: 0 }}>Personal data</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
Nunc vitae orci ultricies, auctor nunc in, volutpat nisl. Integer sit
amet egestas eros, vitae egestas augue. Duis vel est augue.
</Typography>
</AccordionDetails>
</Accordion> */}
</div>
);
}
export default Help;

View File

@@ -0,0 +1,9 @@
import React from 'react';
const HomePage = (props) => {
return (
<div>HomePage</div>
);
};
export default HomePage;

View File

@@ -0,0 +1,47 @@
import React, { useState } from 'react';
const ImageDownloader = () => {
const [imageUrl, setImageUrl] = useState('');
const [imageData, setImageData] = useState(null);
const handleInputChange = (e) => {
setImageUrl(e.target.value);
};
const handleFetchImage = async () => {
try {
const response = await fetch(imageUrl, { mode: 'no-cors'});
const blob = await response.blob();
console.log(blob);
setImageData(URL.createObjectURL(blob));
} catch (error) {
console.error('Error fetching image:', error);
}
};
const handleDownloadImage = () => {
const link = document.createElement('a');
link.href = imageData;
link.download = 'downloaded_image';
link.click();
};
return (
<div>
<label>
Image URL:
<input type="text" value={imageUrl} onChange={handleInputChange} />
</label>
<button onClick={handleFetchImage}>Fetch Image</button>
{imageData && (
<div>
<img src={imageData} alt="Downloadable" style={{ maxWidth: '100%' }} />
<button onClick={handleDownloadImage}>Download Image</button>
</div>
)}
</div>
);
};
export default ImageDownloader;

View File

View File

@@ -0,0 +1,70 @@
import React, { useEffect, useState, useRef } from 'react';
import { query, collection, onSnapshot, where, orderBy, limit } from 'firebase/firestore';
import moment from 'moment';
import db from '../firebase';
import './KitCheck.css';
const KitCheck = () => {
const [samples, setSamples] = useState([]);
useEffect(() => {
if (!samples.length) {
const q = query(collection(db, "buffers"), where("runTime", ">=", moment().format("YYYY-MM-DD").toString()), orderBy("runTime", "desc"));
const unsub = onSnapshot(q, (querySnapshot) => {
const samples = [];
querySnapshot.forEach((document) => {
console.log(document.id);
const data = document.data();
const id = document.id;
samples.push({id, ...data});
});
setSamples(samples);
});
return () => unsub;
}
}, []);
return (
<div>
{/* <div className='samples-heading'><h3>Samples page for denovix useage</h3></div>
{samples.length ? (
samples.map((alert, index) => (
<div key={index} className='samples-card'>
<div tabIndex={index}><strong>{alert._id}</strong></div>
<p className='sample-time'>{alert.testTime}</p>
<p className='sample-time-ago'>{moment(alert.testTime).fromNow()}</p>
</div>
))
) : (
<div className='no-samples-message'>Samples will appear as in when a test is recorded</div>
)} */}
<h1>Kit Check</h1>
<div className='samples-container'>
<table id="samples-table">
<tr>
<th>Device</th>
<th>Kit</th>
<th>Result</th>
<th>Time</th>
</tr>
{samples.map((sample, index) => (
<tr key={index} className='samples-table-tr'>
<td>{sample.deviceId}</td>
<td>{sample.kitSerial || 'NA'}</td>
<td>{sample.classificationResult || 'NA'}</td>
<td>{moment(sample.runTime).format('HH:mm:ss')}</td>
</tr>
))}
</table>
</div>
</div>
)
}
export default KitCheck;

View File

@@ -1,6 +1,6 @@
.locations-card {
width: 40vw;
height: 40vh;
height: 45vh;
background-color: white;
margin: 10px;
display: flex;
@@ -15,7 +15,7 @@
.locations-graph-card {
width: 40vw;
height: 40vh;
height: 45vh;
background-color: white;
margin: 10px;
border-style: solid;
@@ -27,7 +27,7 @@
.locations-card-title {
text-align: center;
font-weight: 600;
/* padding-top: 10px; */
padding-top: 10px;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -0,0 +1,38 @@
import { Outlet } from "react-router-dom";
import { Box, Toolbar } from "@mui/material";
import colorConfigs from "../configs/colorConfigs";
import sizeConfigs from "../configs/sizeConfigs";
import Sidebar from "./nav/Sidebar";
import Topbar from "./nav/Topbar";
const MainLayout = () => {
return (
<Box sx={{ display: "flex" }}>
<Topbar />
<Box
component="nav"
sx={{
width: sizeConfigs.sidebar.width,
flexShrink: 0
}}
>
<Sidebar />
</Box>
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: `calc(100% - ${sizeConfigs.sidebar.width})`,
minHeight: "100vh",
backgroundColor: colorConfigs.mainBg
}}
>
<Toolbar />
<Outlet />
</Box>
</Box>
);
};
export default MainLayout;

View File

@@ -0,0 +1,19 @@
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { setAppState } from "../store/appStateSlice";
const PageWrapper = (props) => {
const dispatch = useDispatch();
useEffect(() => {
if (props.state) {
dispatch(setAppState(props.state));
}
}, [dispatch, props]);
return (
<>{props.children}</>
);
};
export default PageWrapper;

View File

View File

@@ -0,0 +1,15 @@
import React, { useState } from 'react';
import { FaInfoCircle } from "react-icons/fa";
import './Precision.css';
function Precision({ show }) {
const [showCardInfo, setShowCardInfo] = useState(false);
return (
<div>
<h1>Precision</h1>
</div>
);
}
export default Precision;

View File

@@ -0,0 +1,32 @@
.message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 80vw;
height: 50px;
background-color: rgb(187, 218, 174);
}
.user-creation {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.clear-users-button {
background-color: #b62200;
border: none;
color: white;
padding: 5px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 5px;
}
.format-text {
color: red;
}

View File

@@ -0,0 +1,107 @@
import React, { useState } from 'react';
import moment from 'moment';
import axios from 'axios';
import './Registration.css';
function Registration() {
const [userId, setUserId] = useState("2808202300103SMIBLR");
const [counter, setCounter] = useState(0);
const [message, setMessage] = useState("");
const [name, setName] = useState("");
const createUser = () => {
setMessage("");
const id = moment().unix() + counter + "SMIBLR";
setCounter(counter + 1);
axios({
method: 'post',
url: `https://asia-south1-${process.env.REACT_APP_FIREBASE_PROJECT_ID}.cloudfunctions.net/registerUser`,
data: {
"_id": id,
"aadharId": "2808202300001",
"abhaId": "12434949797979",
"birthYear": "2000",
"bloodGroup": "",
"careOf": "",
"caste": "",
"city": "Bengaluru",
"district": "Bengaluru",
"gender": "Male",
"house": "42",
"maritalStatus": "Single",
"name": name ? name : "Web",
"phoneNumber": "9123456789",
"pinCode": "560060",
"registeredBy": "admin",
"registrationCenterName": "SMI",
"sickleCellHistory": null,
"state": "Karnataka",
"subCaste": "",
"testStatus": false,
"incubationTime": moment().subtract('15', 'minutes').utcOffset("+05:30").format("YYYY-MM-DD HH:mm:ss"),
"underMedication": false,
"underTransfusion": false,
"userImageURL": "https://hpos-prod.web.app/logo.png"
},
withCredentials: false,
secure: false,
headers: {
"Content-Type": "application/json",
"x-client": "e16a1bd15af2ee640f5a7a18c8d8333f5f01f7c66e003bbc21ee52870f1bce27",
},
})
.then(response => response.data)
.then((data) => {
setMessage(`${id} is being created`);
})
.catch((err) => {
console.log(err);
setMessage(`error: ${err?.message}`);
});
}
const clearsUsers = () => {
setMessage("");
axios({
method: 'get',
url: `https://asia-south1-${process.env.REACT_APP_FIREBASE_PROJECT_ID}.cloudfunctions.net/clearUsers`,
withCredentials: false,
secure: false,
headers: {
"Content-Type": "application/json",
// "x-client": "e16a1bd15af2ee640f5a7a18c8d8333f5f01f7c66e003bbc21ee52870f1bce27",
},
})
.then(response => response.data)
.then((data) => {
setMessage(`${data}`);
})
.catch((err) => {
console.log(err);
setMessage(`error: ${err?.message}`);
});
}
const onNameChange = (event) => {
setName(event.target.value);
}
return (
<div className="user-creation">
<label>
name:
<input name="name" value={name} placeholder="350 UMOL KMNO4" onChange={onNameChange} />
</label>
<h5 className='format-text'>Correct formats for automatic analysis are: 100 UMOL KMNO4, 290uM KMnO4, KMnO4-720umol, 350 UMOL TAR, Tar-120umol, HB-13-N-4, HB-11-4</h5>
<div className='dates'>
<div className='button' onClick={() => createUser()}>Create</div>
</div>
<div className='dates'>
<div className='clear-users-button' onClick={() => clearsUsers()}>Clear</div>
</div>
<div>{message && <div className='message'>{message}</div>}</div>
</div>
);
}
export default Registration;

View File

@@ -0,0 +1,39 @@
#samples-table {
font-family: Arial, Helvetica, sans-serif;
border-collapse: collapse;
width: 100%;
font-size: small;
}
#samples-table td,
#samples-table th {
border: 1px solid #ddd;
padding: 8px;
width: 10vw;
}
#samples-table td {
width: 15vw;
}
#samples-table tr:nth-child(even) {
background-color: #f2f2f2;
}
#samples-table tr:hover {
background-color: #ddd;
}
#samples-table th {
padding-top: 12px;
padding-bottom: 12px;
text-align: center;
background-color: rgb(144, 183, 255);
color: black;
width: 5vw;
}
.samples-container {
overflow-x: auto;
overflow-y: auto;
}

87
src/components/Samples.js Normal file
View File

@@ -0,0 +1,87 @@
import React, { useEffect, useState, useRef } from 'react';
import { query, collection, onSnapshot, where, orderBy, limit } from 'firebase/firestore';
import './Samples.css';
import db from '../firebase';
import moment from 'moment';
const Samples = () => {
const [samples, setSamples] = useState([]);
useEffect(() => {
if (!samples.length) {
const q = query(collection(db, "testData"), where("testTime", ">=", moment().format("YYYY-MM-DD").toString()), orderBy("testTime", "desc"));
const unsub = onSnapshot(q, (querySnapshot) => {
const samples = [];
querySnapshot.forEach((document) => {
console.log(document.id);
const data = document.data();
const id = document.id;
samples.push({id, ...data});
});
setSamples(samples);
});
return () => unsub;
}
}, []);
return (
<div>
{/* <div className='samples-heading'><h3>Samples page for denovix useage</h3></div>
{samples.length ? (
samples.map((alert, index) => (
<div key={index} className='samples-card'>
<div tabIndex={index}><strong>{alert._id}</strong></div>
<p className='sample-time'>{alert.testTime}</p>
<p className='sample-time-ago'>{moment(alert.testTime).fromNow()}</p>
</div>
))
) : (
<div className='no-samples-message'>Samples will appear as in when a test is recorded</div>
)} */}
<h1>Samples</h1>
<div className='samples-container'>
<table id="samples-table">
<tr>
<th>Sample Name</th>
<th>CResult</th>
<th>Dev Ratio Class</th>
<th>BM2 Class</th>
<th>Dev Ratio</th>
<th>Slope Ratio</th>
<th>led1Sam.</th>
{/* <th>led2Average</th> */}
<th>led2Sam.</th>
<th>led3Sam.</th>
<th>led4Sam.</th>
<th>Device</th>
<th>Time</th>
</tr>
{samples.map((sample, index) => (
<tr key={index} className='samples-table-tr'>
<td>{sample.name || sample._id}</td>
<td>{sample.classificationResult}</td>
<td>{sample.deviceRatioClass}</td>
<td>{sample.borderlineMethod2Class}</td>
<td>{sample?.deviceRatio?.toFixed(4)}</td>
<td>{sample?.slopeRatio?.toFixed(4)}</td>
<td>{sample.led1Sample || 'NA'}</td>
<td>{sample.led2Sample || 'NA'}</td>
<td>{sample.led3Sample || 'NA'}</td>
<td>{sample.led4Sample || 'NA'}</td>
<td>{sample.deviceId}</td>
<td>{moment(sample.testTime).format('HH:mm:ss')}</td>
</tr>
))}
</table>
</div>
</div>
)
}
export default Samples;

View File

@@ -6,7 +6,7 @@
.resultratio-graph-card {
width: 40vw;
height: 40vh;
height: 45vh;
background-color: white;
margin: 10px;
border-style: solid;

View File

@@ -24,7 +24,7 @@ class ScatterPlot extends Component {
if (!data) return;
// set the dimensions and margins of the graph
var margin = { top: 10, right: 30, bottom: 40, left: 60 },
var margin = { top: 10, right: 30, bottom: 30, left: 60 },
width = 460 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;

View File

@@ -1,38 +1,17 @@
.analytics-page::-webkit-scrollbar {
display: none;
}
.analytics-title::-webkit-scrollbar {
display: none;
}
.analytics-page {
width: 100vw;
}
.analytics-container {
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: sans-serif;
}
.analytics-title {
padding: 10px;
text-align: center;
background: rgb(207, 235, 250);
color: black;
font-size: 30px;
font-family: sans-serif;
height: 50vh;
}
@media screen and (min-width: 900px) {
.analytics-container {
.container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 45vh;
height: 50vh;
}
}

View File

@@ -53,7 +53,10 @@ const TestAnalytics = () => {
const dailyCountsList = Object.entries(dailyCounts).sort().map(x => {
return { date: x[0], count: x[1] };
});
setDailyResults(dailyCountsList);
if (dailyCountsList?.length > 7)
setDailyResults(dailyCountsList.slice(dailyCountsList.length - 7, dailyCountsList.length));
else
setDailyResults(dailyCountsList);
const rss = _.groupBy(hposTests, function (test) {
return moment(test.testTime).startOf('day').format();
});
@@ -82,8 +85,8 @@ const TestAnalytics = () => {
}, [curTestData, curTest, device, testDate]);
return (
<div className="analytics-page">
<div className='analytics-title'>Analytics</div>
<div className="devices-page">
<div className='title'>Analytics</div>
{/* <div className='dropdown-group'>
<Dropdown className='device-dropdown' options={devices} onChange={changeDevice} value={device} placeholder="Select an device" />
<Dropdown className='day-dropdown' options={days} onChange={changeTestDate} value={testDate} placeholder="Select an test date" />
@@ -92,11 +95,11 @@ const TestAnalytics = () => {
<button className='clear-button' onClick={toggleShowGraph}><img className="graph-icon" src={require('./graph_icon.png')}></img>{curTestData ? 'Clear' : 'Show'}</button>
</div> */}
<div className='analytics-container'>
<div className='container'>
{dailyCategoryCounts && <TestAnalyticsPie data={dailyCategoryCounts} />}
{dailyResults && <TestAnalyticsGraph data={dailyResults} />}
</div>
<div className='analytics-container'>
<div className='container'>
{allTests && <ScatterPlot data={allTests} />}
{allTests && <Locations />}
</div>

View File

@@ -6,7 +6,7 @@
.tests-count-card {
width: 40vw;
height: 40vh;
height: 45vh;
background-color: white;
margin: 10px;
/* display: flex;
@@ -58,18 +58,4 @@
font-size: x-small;
font-weight: 400;
padding: 0.5em;
}
div#tests-count-graph {
height: 180px;
width: 430px;
/* border: 2px solid #000; */
overflow-y: auto;
}
svg#bargraph {
height: 180px; /* 500px; */
width: 700px; /* 1100px; */
/* border: 1px dotted #ccc; */
/* background-color: #ccc; */
}

View File

@@ -26,11 +26,10 @@ class TestAnalyticsGraph extends Component {
data.reverse();
var margin = { top: 30, right: 20, bottom: 50, left: 15 },
width = 700 - margin.left - margin.right,
width = 400 - margin.left - margin.right,
height = 180 - margin.top - margin.bottom;
var svg = d3.select("#tests-count-graph").append('svg')
.attr("id", "bargraph")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g").attr("transform",
@@ -41,7 +40,7 @@ class TestAnalyticsGraph extends Component {
yScale = d3.scaleLinear().range([height, 0]);
var g = svg.append("g")
.attr("transform", "translate(" + 5 + "," + 5 + ")");
.attr("transform", "translate(" + 10 + "," + 10 + ")");
xScale.domain(data.map(function (d) { return moment(d.date).format("MM-DD"); }));
@@ -114,7 +113,7 @@ class TestAnalyticsGraph extends Component {
drawChart(data) {
if (!data) return;
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 960 - margin.left - margin.right,
width = 420 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
const INNER_WIDTH = width;// - margin.left - margin.right;

View File

@@ -6,7 +6,7 @@
.category-graph-card {
width: 40vw;
height: 40vh;
height: 45vh;
background-color: white;
margin: 10px;
border-style: solid;

View File

@@ -24,7 +24,7 @@ class TestAnalyticsPie extends Component {
drawChart(data) {
if (!data) return;
var margin = { top: 15, right: 10, bottom: 45, left: 10 },
var margin = { top: 30, right: 10, bottom: 15, left: 25 },
width = 480 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom,
radius = Math.min(width, height) / 2;

View File

@@ -44,7 +44,7 @@ const WorldMap = () => {
}
return (
<svg width={ '40vw' } height={ '35vh' } viewBox="0 0 800 450">
<svg width={ '40vw' } height={ '50vh' } viewBox="0 0 800 450">
<g className="countries">
{
geographies.map((d,i) => (

View File

@@ -0,0 +1,50 @@
import { Avatar, Drawer, List, Stack, Toolbar } from "@mui/material";
// import assets from "../../assets";
import colorConfigs from "../../configs/colorConfigs";
import sizeConfigs from "../../configs/sizeConfigs";
import appRoutes from "../../routes/appRoutes";
import SidebarItem from "./SidebarItem";
import SidebarItemCollapse from "./SidebarItemCollapse";
const Sidebar = () => {
return (
<Drawer
variant="permanent"
sx={{
width: sizeConfigs.sidebar.width,
flexShrink: 0,
"& .MuiDrawer-paper": {
width: sizeConfigs.sidebar.width,
boxSizing: "border-box",
borderRight: "0px",
backgroundColor: colorConfigs.sidebar.bg,
color: colorConfigs.sidebar.color
}
}}
>
<List disablePadding>
<Toolbar sx={{ marginBottom: "20px" }}>
<Stack
sx={{ width: "100%" }}
direction="row"
justifyContent="center"
>
<Avatar src={require('../../smi-logo-120x120.webp')} />
</Stack>
</Toolbar>
{appRoutes.map((route, index) => (
route.sidebarProps ? (
route.child ? (
<SidebarItemCollapse item={route} key={index} />
) : (
<SidebarItem item={route} key={index} />
)
) : null
))}
</List>
</Drawer>
);
};
export default Sidebar;

View File

@@ -0,0 +1,34 @@
import { ListItemButton, ListItemIcon } from "@mui/material";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
import colorConfigs from "../../configs/colorConfigs";
const SidebarItem = ({ item }) => {
const { appState } = useSelector((state) => state.appState);
return (
item.sidebarProps && item.path ? (
<ListItemButton
component={Link}
to={item.path}
sx={{
"&: hover": {
backgroundColor: colorConfigs.sidebar.hoverBg
},
backgroundColor: appState === item.state ? colorConfigs.sidebar.activeBg : "unset",
paddingY: "12px",
paddingX: "24px"
}}
>
<ListItemIcon sx={{
color: colorConfigs.sidebar.color
}}>
{item.sidebarProps.icon && item.sidebarProps.icon}
</ListItemIcon>
{item.sidebarProps.displayText}
</ListItemButton>
) : null
);
};
export default SidebarItem;

View File

@@ -0,0 +1,66 @@
import { Collapse, List, ListItemButton, ListItemIcon, ListItemText, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import colorConfigs from "../../configs/colorConfigs";
import ExpandLessOutlinedIcon from '@mui/icons-material/ExpandLessOutlined';
import ExpandMoreOutlinedIcon from '@mui/icons-material/ExpandMoreOutlined';
import SidebarItem from "./SidebarItem";
import { useSelector } from "react-redux";
const SidebarItemCollapse = ({ item }) => {
const [open, setOpen] = useState(false);
const { appState } = useSelector((state) => state.appState);
useEffect(() => {
if (appState.includes(item.state)) {
setOpen(true);
}
}, [appState, item]);
return (
item.sidebarProps ? (
<>
<ListItemButton
onClick={() => setOpen(!open)}
sx={{
"&: hover": {
backgroundColor: colorConfigs.sidebar.hoverBg
},
paddingY: "12px",
paddingX: "24px"
}}
>
<ListItemIcon sx={{
color: colorConfigs.sidebar.color
}}>
{item.sidebarProps.icon && item.sidebarProps.icon}
</ListItemIcon>
<ListItemText
disableTypography
primary={
<Typography>
{item.sidebarProps.displayText}
</Typography>
}
/>
{open ? <ExpandLessOutlinedIcon /> : <ExpandMoreOutlinedIcon />}
</ListItemButton>
<Collapse in={open} timeout="auto">
<List>
{item.child?.map((route, index) => (
route.sidebarProps ? (
route.child ? (
<SidebarItemCollapse item={route} key={index} />
) : (
<SidebarItem item={route} key={index} />
)
) : null
))}
</List>
</Collapse>
</>
) : null
);
};
export default SidebarItemCollapse;

View File

@@ -0,0 +1,26 @@
import { AppBar, Toolbar, Typography } from "@mui/material";
import colorConfigs from "../../configs/colorConfigs";
import sizeConfigs from "../../configs/sizeConfigs";
const Topbar = () => {
return (
<AppBar
position="fixed"
sx={{
width: `calc(100% - ${sizeConfigs.sidebar.width})`,
ml: sizeConfigs.sidebar.width,
boxShadow: "unset",
backgroundColor: colorConfigs.topbar.bg,
color: colorConfigs.topbar.color
}}
>
<Toolbar>
<Typography variant="h6">
HPOS Web - {process.env.REACT_APP_TOPBAR_TITLE}
</Typography>
</Toolbar>
</AppBar>
);
};
export default Topbar;

View File

@@ -0,0 +1,17 @@
import { colors } from "@mui/material";
const colorConfigs = {
sidebar: {
bg: "#233044",
color: "#eeeeee",
hoverBg: "#1e293a",
activeBg: "#1e253a"
},
topbar: {
bg: "#fff",
color: "#000"
},
mainBg: colors.grey["100"]
};
export default colorConfigs;

View File

@@ -0,0 +1,7 @@
const sizeConfigs = {
sidebar: {
width: "260px"
}
};
export default sizeConfigs;

View File

@@ -3,30 +3,16 @@ import { getFirestore} from "firebase/firestore";
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
// /* dev and test */
// const firebaseConfig = {
// apiKey: "AIzaSyBFl_YkJ5PMIvMY3G70313SyrqemjFnUv8",
// authDomain: "hpos-af3cc.firebaseapp.com",
// projectId: "hpos-af3cc",
// storageBucket: "hpos-af3cc.appspot.com",
// messagingSenderId: "650071678820",
// appId: "1:650071678820:web:ce50538bf46632d46c6471",
// measurementId: "G-6JHQ47S9DD"
// };
/* prod */
const firebaseConfig = {
apiKey: "AIzaSyBF4pLZReAedU4dWX1eRJNFNIaOIz2xk4s",
authDomain: "hpos-prod.firebaseapp.com",
projectId: "hpos-prod",
storageBucket: "hpos-prod.appspot.com",
messagingSenderId: "1012719870714",
appId: "1:1012719870714:web:fcd00acef93f6612de5f43",
measurementId: "G-B0NZ7DEF69"
const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APP_ID,
measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID,
};
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);

View File

@@ -4,6 +4,9 @@ import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import { Provider } from 'react-redux';
import { store } from './store/store';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
@@ -11,7 +14,7 @@ import Devices from './components/Devices';
import TestAnalytics from './components/TestAnalytics';
import WorldMap from './components/WorldMap';
import RegistraionChart from './RegistraionChart';
import Calibrations from './components/Calibrations';
import TestAnalyticsGraph from './components/TestAnalyticsGraph';
const router = createBrowserRouter([
{
@@ -31,19 +34,23 @@ const router = createBrowserRouter([
element: <TestAnalytics />,
},
{
path: "/map",
element: <RegistraionChart />,
path: "/hposdata",
element: <TestAnalyticsGraph />,
},
{
path: "/calibrations",
element: <Calibrations />,
path: "/map",
element: <RegistraionChart />,
}
]);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<RouterProvider router={router} />
<Provider store={store}>
{/* <CssBaseline /> */}
{/* <RouterProvider router={router} /> */}
<App />
</Provider>
);
// If you want to start measuring performance in your app, pass a function

146
src/routes/appRoutes.js Normal file
View File

@@ -0,0 +1,146 @@
import DashboardOutlinedIcon from '@mui/icons-material/DashboardOutlined';
import AppsOutlinedIcon from '@mui/icons-material/AppsOutlined';
import TestAnalytics from "../components/TestAnalytics";
import HomePage from '../components/HomePage';
import Devices from '../components/Devices';
import { Download, FactCheck } from '@mui/icons-material';
import ExportData from '../components/ExportData';
import Registration from '../components/Registration';
import { FaAndroid, FaArrowCircleLeft, FaBars, FaBezierCurve, FaBox, FaChalkboardTeacher, FaChartArea, FaChartLine, FaCheck, FaCommentAlt, FaExclamation, FaFlask, FaGolfBall, FaMicrochip, FaPhone, FaQuestionCircle, FaSearch, FaUser } from 'react-icons/fa';
import AppInstallation from '../components/AppInstallation';
import Samples from '../components/Samples';
import Precision from '../components/Precision';
import Accuracy from '../components/Accuracy';
import CurveFit from '../components/CurveFit';
import Help from '../components/Help';
import Gemini from '../components/Gemini';
import Diagnostics from '../components/Diagnostics';
import KitCheck from '../components/KitCheck';
const appRoutes = [
{
index: true,
element: <HomePage />,
state: "home"
},
{
path: "/analytics",
element: <TestAnalytics />,
state: "analytics",
sidebarProps: {
displayText: "Analytics",
icon: <DashboardOutlinedIcon />
}
},
{
path: "/tests/insights",
element: <Devices />,
state: "insights",
sidebarProps: {
displayText: "Insights",
icon: <AppsOutlinedIcon />
}
},
{
path: "/downloaddata",
element: <ExportData />,
state: "downloaddata",
sidebarProps: {
displayText: "Raw Data",
icon: <Download />
}
},
{
path: "/registration",
element: <Registration />,
state: "registration",
sidebarProps: {
displayText: "Registration",
icon: <FaUser />
}
},
{
path: "/appinstallation",
element: <AppInstallation />,
state: "appinstallation",
sidebarProps: {
displayText: "App Installation",
icon: <FaAndroid />
}
},
{
path: "/samples",
element: <Samples />,
state: "Samples",
sidebarProps: {
displayText: "Samples",
icon: <FaFlask />
}
},
{
path: "/kits",
element: <KitCheck />,
state: "kits",
sidebarProps: {
displayText: "Kit Check",
icon: <FaBox />
}
},
{
path: "/diagnostics",
element: <Diagnostics />,
state: "Diagnostics",
sidebarProps: {
displayText: "Device Data",
icon: <FaMicrochip />
}
},
// {
// path: "/precision",
// element: <Precision />,
// state: "precision",
// sidebarProps: {
// displayText: "Precision",
// icon: <FaCheck />
// }
// },
{
path: "/autoanalysis",
element: <Accuracy />,
state: "autoanalysis",
sidebarProps: {
displayText: "Auto Analysis",
icon: <FaChartLine />
}
},
{
path: "/plots",
element: <CurveFit />,
state: "plots",
sidebarProps: {
displayText: "EDA Plots",
icon: <FaBezierCurve />
}
},
{
path: "/chat",
element: <Gemini />,
state: "chat",
sidebarProps: {
displayText: "Chat Gemini",
icon: <FaCommentAlt />
}
},
{
path: "/helpcenter",
element: <Help />,
state: "helpcenter",
sidebarProps: {
displayText: "Help Center",
icon: <FaQuestionCircle />
}
},
]
export default appRoutes;

34
src/routes/index.js Normal file
View File

@@ -0,0 +1,34 @@
import { Route } from "react-router-dom";
import PageWrapper from "../components/PageWrapper";
import appRoutes from "./appRoutes";
const generateRoute = (routes) => {
return routes.map((route, index) => (
route.index ? (
<Route
index
path={route.path}
element={<PageWrapper state={route.state}>
{route.element}
</PageWrapper>}
key={index}
/>
) : (
<Route
path={route.path}
element={
<PageWrapper state={route.child ? undefined : route.state}>
{route.element}
</PageWrapper>
}
key={index}
>
{route.child && (
generateRoute(route.child)
)}
</Route>
)
));
};
export const routes = generateRoute(appRoutes);

View File

@@ -3,3 +3,4 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
// import '@testing-library/jest-dom/extend-expect';

View File

@@ -0,0 +1,21 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
const initialState = {
appState: ""
};
export const appStateSlice = createSlice({
name: "appState",
initialState,
reducers: {
setAppState: (state, action) => {
state.appState = action.payload;
}
}
});
export const {
setAppState
} = appStateSlice.actions;
export default appStateSlice.reducer;

8
src/store/store.js Normal file
View File

@@ -0,0 +1,8 @@
import { configureStore } from "@reduxjs/toolkit";
import appStateSlice from "./appStateSlice";
export const store = configureStore({
reducer: {
appState: appStateSlice
}
});