Compare commits
24 Commits
preprod
...
Mariya_Var
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbd252b6ad | ||
|
|
31a4b3f278 | ||
|
|
f8a8c6a789 | ||
|
|
0715733163 | ||
|
|
aefcecf90f | ||
|
|
ebd9d23f12 | ||
|
|
e681a4a9bf | ||
|
|
f17408f5ce | ||
|
|
60a8044041 | ||
|
|
2d040ff1c8 | ||
|
|
e9da7a7983 | ||
|
|
4579ad914d | ||
|
|
b4ab8cbff0 | ||
|
|
d35510d39a | ||
|
|
5af8197664 | ||
|
|
29b7db446e | ||
|
|
ca71641341 | ||
|
|
45aecc658e | ||
|
|
5524f135ff | ||
|
|
65f5fdc428 | ||
|
|
4bf75a2473 | ||
|
|
87e4ca7c9a | ||
|
|
e34a618198 | ||
|
|
f9c054c62b |
20
.firebaserc
@@ -1,21 +1,5 @@
|
|||||||
{
|
{
|
||||||
"projects": {
|
"projects": {
|
||||||
"default": "hpos-af3cc"
|
"default": "hpos-af3cc"
|
||||||
},
|
}
|
||||||
"targets": {
|
}
|
||||||
"hpos-af3cc": {
|
|
||||||
"hosting": {
|
|
||||||
"dev1": [
|
|
||||||
"hpos-dev1"
|
|
||||||
],
|
|
||||||
"qa": [
|
|
||||||
"hpos-qa"
|
|
||||||
],
|
|
||||||
"dev": [
|
|
||||||
"hpos-dev"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"etags": {}
|
|
||||||
}
|
|
||||||
|
|||||||
6
.gitignore
vendored
@@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
@@ -28,8 +27,3 @@ yarn-error.log*
|
|||||||
src/components/testData.json
|
src/components/testData.json
|
||||||
public/world-110m.json
|
public/world-110m.json
|
||||||
public/india-states.json
|
public/india-states.json
|
||||||
*.apk
|
|
||||||
|
|
||||||
scripts/reports
|
|
||||||
scripts/data
|
|
||||||
firebase-debug.log
|
|
||||||
|
|||||||
18
README.md
@@ -14,22 +14,4 @@
|
|||||||
http://localhost:3000
|
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 build
|
|
||||||
|
|
||||||
|
|
||||||
### deploy
|
|
||||||
|
|
||||||
firebase deploy --only hosting:dev
|
|
||||||
|
|
||||||
pyinstaller --noconfirm --noconsole -i "smi.ico" --windowed --clean hemocube.py
|
pyinstaller --noconfirm --noconsole -i "smi.ico" --windowed --clean hemocube.py
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"hosting": {
|
"hosting": {
|
||||||
"target": "preprod",
|
|
||||||
"public": "build",
|
"public": "build",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"firebase.json",
|
"firebase.json",
|
||||||
|
|||||||
45
firmware/hemocube_mock/hemocube_mock.ino
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
3007
package-lock.json
generated
11
package.json
@@ -3,11 +3,6 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"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/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
@@ -19,12 +14,10 @@
|
|||||||
"ml-savitzky-golay-generalized": "^4.0.1",
|
"ml-savitzky-golay-generalized": "^4.0.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-datepicker": "^4.16.0",
|
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropdown": "^1.11.0",
|
"react-dropdown": "^1.11.0",
|
||||||
"react-flip-numbers": "^3.0.8",
|
"react-flip-numbers": "^3.0.8",
|
||||||
"react-icons": "^4.10.1",
|
"react-icons": "^4.10.1",
|
||||||
"react-redux": "^8.1.2",
|
|
||||||
"react-router-dom": "^6.14.2",
|
"react-router-dom": "^6.14.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-select": "^5.7.4",
|
"react-select": "^5.7.4",
|
||||||
@@ -56,9 +49,5 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"csv-parser": "^3.0.0",
|
|
||||||
"html-pdf-node": "^1.0.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
scripts/224321944144PADGMC.pdf
Normal file
64
scripts/UserCollection.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
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())
|
||||||
BIN
scripts/logo.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
281
scripts/pdf.js
235
scripts/report_template.html
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<!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>
|
||||||
16
src/App.css
@@ -50,7 +50,7 @@ body {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
background-color: rgb(215, 192, 252);
|
background-color: rgb(207, 235, 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
.last-update {
|
.last-update {
|
||||||
@@ -146,7 +146,7 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
background-color: rgb(215, 192, 252);
|
background-color: rgb(207, 235, 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nscm-logo {
|
.nscm-logo {
|
||||||
@@ -206,7 +206,7 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
background-color: rgb(215, 192, 252);
|
background-color: rgb(207, 235, 250);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
background-color: rgb(215, 192, 252);
|
background-color: rgb(207, 235, 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-text {
|
.title-text {
|
||||||
@@ -289,7 +289,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cards-container {
|
.cards-container {
|
||||||
height: 80vh;
|
height: 90vh;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -307,7 +307,7 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
background-color: rgb(215, 192, 252);
|
background-color: rgb(207, 235, 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-text {
|
.title-text {
|
||||||
@@ -356,7 +356,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cards-container {
|
.cards-container {
|
||||||
height: 80vh;
|
height: 90vh;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -372,7 +372,7 @@ body {
|
|||||||
.dashboard-title {
|
.dashboard-title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
background-color: rgb(215, 192, 252);
|
background-color: rgb(207, 235, 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-text {
|
.title-text {
|
||||||
|
|||||||
16
src/App.js
@@ -2,27 +2,11 @@ import './App.css';
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import moment from 'moment';
|
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 { collection, onSnapshot, query } from "firebase/firestore";
|
||||||
import Card from './components/Card';
|
import Card from './components/Card';
|
||||||
import db from './firebase';
|
import db from './firebase';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
|
||||||
<BrowserRouter>
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<MainLayout />}>
|
|
||||||
{routes}
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function App1() {
|
|
||||||
const [totalScreened, setTotalScreened] = useState(0);
|
const [totalScreened, setTotalScreened] = useState(0);
|
||||||
const [males, setMales] = useState(0);
|
const [males, setMales] = useState(0);
|
||||||
const [females, setFemales] = useState(0);
|
const [females, setFemales] = useState(0);
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,63 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
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-hpos-af3cc.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;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
.app-qrcode {
|
|
||||||
height: 250px;
|
|
||||||
width: 250px;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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;
|
|
||||||
9
src/components/Calibrations.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.page-title {
|
||||||
|
/* width: 100vw; */
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
background: rgb(207, 235, 250);
|
||||||
|
color: black;
|
||||||
|
font-size: 30px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
78
src/components/Calibrations.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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;
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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 fetchSvg = () => {
|
|
||||||
axios({
|
|
||||||
method: 'get',
|
|
||||||
url: "https://asia-south1-hpos-qa.cloudfunctions.net/curvefit",
|
|
||||||
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) => {
|
|
||||||
// setMessage(`${id} is being created`);
|
|
||||||
// setSvgData(data?.plot?.replace('\"', '').replace('\n', '').replace('"', "'"));
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
// setMessage(`error: ${err?.message}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!svgData) {
|
|
||||||
// fetchSvg();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Curve Fit</h1>
|
|
||||||
{/* <img alt='' src={ svgData }/>; */}
|
|
||||||
<img src="https://asia-south1-hpos-qa.cloudfunctions.net/curvefit" width="450" height="340" />
|
|
||||||
|
|
||||||
{/* <ImageDownloader /> */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default CurveFit;
|
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.insights-title {
|
||||||
|
/* width: 100vw; */
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: rgb(215, 192, 252);
|
background: rgb(207, 235, 250);
|
||||||
color: black;
|
color: black;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-dropdown {
|
.device-dropdown {
|
||||||
@@ -79,7 +81,7 @@
|
|||||||
background-color: rgb(207, 235, 250);
|
background-color: rgb(207, 235, 250);
|
||||||
width: 10vw;
|
width: 10vw;
|
||||||
height: 5vh;
|
height: 5vh;
|
||||||
margin: 5px;
|
/* margin: 5px; */
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: medium;
|
font-size: medium;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { query, collection, onSnapshot } from 'firebase/firestore';
|
import { query, collection, onSnapshot } from 'firebase/firestore';
|
||||||
import Dropdown from 'react-dropdown';
|
import Dropdown from 'react-dropdown';
|
||||||
import 'react-dropdown/style.css';
|
import 'react-dropdown/style.css';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import Select from 'react-select';
|
||||||
|
|
||||||
import DeviceGraph from "./DeviceGraph";
|
import DeviceGraph from "./DeviceGraph";
|
||||||
import db from '../firebase';
|
import db from '../firebase';
|
||||||
@@ -18,13 +19,17 @@ const Devices = () => {
|
|||||||
const [tests, setTests] = useState([]);
|
const [tests, setTests] = useState([]);
|
||||||
const [allTests, setAllTests] = useState(null);
|
const [allTests, setAllTests] = useState(null);
|
||||||
const [showGraph, setShowGraph] = useState(false);
|
const [showGraph, setShowGraph] = useState(false);
|
||||||
|
const selectDateDropdownRef = useRef();
|
||||||
|
const selectTestDropdownRef = useRef();
|
||||||
|
|
||||||
const changeDevice = ({ value }) => {
|
const changeDevice = ({ value }) => {
|
||||||
setDevice(value);
|
setDevice(value);
|
||||||
setTestDate(null);
|
setTestDate(null);
|
||||||
setCurTest(null);
|
setCurTest(null);
|
||||||
|
selectDateDropdownRef.current?.setValue("");
|
||||||
|
selectTestDropdownRef.current?.setValue("");
|
||||||
setDays([...new Set(allTests
|
setDays([...new Set(allTests
|
||||||
.filter((x) => x.deviceSerialNumber === value || x.deviceId === value)
|
.filter((x) => x.deviceSerialNumber === value)
|
||||||
.map(x => moment(x.testTime).format("YYYY-MM-DD")))]
|
.map(x => moment(x.testTime).format("YYYY-MM-DD")))]
|
||||||
.sort()
|
.sort()
|
||||||
.reverse());
|
.reverse());
|
||||||
@@ -32,11 +37,12 @@ const Devices = () => {
|
|||||||
|
|
||||||
const changeTestDate = ({ value }) => {
|
const changeTestDate = ({ value }) => {
|
||||||
setTestDate(value);
|
setTestDate(value);
|
||||||
|
selectTestDropdownRef.current?.setValue("");
|
||||||
setCurTest(null);
|
setCurTest(null);
|
||||||
setCurTestData(null);
|
setCurTestData(null);
|
||||||
setShowGraph(false);
|
setShowGraph(false);
|
||||||
setTests(allTests
|
setTests(allTests
|
||||||
.filter((x) => (x.deviceSerialNumber === device || x.deviceId === device) && moment(x.testTime).isBetween(moment(value).startOf('day'), moment(value).endOf('day')))
|
.filter((x) => x.deviceSerialNumber === device && moment(x.testTime).isBetween(moment(value).startOf('day'), moment(value).endOf('day')))
|
||||||
.map(x => x._id)
|
.map(x => x._id)
|
||||||
.sort());
|
.sort());
|
||||||
}
|
}
|
||||||
@@ -70,13 +76,8 @@ const Devices = () => {
|
|||||||
querySnapshot.forEach((document) => {
|
querySnapshot.forEach((document) => {
|
||||||
const hposTestData = document.data();
|
const hposTestData = document.data();
|
||||||
hposTests.push(hposTestData);
|
hposTests.push(hposTestData);
|
||||||
const { deviceType, deviceId, deviceSerialNumber } = hposTestData;
|
const { deviceSerialNumber } = hposTestData;
|
||||||
if (deviceType === 'HEMOCUBE' && deviceId) {
|
testDevices.add(deviceSerialNumber);
|
||||||
testDevices.add(deviceId);
|
|
||||||
}
|
|
||||||
if (deviceType !== 'HEMOCUBE' && deviceSerialNumber) {
|
|
||||||
testDevices.add(deviceSerialNumber);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
const groups = [...testDevices];
|
const groups = [...testDevices];
|
||||||
setDevices(groups);
|
setDevices(groups);
|
||||||
@@ -89,22 +90,45 @@ const Devices = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="devices-page">
|
<div className="devices-page">
|
||||||
<div className='title'>Insights</div>
|
<div className='insights-title'>Insights</div>
|
||||||
<div className='dropdown-group'>
|
<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='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' options={devices.map(x => { return { value: x, label: x }; })} menuPlacement='auto' maxMenuHeight={250} menuShouldScrollIntoView={true} onChange={changeDevice} />
|
<Select
|
||||||
<Select className='day-dropdown' options={days.map(x => { return { value: x, label: x }; })} menuPlacement='auto' maxMenuHeight={250} menuShouldScrollIntoView={true} onChange={changeTestDate} />
|
className='device-dropdown'
|
||||||
<Select className='test-dropdown' options={tests.map(x => { return { value: x, label: x }; })} menuPlacement='auto' maxMenuHeight={220} menuShouldScrollIntoView={true} onChange={changeTestDate} /> */}
|
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} />
|
||||||
|
|
||||||
<button className='clear-button' onClick={toggleShowGraph}><img className="graph-icon" src={require('../assets/graph_icon.png')} alt='icon'></img>{curTestData ? 'Clear' : 'Show'}</button>
|
<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>
|
||||||
|
|
||||||
<div className='graph-person-conatiner'>
|
<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 && <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 && <DeviceGraph url={curTestData?.csvPath} show={showGraph} />}
|
{curTestData?.csvPath && curTestData?.testType !== 'HEMOCUBE' && <DeviceGraph url={curTestData?.csvPath} show={showGraph} />}
|
||||||
|
|
||||||
{curTestData && <div className='test-details'>
|
{curTestData && <div className='test-details'>
|
||||||
<table>
|
<table>
|
||||||
@@ -127,6 +151,26 @@ const Devices = () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>result</td> <td>{curTestData?.result}</td>
|
<td>result</td> <td>{curTestData?.result}</td>
|
||||||
</tr>
|
</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>
|
</table>
|
||||||
{/* <img src={curTestData?.userImageURL} width={100} height={150} /> */}
|
{/* <img src={curTestData?.userImageURL} width={100} height={150} /> */}
|
||||||
</div>}
|
</div>}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const HomePage = (props) => {
|
|
||||||
return (
|
|
||||||
<div>HomePage</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HomePage;
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
.locations-card {
|
.locations-card {
|
||||||
width: 40vw;
|
width: 40vw;
|
||||||
height: 45vh;
|
height: 40vh;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
.locations-graph-card {
|
.locations-graph-card {
|
||||||
width: 40vw;
|
width: 40vw;
|
||||||
height: 45vh;
|
height: 40vh;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
.locations-card-title {
|
.locations-card-title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding-top: 10px;
|
/* padding-top: 10px; */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
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>Pdr Class</th>
|
|
||||||
<th>Dev Ratio Class</th>
|
|
||||||
<th>SR Class</th>
|
|
||||||
<th>PDR</th>
|
|
||||||
<th>Dev Ratio</th>
|
|
||||||
<th>Slope Ratio</th>
|
|
||||||
<th>led1Sample</th>
|
|
||||||
{/* <th>led2Average</th> */}
|
|
||||||
<th>led2Sample</th>
|
|
||||||
<th>led3Sample</th>
|
|
||||||
<th>led4Sample</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.prdClassification}</td>
|
|
||||||
<td>{sample.deviceRatioClass}</td>
|
|
||||||
<td>{sample.slopeRatioClass}</td>
|
|
||||||
<td>{sample?.predictedDenovixRatio?.toFixed(4)}</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;
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
.resultratio-graph-card {
|
.resultratio-graph-card {
|
||||||
width: 40vw;
|
width: 40vw;
|
||||||
height: 45vh;
|
height: 40vh;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ScatterPlot extends Component {
|
|||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
// set the dimensions and margins of the graph
|
// set the dimensions and margins of the graph
|
||||||
var margin = { top: 10, right: 30, bottom: 30, left: 60 },
|
var margin = { top: 10, right: 30, bottom: 40, left: 60 },
|
||||||
width = 460 - margin.left - margin.right,
|
width = 460 - margin.left - margin.right,
|
||||||
height = 250 - margin.top - margin.bottom;
|
height = 250 - margin.top - margin.bottom;
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,38 @@
|
|||||||
.container {
|
.analytics-page::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-title::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-page {
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 50vh;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 900px) {
|
@media screen and (min-width: 900px) {
|
||||||
.container {
|
.analytics-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 50vh;
|
height: 45vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,10 +53,7 @@ const TestAnalytics = () => {
|
|||||||
const dailyCountsList = Object.entries(dailyCounts).sort().map(x => {
|
const dailyCountsList = Object.entries(dailyCounts).sort().map(x => {
|
||||||
return { date: x[0], count: x[1] };
|
return { date: x[0], count: x[1] };
|
||||||
});
|
});
|
||||||
if (dailyCountsList?.length > 7)
|
setDailyResults(dailyCountsList);
|
||||||
setDailyResults(dailyCountsList.slice(dailyCountsList.length - 7, dailyCountsList.length));
|
|
||||||
else
|
|
||||||
setDailyResults(dailyCountsList);
|
|
||||||
const rss = _.groupBy(hposTests, function (test) {
|
const rss = _.groupBy(hposTests, function (test) {
|
||||||
return moment(test.testTime).startOf('day').format();
|
return moment(test.testTime).startOf('day').format();
|
||||||
});
|
});
|
||||||
@@ -85,8 +82,8 @@ const TestAnalytics = () => {
|
|||||||
}, [curTestData, curTest, device, testDate]);
|
}, [curTestData, curTest, device, testDate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="devices-page">
|
<div className="analytics-page">
|
||||||
<div className='title'>Analytics</div>
|
<div className='analytics-title'>Analytics</div>
|
||||||
{/* <div className='dropdown-group'>
|
{/* <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='day-dropdown' options={days} onChange={changeTestDate} value={testDate} placeholder="Select an test date" />
|
||||||
@@ -95,11 +92,11 @@ const TestAnalytics = () => {
|
|||||||
<button className='clear-button' onClick={toggleShowGraph}><img className="graph-icon" src={require('./graph_icon.png')}></img>{curTestData ? 'Clear' : 'Show'}</button>
|
<button className='clear-button' onClick={toggleShowGraph}><img className="graph-icon" src={require('./graph_icon.png')}></img>{curTestData ? 'Clear' : 'Show'}</button>
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<div className='container'>
|
<div className='analytics-container'>
|
||||||
{dailyCategoryCounts && <TestAnalyticsPie data={dailyCategoryCounts} />}
|
{dailyCategoryCounts && <TestAnalyticsPie data={dailyCategoryCounts} />}
|
||||||
{dailyResults && <TestAnalyticsGraph data={dailyResults} />}
|
{dailyResults && <TestAnalyticsGraph data={dailyResults} />}
|
||||||
</div>
|
</div>
|
||||||
<div className='container'>
|
<div className='analytics-container'>
|
||||||
{allTests && <ScatterPlot data={allTests} />}
|
{allTests && <ScatterPlot data={allTests} />}
|
||||||
{allTests && <Locations />}
|
{allTests && <Locations />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
.tests-count-card {
|
.tests-count-card {
|
||||||
width: 40vw;
|
width: 40vw;
|
||||||
height: 45vh;
|
height: 40vh;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
/* display: flex;
|
/* display: flex;
|
||||||
@@ -58,4 +58,18 @@
|
|||||||
font-size: x-small;
|
font-size: x-small;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
padding: 0.5em;
|
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; */
|
||||||
}
|
}
|
||||||
@@ -26,10 +26,11 @@ class TestAnalyticsGraph extends Component {
|
|||||||
data.reverse();
|
data.reverse();
|
||||||
|
|
||||||
var margin = { top: 30, right: 20, bottom: 50, left: 15 },
|
var margin = { top: 30, right: 20, bottom: 50, left: 15 },
|
||||||
width = 400 - margin.left - margin.right,
|
width = 700 - margin.left - margin.right,
|
||||||
height = 180 - margin.top - margin.bottom;
|
height = 180 - margin.top - margin.bottom;
|
||||||
|
|
||||||
var svg = d3.select("#tests-count-graph").append('svg')
|
var svg = d3.select("#tests-count-graph").append('svg')
|
||||||
|
.attr("id", "bargraph")
|
||||||
.attr("width", width + margin.left + margin.right)
|
.attr("width", width + margin.left + margin.right)
|
||||||
.attr("height", height + margin.top + margin.bottom)
|
.attr("height", height + margin.top + margin.bottom)
|
||||||
.append("g").attr("transform",
|
.append("g").attr("transform",
|
||||||
@@ -40,7 +41,7 @@ class TestAnalyticsGraph extends Component {
|
|||||||
yScale = d3.scaleLinear().range([height, 0]);
|
yScale = d3.scaleLinear().range([height, 0]);
|
||||||
|
|
||||||
var g = svg.append("g")
|
var g = svg.append("g")
|
||||||
.attr("transform", "translate(" + 10 + "," + 10 + ")");
|
.attr("transform", "translate(" + 5 + "," + 5 + ")");
|
||||||
|
|
||||||
|
|
||||||
xScale.domain(data.map(function (d) { return moment(d.date).format("MM-DD"); }));
|
xScale.domain(data.map(function (d) { return moment(d.date).format("MM-DD"); }));
|
||||||
@@ -113,7 +114,7 @@ class TestAnalyticsGraph extends Component {
|
|||||||
drawChart(data) {
|
drawChart(data) {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
|
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
|
||||||
width = 420 - margin.left - margin.right,
|
width = 960 - margin.left - margin.right,
|
||||||
height = 500 - margin.top - margin.bottom;
|
height = 500 - margin.top - margin.bottom;
|
||||||
|
|
||||||
const INNER_WIDTH = width;// - margin.left - margin.right;
|
const INNER_WIDTH = width;// - margin.left - margin.right;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
.category-graph-card {
|
.category-graph-card {
|
||||||
width: 40vw;
|
width: 40vw;
|
||||||
height: 45vh;
|
height: 40vh;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class TestAnalyticsPie extends Component {
|
|||||||
drawChart(data) {
|
drawChart(data) {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
var margin = { top: 30, right: 10, bottom: 15, left: 25 },
|
var margin = { top: 15, right: 10, bottom: 45, left: 10 },
|
||||||
width = 480 - margin.left - margin.right,
|
width = 480 - margin.left - margin.right,
|
||||||
height = 250 - margin.top - margin.bottom,
|
height = 250 - margin.top - margin.bottom,
|
||||||
radius = Math.min(width, height) / 2;
|
radius = Math.min(width, height) / 2;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const WorldMap = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg width={ '40vw' } height={ '50vh' } viewBox="0 0 800 450">
|
<svg width={ '40vw' } height={ '35vh' } viewBox="0 0 800 450">
|
||||||
<g className="countries">
|
<g className="countries">
|
||||||
{
|
{
|
||||||
geographies.map((d,i) => (
|
geographies.map((d,i) => (
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
const sizeConfigs = {
|
|
||||||
sidebar: {
|
|
||||||
width: "300px"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default sizeConfigs;
|
|
||||||
@@ -14,62 +14,17 @@ import { getAnalytics } from "firebase/analytics";
|
|||||||
// measurementId: "G-6JHQ47S9DD"
|
// measurementId: "G-6JHQ47S9DD"
|
||||||
// };
|
// };
|
||||||
|
|
||||||
// /* qa: hpos-qa */
|
|
||||||
// const firebaseConfig = {
|
/* prod */
|
||||||
// apiKey: "AIzaSyCF7BNZ5vOMQIpKRi_vxquzWP5pIjcb59I",
|
const firebaseConfig = {
|
||||||
// authDomain: "hpos-qa.firebaseapp.com",
|
apiKey: "AIzaSyBF4pLZReAedU4dWX1eRJNFNIaOIz2xk4s",
|
||||||
// projectId: "hpos-qa",
|
authDomain: "hpos-prod.firebaseapp.com",
|
||||||
// storageBucket: "hpos-qa.appspot.com",
|
projectId: "hpos-prod",
|
||||||
// messagingSenderId: "1004619739289",
|
storageBucket: "hpos-prod.appspot.com",
|
||||||
// appId: "1:1004619739289:web:c3ee0c0127f0d073e5c808",
|
messagingSenderId: "1012719870714",
|
||||||
// measurementId: "G-4MSTJHE64W"
|
appId: "1:1012719870714:web:fcd00acef93f6612de5f43",
|
||||||
// };
|
measurementId: "G-B0NZ7DEF69"
|
||||||
|
|
||||||
|
|
||||||
/* preprod */
|
|
||||||
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"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// /* qa: hpos-qa */
|
|
||||||
// const firebaseConfig = {
|
|
||||||
// apiKey: "AIzaSyCF7BNZ5vOMQIpKRi_vxquzWP5pIjcb59I",
|
|
||||||
// authDomain: "hpos-qa.firebaseapp.com",
|
|
||||||
// projectId: "hpos-qa",
|
|
||||||
// storageBucket: "hpos-qa.appspot.com",
|
|
||||||
// messagingSenderId: "1004619739289",
|
|
||||||
// appId: "1:1004619739289:web:c3ee0c0127f0d073e5c808",
|
|
||||||
// measurementId: "G-4MSTJHE64W"
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
// /* preprod */
|
|
||||||
// const firebaseConfig = {
|
|
||||||
// apiKey: "AIzaSyB2-suWmKh2PgQbRmx7rzPjYhgoovQIsQ8",
|
|
||||||
// authDomain: "hpos-preprod.firebaseapp.com",
|
|
||||||
// projectId: "hpos-preprod",
|
|
||||||
// storageBucket: "hpos-preprod.appspot.com",
|
|
||||||
// messagingSenderId: "121176529204",
|
|
||||||
// appId: "1:121176529204:web:6c0db8e642992192bbed61",
|
|
||||||
// measurementId: "G-GYM6LRQ8KR"
|
|
||||||
// };
|
|
||||||
|
|
||||||
// /* 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 app = initializeApp(firebaseConfig);
|
const app = initializeApp(firebaseConfig);
|
||||||
|
|||||||
19
src/index.js
@@ -4,9 +4,6 @@ import {
|
|||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
|
|
||||||
import { store } from './store/store';
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
@@ -14,7 +11,7 @@ import Devices from './components/Devices';
|
|||||||
import TestAnalytics from './components/TestAnalytics';
|
import TestAnalytics from './components/TestAnalytics';
|
||||||
import WorldMap from './components/WorldMap';
|
import WorldMap from './components/WorldMap';
|
||||||
import RegistraionChart from './RegistraionChart';
|
import RegistraionChart from './RegistraionChart';
|
||||||
import TestAnalyticsGraph from './components/TestAnalyticsGraph';
|
import Calibrations from './components/Calibrations';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -33,24 +30,20 @@ const router = createBrowserRouter([
|
|||||||
path: "/analytics",
|
path: "/analytics",
|
||||||
element: <TestAnalytics />,
|
element: <TestAnalytics />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/hposdata",
|
|
||||||
element: <TestAnalyticsGraph />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/map",
|
path: "/map",
|
||||||
element: <RegistraionChart />,
|
element: <RegistraionChart />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/calibrations",
|
||||||
|
element: <Calibrations />,
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
root.render(
|
root.render(
|
||||||
<Provider store={store}>
|
<RouterProvider router={router} />
|
||||||
{/* <CssBaseline /> */}
|
|
||||||
{/* <RouterProvider router={router} /> */}
|
|
||||||
<App />
|
|
||||||
</Provider>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
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, FaChalkboardTeacher, FaChartArea, FaChartLine, FaCheck, FaExclamation, FaFlask, FaGolfBall, 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';
|
|
||||||
|
|
||||||
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: "/precision",
|
|
||||||
// element: <Precision />,
|
|
||||||
// state: "precision",
|
|
||||||
// sidebarProps: {
|
|
||||||
// displayText: "Precision",
|
|
||||||
// icon: <FaCheck />
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
path: "/autoanalysis",
|
|
||||||
element: <Accuracy />,
|
|
||||||
state: "autoanalysis",
|
|
||||||
sidebarProps: {
|
|
||||||
displayText: "Auto Analysis (NEW)",
|
|
||||||
icon: <FaChartLine />
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// path: "/curvefit",
|
|
||||||
// element: <CurveFit />,
|
|
||||||
// state: "curvefit",
|
|
||||||
// sidebarProps: {
|
|
||||||
// displayText: "Linear Fit",
|
|
||||||
// icon: <FaBezierCurve />
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
path: "/helpcenter",
|
|
||||||
element: <Help />,
|
|
||||||
state: "helpcenter",
|
|
||||||
sidebarProps: {
|
|
||||||
displayText: "Help Center",
|
|
||||||
icon: <FaQuestionCircle />
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export default appRoutes;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit";
|
|
||||||
import appStateSlice from "./appStateSlice";
|
|
||||||
|
|
||||||
export const store = configureStore({
|
|
||||||
reducer: {
|
|
||||||
appState: appStateSlice
|
|
||||||
}
|
|
||||||
});
|
|
||||||