77 Commits

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

View File

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

14
.gitignore vendored
View File

@@ -13,6 +13,7 @@
# misc # misc
.DS_Store .DS_Store
.env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
@@ -27,3 +28,16 @@ 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
.env.preprod
.env.prod
.env.qa
.env.dev
.env.uat
.env.iocl
certificate_path/

18
.gitlab-ci.yml Normal file
View File

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

View File

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

1
__mocks__/styleMock.js Normal file
View File

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

1
__mocks__/svgMock.js Normal file
View File

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

4
babel.config.json Normal file
View File

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

View File

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

19
jest.config.js Normal file
View File

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

BIN
logo_nobg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

74
nats.js Normal file
View File

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

16278
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,11 @@
"version": "0.1.0", "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",
@@ -10,17 +15,26 @@
"d3": "^7.8.5", "d3": "^7.8.5",
"d3-selection": "^3.0.0", "d3-selection": "^3.0.0",
"firebase": "^9.23.0", "firebase": "^9.23.0",
"katex": "^0.16.9",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"ml-savitzky-golay-generalized": "^4.0.1", "ml-savitzky-golay-generalized": "^4.0.1",
"moment": "^2.29.4", "moment": "^2.29.4",
"nats": "^2.19.0",
"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-katex": "^3.0.1",
"react-markdown": "^9.0.1",
"react-mathjax": "^1.0.1",
"react-redux": "^8.1.2",
"react-router-dom": "^6.14.2", "react-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",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"topojson-client": "^3.1.0", "topojson-client": "^3.1.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
@@ -30,7 +44,8 @@
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"deploy": "npm run build && firebase deploy", "deploy": "npm run build && firebase deploy",
"coverage": "react-scripts test --coverage" "coverage": "react-scripts test --coverage",
"test-ci": "jest --testPathIgnorePatterns=src/tests/ --passWithNoTests --ci --bail"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@@ -49,5 +64,15 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"@babel/core": "^7.23.7",
"@babel/preset-env": "^7.23.8",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"csv-parser": "^3.0.0",
"html-pdf-node": "^1.0.8",
"jest-environment-jsdom": "^29.7.0",
"ts-jest": "^29.1.1"
} }
} }

View File

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

281
scripts/pdf.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -2,11 +2,27 @@ 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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

View File

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

View File

@@ -24,7 +24,7 @@ const Devices = () => {
setTestDate(null); setTestDate(null);
setCurTest(null); setCurTest(null);
setDays([...new Set(allTests setDays([...new Set(allTests
.filter((x) => x.deviceSerialNumber === value) .filter((x) => x.deviceSerialNumber === value || x.deviceId === value)
.map(x => moment(x.testTime).format("YYYY-MM-DD")))] .map(x => moment(x.testTime).format("YYYY-MM-DD")))]
.sort() .sort()
.reverse()); .reverse());
@@ -36,7 +36,7 @@ const Devices = () => {
setCurTestData(null); setCurTestData(null);
setShowGraph(false); setShowGraph(false);
setTests(allTests setTests(allTests
.filter((x) => x.deviceSerialNumber === device && moment(x.testTime).isBetween(moment(value).startOf('day'), moment(value).endOf('day'))) .filter((x) => (x.deviceSerialNumber === device || x.deviceId === device) && moment(x.testTime).isBetween(moment(value).startOf('day'), moment(value).endOf('day')))
.map(x => x._id) .map(x => x._id)
.sort()); .sort());
} }
@@ -70,8 +70,13 @@ const Devices = () => {
querySnapshot.forEach((document) => { querySnapshot.forEach((document) => {
const hposTestData = document.data(); const hposTestData = document.data();
hposTests.push(hposTestData); hposTests.push(hposTestData);
const { deviceSerialNumber } = hposTestData; const { deviceType, deviceId, deviceSerialNumber } = hposTestData;
testDevices.add(deviceSerialNumber); if (deviceType === 'HEMOCUBE' && deviceId) {
testDevices.add(deviceId);
}
if (deviceType !== 'HEMOCUBE' && deviceSerialNumber) {
testDevices.add(deviceSerialNumber);
}
}); });
const groups = [...testDevices]; const groups = [...testDevices];
setDevices(groups); setDevices(groups);

View File

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -53,7 +53,10 @@ 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] };
}); });
setDailyResults(dailyCountsList); if (dailyCountsList?.length > 7)
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();
}); });

View File

@@ -25,9 +25,9 @@ class TestAnalyticsGraph extends Component {
data.reverse(); data.reverse();
var margin = { top: 60, right: 20, bottom: 30, left: 50 }, var margin = { top: 30, right: 20, bottom: 50, left: 15 },
width = 520 - margin.left - margin.right, width = 400 - margin.left - margin.right,
height = 220 - 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("width", width + margin.left + margin.right) .attr("width", width + margin.left + margin.right)
@@ -113,7 +113,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 = 960 - margin.left - margin.right, width = 420 - 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,9 @@ 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';
@@ -11,6 +14,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';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -29,6 +33,10 @@ const router = createBrowserRouter([
path: "/analytics", path: "/analytics",
element: <TestAnalytics />, element: <TestAnalytics />,
}, },
{
path: "/hposdata",
element: <TestAnalyticsGraph />,
},
{ {
path: "/map", path: "/map",
element: <RegistraionChart />, element: <RegistraionChart />,
@@ -38,7 +46,11 @@ const router = createBrowserRouter([
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( root.render(
<RouterProvider router={router} /> <Provider store={store}>
{/* <CssBaseline /> */}
{/* <RouterProvider router={router} /> */}
<App />
</Provider>
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

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

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

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

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

View File

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

View File

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

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

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