This commit is contained in:
ilia 2022-12-09 16:51:15 +03:00
parent 45f5c4e5b1
commit ad9b072c40
84 changed files with 4062 additions and 108 deletions

1882
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,9 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.8.0",
"@reduxjs/toolkit": "^1.9.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@ -10,9 +13,15 @@
"@types/node": "^16.18.6",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"antd": "^5.0.4",
"antd-mask-input": "^2.0.7",
"axios": "^1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.5",
"react-scripts": "5.0.1",
"redux": "^4.2.0",
"typescript": "^4.9.3",
"web-vitals": "^2.1.4"
},

View File

@ -29,6 +29,14 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<style>
body{
background-color: white;
}
*::-webkit-scrollbar {
display: none;
}
</style>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

View File

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -1,25 +1,28 @@
import React from 'react';
import logo from './logo.svg';
import './App.css';
import {RouterProvider} from 'react-router-dom';
import { Header } from './components/Header';
import router from './router';
import {ConfigProvider} from 'antd';
import store from './store'
import { Provider } from 'react-redux';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
<>
<Provider store={store}>
<ConfigProvider
theme={{
token: {
colorPrimary: '#00ABB5'
}
}}
>
Learn React
</a>
</header>
</div>
<RouterProvider router={router}></RouterProvider>
</ConfigProvider>
</Provider>
</>
);
}

View File

@ -0,0 +1,33 @@
import { ShareAltOutlined } from '@ant-design/icons'
import { Avatar, Tag, Typography } from 'antd'
import react from 'react'
import './style.css'
interface IArticelPreview{
doctorName: string;
tags: string[]
title: string
time: string
}
export const ArticlePreview: react.FC<IArticelPreview> = (props) => {
return <div className="article-preview__container">
<div className="article-preview__headings">
<div className="article-preview__author">
<Avatar>Врач</Avatar>
<Typography.Text>{props.doctorName}</Typography.Text>
</div>
<ShareAltOutlined />
</div>
<div className="tags">
{
props.tags.map(e => <Tag>{e}</Tag>)
}
</div>
<Typography.Text style={{fontWeight: 'bold', marginTop: 30, paddingRight: 20}}>
{props.title}
</Typography.Text>
<Typography.Text type='secondary'>{props.time}</Typography.Text>
</div>
}

View File

@ -0,0 +1,29 @@
.article-preview__container{
display: flex;
flex-direction: column;
justify-content: start;
gap: 10px;
box-shadow: 0px 0px 25px rgba(0, 0, 0, 0.06);
border-radius: 6px;
width: 300px;
padding: 20px;
}
.article-preview__author{
display: flex;
align-items: center;
gap: 5px;
}
.article-preview__headings{
display: flex;
justify-content: space-between;
flex-direction: row;
}
.articles__list{
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 20px;
margin-top: 30px;
}

View File

@ -0,0 +1,54 @@
import react from 'react'
import './style.css'
interface IColorBlock {
text: string;
color: string;
onSelected: () => void;
selected: boolean;
}
const ColorBlock: react.FC<IColorBlock> = (props) => {
return <div className='color-block__container' onClick={() => {
props.onSelected();
}}>
<div className="circle" style={{background: props.color}}></div>
<div className="text" style={{
fontWeight: props.selected ? '600' : '500'
}}> - {props.text}</div>
</div>
}
interface IColorAssign {
names: string[]
}
export const ColorsAssign: react.FC<IColorAssign> = (props) => {
const colors = [
'#56D96B',
'rgba(86, 217, 107, 0.8)',
'linear-gradient(0deg, rgba(245, 199, 33, 0.1), rgba(245, 199, 33, 0.1)), rgba(86, 217, 107, 0.6)',
'rgba(245, 199, 33, 0.6)',
'rgba(245, 199, 33, 0.8)',
'#F5C721',
'linear-gradient(0deg, rgba(249, 87, 33, 0.3), rgba(249, 87, 33, 0.3)), rgba(245, 199, 33, 0.8)',
'rgba(249, 87, 33, 0.8)',
'#F95721'
]
const [selected, setSelected] = react.useState(new Array(10).fill(false));
return <>
{props.names.map((e, index) => {
return <ColorBlock
color={colors[index]}
text={e}
selected={selected[index]}
onSelected={() => {
setSelected(
selected.map((sel, selIndex) => selIndex == index ? true : false)
);
}}
/>
})}
</>
}

View File

@ -0,0 +1,11 @@
.circle{
border-radius: 50%;
width: 20px;
height: 20px;
}
.color-block__container{
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}

View File

@ -0,0 +1,8 @@
import { Input } from 'antd'
import react from 'react'
export const NumberAssign: react.FC = () => {
return <>
<Input />
</>
}

View File

@ -0,0 +1,32 @@
import { Typography } from 'antd'
import react from 'react'
import { ColorsAssign } from '../ColorsAssign'
import { NumberAssign } from '../NumberAssign'
import './style.css'
interface IAssignQuestion{
type: "Colors" | "Input";
title: string;
}
export const AssignQuestion: react.FC<IAssignQuestion> = (props) => {
return <div className="assign-question-el__container">
<Typography.Text style={{fontWeight: 'bold', marginBottom: 20}}>{props.title}</Typography.Text>
{
props.type == 'Colors' ? <ColorsAssign
names={[
"Тошноты нет",
"Что-то чувствую",
"Небольшая тошнота",
"Подташнивает",
"Тошнит",
"Очень тошнит",
"Очень сильно тошнит",
"Сейчас вырвет",
"Рвота"
]}
/> : <NumberAssign />
}
</div>
}

View File

@ -0,0 +1,8 @@
.assign-question-el__container{
display: flex;
padding: 20px;
box-shadow: 0px 0px 25px rgba(0, 0, 0, 0.06);
border-radius: 6px;
flex-direction: column;
gap: 10px;
}

View File

@ -0,0 +1,12 @@
import { Typography } from 'antd'
import react from 'react'
import './style.css'
export const DoctorAdd: react.FC = () => {
return <div className='doctor-add__container'>
<Typography.Text style={{fontWeight: 'bold'}}>
Прикрепиться к врачу
</Typography.Text>
</div>
}

View File

@ -0,0 +1,10 @@
.doctor-add__container{
width: 400px;
height: 116px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 6px;
border: 1px solid #00ABB5;
cursor: pointer;
}

View File

@ -0,0 +1,15 @@
import { Avatar, Button, Typography } from 'antd'
import react from 'react'
import './style.css'
export const DoctorPreview: react.FC = () => {
return <div className='doctor-preview__container'>
<Avatar>Врач</Avatar>
<div className="doctor-preview__content">
<Typography.Text>Севастьянов А.А</Typography.Text>
<Typography.Text type='secondary'>Врач гастроэнтеролог</Typography.Text>
<Button>Написать врачу</Button>
</div>
</div>
}

View File

@ -0,0 +1,14 @@
.doctor-preview__container{
display: inline-flex;
flex-direction: row;
gap: 5px;
box-shadow: 0px 0px 25px rgba(0, 0, 0, 0.06);
border-radius: 6px;
width: 400px;
padding: 15px;
}
.doctor-preview__content{
display: flex;
flex-direction: column;
gap: 5px;
}

View File

@ -0,0 +1,13 @@
import react from 'react';
import { Popover, Button } from 'antd';
import {EllipsisOutlined} from '@ant-design/icons'
interface DottedTemplate {
content: react.ReactNode
}
export const DottedTemplate: react.FC<DottedTemplate> = (props) => {
return <Popover content={props.content} trigger={'click'}>
<Button type='text' icon={<EllipsisOutlined style={{transform: 'rotate(90deg)'}}/>}/>
</Popover>
}

View File

@ -0,0 +1,23 @@
import { PlusOutlined } from '@ant-design/icons'
import { Button, Typography } from 'antd'
import react from 'react'
import { useNavigate } from 'react-router-dom'
export const AssignHeader: react.FC = () => {
const navigate = useNavigate();
return <>
<Button className="cross" icon={<PlusOutlined />} type='text' onClick={() => {
navigate(-1);
}}/>
<div className='header__container'>
<div className="positioned">
<div className="logo__container">
<Typography.Title level={3}>Название анкеты</Typography.Title>
<Button type='primary' onClick={() => {
navigate(-1)
}}>Отправить анкету</Button>
</div>
</div>
</div>
</>
}

View File

@ -0,0 +1,25 @@
import { PlusOutlined } from '@ant-design/icons'
import { Button, Typography } from 'antd'
import react from 'react'
import { useNavigate } from 'react-router-dom'
import './style.css'
export const CreateQuestionHeader: react.FC = () => {
const navigate = useNavigate();
return <>
<Button className="cross" icon={<PlusOutlined />} type='text' onClick={() => {
navigate(-1);
}}/>
<div className='header__container'>
<div className="positioned">
<div className="logo__container">
<Typography.Title level={3}>Создание анкеты</Typography.Title>
<Button type='primary' onClick={() => {
navigate(-1)
}}>Создать анкету</Button>
</div>
</div>
</div>
</>
}

View File

@ -0,0 +1,80 @@
import react from 'react'
import { Logo } from './logo';
import {Avatar, Typography} from 'antd';
import {useLocation} from 'react-router-dom';
import { green, grey } from '@ant-design/colors';
import './style.css';
export const Header: react.FC = () => {
const [selected, setSelected] = react.useState(-1)
const location = useLocation()
console.log(location.pathname);
var links = [
"/patients",
"/questions",
"/articles",
"https://t.me/+W2DTe7Qw5e9lYmVi"
]
var linkText = [
'Пациенты',
'Анкеты',
'Статьи',
'Чаты'
]
var avatarName = 'Врач'
var fullName = 'Севастьянов А.А.'
if (location.pathname.startsWith('/index')) {
links = [
'/index/diary',
'/index/articles',
'https://t.me/s4nspie'
]
linkText = [
'Дневник пациента',
'Рекомендации',
'Чаты'
]
avatarName = 'Пациент'
fullName = 'Иванов И.И.'
}
for (var i = 0; i < links.length; ++i) {
if (i != selected) {
if (location.pathname == links[i]) {
setSelected(i);
}
}
}
return <div className="header__container">
<div className="positioned">
<div className="header__content">
<div className="logo__container">
<Logo />
<div className="header__menu">
{
linkText.map(
e => <Typography.Link
href={links[linkText.indexOf(e)]}
style ={{color:
linkText.indexOf(e) == selected ?
'#00ABB5' : 'black'}}
>
{e}
</Typography.Link>
)
}
</div>
<div className="doctor__menu">
<Avatar>{avatarName}</Avatar>
<div className="fi">{fullName}</div>
</div>
</div>
</div>
</div>
</div>
}

View File

@ -0,0 +1,25 @@
export const Logo = () => {
return <svg width="155" height="18" viewBox="0 0 155 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_37_89)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.80301 0.947266C7.6842 0.947266 9.58836 1.4 10.9485 2.16992V5.34042C9.61132 4.38887 7.82021 3.91377 6.05251 3.93658C4.32984 3.95895 3.46829 4.4345 3.46829 5.40842C3.46829 8.14855 12.1501 7.129 12.1501 12.7681C12.1501 16.3233 9.29382 17.4332 5.96154 17.4332C3.76283 17.4332 1.72266 16.9121 0.0905271 16.0066V12.7681C1.90416 13.9684 3.89885 14.4435 5.71248 14.4435C7.54819 14.4435 8.68177 14.0587 8.68177 12.9264C8.68177 9.95995 0 11.0699 0 5.45361C0 2.39629 2.67519 0.947266 5.80301 0.947266Z" fill="#E30611"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.0096 1.26465V17.1163H27.6093V7.35557L23.4159 13.7194H21.4438L17.2504 7.3332V17.1163H13.8501V1.26465H17.0009L22.4413 9.50786L27.8588 1.26465H31.0096Z" fill="#E30611"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M37.9604 11.3416H43.0829L40.8616 5.65781C40.7481 5.34062 40.6351 5.02344 40.5216 4.41233C40.4086 5.02344 40.2951 5.34062 40.1591 5.65781L37.9604 11.3416ZM32.2024 17.1158L38.6859 1.26465H42.3578L48.8179 17.1158H45.3045L44.1489 14.1493H36.8948L35.7157 17.1158H32.2024Z" fill="#E30611"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M53.4113 8.73749H56.6981C58.1262 8.73749 59.3728 8.10312 59.3728 6.49528C59.3728 4.86507 58.1262 4.25396 56.6981 4.25396H53.4113V8.73749ZM50.011 17.1163V1.26465H56.5391C59.6219 1.26465 62.8411 2.41886 62.8411 6.42773C62.8411 9.14504 61.2999 10.5261 59.3728 11.1381L64.1102 17.1163H60.1889L55.9045 11.568H53.4113V17.1163H50.011Z" fill="#E30611"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M76.4488 1.26465V4.32152H71.763V17.1163H68.3852V4.32152H63.6995V1.26465H76.4488Z" fill="#E30611"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M147.732 9.18974C147.732 11.9366 147.359 14.594 146.664 17.1158H148.609C149.261 14.5846 149.609 11.9285 149.609 9.18974C149.609 6.4514 149.261 3.79537 148.609 1.26416H146.664C147.359 3.78598 147.732 6.44334 147.732 9.18974Z" fill="#00ABB5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M153.756 9.18974C153.756 11.9147 153.452 14.5676 152.878 17.1158H153.839C154.398 14.564 154.694 11.912 154.694 9.18974C154.694 6.4675 154.398 3.81595 153.839 1.26416H152.878C153.452 3.81237 153.756 6.46527 153.756 9.18974Z" fill="#00ABB5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M135.683 9.18974C135.683 12.0493 134.991 14.7434 133.772 17.1158H137.91C138.895 14.67 139.439 11.9947 139.439 9.18974C139.439 6.38519 138.895 3.70992 137.91 1.26416H133.772C134.991 3.63655 135.683 6.33061 135.683 9.18974Z" fill="#00ABB5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M132.255 1.26465H126.369C128.399 3.2787 129.659 6.0846 129.659 9.19023C129.659 12.2959 128.399 15.1022 126.369 17.1163H132.255C133.587 14.7841 134.354 12.0789 134.354 9.19023C134.354 6.30202 133.587 3.59678 132.255 1.26465Z" fill="#00ABB5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M141.708 9.18974C141.708 11.9733 141.225 14.6418 140.342 17.1158H143.316C144.1 14.6154 144.524 11.9531 144.524 9.18974C144.524 6.42679 144.1 3.7645 143.316 1.26416H140.342C141.225 3.73811 141.708 6.40666 141.708 9.18974Z" fill="#00ABB5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M95.4967 1.26465V17.1163H92.0964V7.35557L87.903 13.7194H85.9308L81.7374 7.3332V17.1163H78.3372V1.26465H81.4875L86.9284 9.50786L92.3454 1.26465H95.4967Z" fill="#00ABB5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M109.801 1.26465V4.25396H102.026V7.62757H109.121V10.5945H102.026V14.1274H109.937V17.1163H98.6255V1.26465H109.801Z" fill="#00ABB5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M118.524 4.25348H115.985V14.1269H118.524C121.675 14.1269 123.375 12.4506 123.375 9.19019C123.375 5.92932 121.675 4.25348 118.524 4.25348ZM118.524 1.26416C124.168 1.26416 126.843 4.59303 126.843 9.19019C126.843 13.7873 124.168 17.1158 118.524 17.1158H112.585V1.26416H118.524Z" fill="#00ABB5"/>
</g>
<defs>
<clipPath id="clip0_37_89">
<rect width="155" height="17" fill="white" transform="translate(0 0.5)"/>
</clipPath>
</defs>
</svg>
}

View File

@ -0,0 +1,44 @@
.header__container{
background-color: white;
box-shadow: 0px 2px 10px rgba(32, 45, 61, 0.24);
display: flex;
justify-content: center;
align-items: center;
padding: 10px 0px;
}
.logo__container{
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.header__menu{
display: flex;
flex-direction: row;
gap: 20px;
}
.positioned{
width: 1100px;
}
.doctor__menu{
display: flex;
align-items: center;
gap: 10px;
}
.patients__content{
margin-top: 40px;
}
.patients__head{
display: flex;
justify-content: space-between;
}
.cross{
position: absolute;
right: 40px;
top: 20px;
transform: rotate(45deg);
}

View File

@ -0,0 +1,66 @@
import { Divider, Typography } from 'antd'
import react from 'react'
import './style.css'
interface IColoredRect {
level: number;
}
const ColoredRect: react.FC<IColoredRect> = (props) => {
const colors = [
'#56D96B',
'rgba(86, 217, 107, 0.8)',
'linear-gradient(0deg, rgba(245, 199, 33, 0.1), rgba(245, 199, 33, 0.1)), rgba(86, 217, 107, 0.6)',
'rgba(245, 199, 33, 0.6)',
'rgba(245, 199, 33, 0.8)',
'#F5C721',
'linear-gradient(0deg, rgba(249, 87, 33, 0.3), rgba(249, 87, 33, 0.3)), rgba(245, 199, 33, 0.8)',
'rgba(249, 87, 33, 0.8)',
'#F95721'
]
return <div className='rect' style={{background: colors[props.level]}}></div>
}
interface IHeatmapParam {
title: string;
levels: number[];
}
const HeatmapParam: react.FC<IHeatmapParam> = (props) => {
return <>
<div className="heatmap__param">
<div className="heatmap-param__heading">
<Typography.Text style={{fontSize: 16}}>{props.title}</Typography.Text>
</div>
{props.levels.map(e => <ColoredRect level={e} />)}
</div>
<Divider style={{margin: 0}}/>
</>
}
const HeatmapHeadings: react.FC = () => {
return <>
<Divider style={{margin: 0}}/>
<div className='heatmap__headings'>
<div className="heatmap-param__heading">
<Typography.Text style={{fontWeight: 'bold', fontSize: 16}}>Показатели</Typography.Text>
</div>
</div>
<Divider style={{margin: 0}}/>
</>
}
export const Heatmap: react.FC = () => {
return <div className="heatmap__container">
<HeatmapHeadings />
<HeatmapParam
title='Тошнота'
levels={[0, 2, 1, 5, 2, 8, 2]}
/>
<HeatmapParam
title='Уровень гемоглобина'
levels={[4, 0, 1, 7, 3, 8, 6]}
/>
</div>
}

View File

@ -0,0 +1,17 @@
.rect{
width: 50px;
height: 50px;
}
.heatmap__param{
display: flex;
align-items: center;
}
.heatmap-param__heading{
display: flex;
justify-content: center;
align-items: center;
width: 200px;
height: 50px;
}

View File

@ -0,0 +1,52 @@
import react from 'react';
import {Input, Modal} from 'antd'
import './style.css';
import { TitledInput } from '../../TitledInput';
import {Select} from 'antd';
import { RegularitySelect } from '../../RegularitySelect';
interface ICreateUpdateNotificationModal {
open: boolean;
setOpened: (open: boolean) => void;
}
export const CreateUpdateNotificationModal: react.FC<ICreateUpdateNotificationModal> = (props) => {
const optionTime = <Select mode='multiple'>
{
[...(Array(24).keys() as any)].map((e) => {
var text = e.toString()
if (e < 10) {
text = '0' + e.toString()
}
return <Select.Option key={text}>{text}:00</Select.Option>
})
}
</Select>
return <Modal
open={props.open}
onCancel={() => {props.setOpened(false)}}
title={'Добавить напоминание'}
okText="Добавить напоминание"
cancelText="Отмена"
>
<div className='create-not__container'>
<TitledInput
title='Напоминание'
placeholder='Введите напоминание'
inputComponent={<Input placeholder='Напоминание'></Input>}
></TitledInput>
<TitledInput
title='Регулярность'
placeholder='Введите напоминание'
inputComponent={<RegularitySelect />}
></TitledInput>
<TitledInput
title='Выберете время'
placeholder='Введите напоминание'
inputComponent={optionTime}
></TitledInput>
</div>
</Modal>
}

View File

@ -0,0 +1,5 @@
.create-not__container{
display: flex;
flex-direction: column;
gap: 10px;
}

View File

@ -0,0 +1,104 @@
import { Alert, DatePicker, Input, Modal } from 'antd';
import { MaskedInput } from 'antd-mask-input';
import react from 'react'
import { useAppDispatch } from '../../../store';
import { createUser } from '../../../store/reducers/patients/asyncThunks';
import { TitledInput } from '../../TitledInput';
import './style.css';
interface ICreateUpdatePatient {
open: boolean;
setOpened: (open: boolean) => void;
}
export const CreateUpdatePatient: react.FC<ICreateUpdatePatient> = (props) => {
const dispatch = useAppDispatch();
const [fam, setFam] = react.useState(' ');
const [name, setNam] = react.useState(' ');
const [midn, setMidn] = react.useState(' ');
const [dob, setDob] = react.useState(' ');
const [email, setEmail] = react.useState(' ');
return <Modal
open={props.open}
title="Добавить пациента"
okText="Добавить пациента"
cancelText="Отмена"
onCancel={() => {
props.setOpened(false)
}}
onOk={() => {
dispatch(
createUser({
fullname: fam + ' ' + name[0] + '.' + midn[0] + '.',
born: dob,
email: email
})
)
props.setOpened(false)
}}
destroyOnClose
>
<div className="patient__content-modal">
<TitledInput
title='Фамилия'
inputComponent={<Input value={fam} onChange={(e) => {
setFam(e.target.value);
}}></Input>}
/>
<TitledInput
title='Имя'
inputComponent={<Input
value={name}
onChange={(e) => {
setNam(e.target.value)
}}
></Input>}
/>
<TitledInput
title='Отчество'
inputComponent={<Input
value={midn}
onChange={(e) => {
setMidn(e.target.value);
}}
></Input>}
/>
<TitledInput
title='Дата рождения'
inputComponent={<DatePicker placeholder='Выберите дату'
onChange={(e) => {
setDob(e?.toDate().toISOString()!)
}}
></DatePicker>}
/>
<TitledInput
title='Email'
inputComponent={<Input
onChange={(e) => {
setEmail(e.target.value)
}}
></Input>}
/>
<TitledInput
title='Телефон'
inputComponent={<MaskedInput
mask={"+7(000)000-00-00"}
></MaskedInput>}
/>
<TitledInput
title='Телефон доверенного лица'
inputComponent={<MaskedInput
mask={"+7(000)000-00-00"}
></MaskedInput>}
/>
{/* <Alert
type='info'
message='Если поле “Телефон доверенного лица” остается пустым, то при экстренной ситуации будет вызвана машина скорой помощи'
showIcon
></Alert> */}
</div>
</Modal>
}

View File

@ -0,0 +1,5 @@
.patient__content-modal{
display: flex;
flex-direction: column;
gap: 10px;
}

View File

@ -0,0 +1,38 @@
import { Input, Modal } from 'antd'
import react from 'react'
import { TitledInput } from '../../TitledInput'
import './style.css'
interface ISosModal{
open: boolean;
onOpenChagen: (open: boolean) => void;
}
export const SosModal: react.FC<ISosModal> = (props) => {
return <Modal
title="SOS"
open={props.open}
onOk={() => {
props.onOpenChagen(false);
}}
onCancel={() => {
props.onOpenChagen(false);
}}
destroyOnClose
okText={"Вызвать скорую"}
cancelText="Закрыть"
>
<div className="sos__container">
<TitledInput
title='Телефон пациента'
inputComponent={<Input value={'+7(999)678-35-52'}/>
}
/>
<TitledInput
title='Телефон доверительного лица'
inputComponent={<Input value={'+7(999)987-78-89'}/>
}
/>
</div>
</Modal>
}

View File

@ -0,0 +1,5 @@
.sos__container{
display: flex;
flex-direction: column;
gap: 10px;
}

View File

@ -0,0 +1,12 @@
import { Checkbox, Typography } from 'antd'
import react from 'react'
import './style.css'
export const MyNotification: react.FC = () => {
return <div className='notification__container'>
<Typography.Text>Заполнить анкету Название анкеты</Typography.Text>
<Typography.Text>08.12.2022, 10:00</Typography.Text>
<Typography.Text>Севастьянов А.А.</Typography.Text>
<Checkbox>Выполнено</Checkbox>
</div>
}

View File

@ -0,0 +1,9 @@
.notification__container{
box-shadow: 0px 0px 25px rgba(0, 0, 0, 0.06);
border-radius: 6px;
padding: 10px;
display: flex;
justify-content: space-between;
padding-left: 80px;
padding-right: 30px;
}

View File

@ -0,0 +1,30 @@
import { Button, Tag, Typography } from 'antd'
import react from 'react'
import './style.css'
interface IQuestionPreivew {
onQuestionClick: () => void;
onHeatMapClick: () => void;
}
export const QuestionPreivew: react.FC<IQuestionPreivew> = (props) => {
return <div className='question-preview__container'>
<Typography.Text style={{fontWeight: 'bold'}}>Название анкеты</Typography.Text>
<span>
<Typography.Text type='secondary'>Регулярность: </Typography.Text>
<Typography.Text>2 раза в день </Typography.Text>
</span>
<span>
<Typography.Text type='secondary'>Последнее прохождение: </Typography.Text>
<Typography.Text>07.12.2022</Typography.Text>
</span>
<span>
<Typography.Text type='secondary'>Последний результат: </Typography.Text>
<Tag color='green'>В норме</Tag>
</span>
<div className="buttons">
<Button onClick={() => {props.onHeatMapClick()}}>Тепловая карта</Button>
<Button type='primary' onClick={() => {props.onQuestionClick()}}>Заполнить анкету</Button>
</div>
</div>
}

View File

@ -0,0 +1,16 @@
.question-preview__container{
display: flex;
flex-direction: column;
gap: 5px;
width: 350px;
padding: 20px;
border-radius: 6px;
box-shadow: 0px 0px 25px rgba(0, 0, 0, 0.06);
}
.buttons{
display: flex;
flex-direction: row;
gap: 10px;
margin-top: 20px;
}

View File

@ -0,0 +1,33 @@
import { Divider, Input } from 'antd'
import react from 'react'
import { TitledInput } from '../../TitledInput'
import './style.css'
interface IInputNumber {
refMin: number,
refMax: number,
onChange: (refMin: number, refMax: number) => void
}
export const InputNumberQuestion: react.FC<IInputNumber> = (props) => {
const [data, setData] = react.useState(props)
return <TitledInput
title='Укажите референсные значения'
inputComponent={
<div className='ref-input__container'>
<Input type='number' onChange={(e) => {
setData({...data, refMin: parseInt(e.target.value)})
props.onChange(parseInt(e.target.value), data.refMax);
}} value={props.refMin}/> - <Input
type='number'
onChange={(e) => {
setData({...data, refMax: parseInt(e.target.value)})
props.onChange(data.refMin, parseInt(e.target.value));
}}
value={props.refMax}
/>
</div>
}
/>
}

View File

@ -0,0 +1,5 @@
.ref-input__container{
display: flex;
gap: 5px;
align-items: center;
}

View File

@ -0,0 +1,56 @@
import { Divider, Input, Select } from 'antd'
import react from 'react'
import { TitledInput } from '../../TitledInput'
import { InputNumberQuestion } from '../InputNumberQuestion'
import { TenChoicesQuestion } from '../TenChoicesQuestion'
import questions, { getActiveQuestions, IQuestion, setQuestion } from '../../../store/reducers/questions'
import { useSelector } from 'react-redux/es/hooks/useSelector'
import { useDispatch } from 'react-redux/es/exports'
import { useAppDispatch } from '../../../store'
export const Question: react.FC<{question: IQuestion, index: number}> = (props) => {
const dispatch = useAppDispatch();
return <>
<TitledInput
title={'Вопрос ' + props.index}
inputComponent={
<Input onChange={() => {}}/>
}
></TitledInput>
<TitledInput
title='Тип ответа'
inputComponent={
<Select value={props.question.type} onChange={(e) => {
dispatch(setQuestion(
{
index: props.index,
question: {...props.question, type: e}
}
))
}}>
<Select.Option key={"ten_choices"}>Шкала от 1 до 10</Select.Option>
<Select.Option key={"input"}>Ввод числа</Select.Option>
</Select>
}
></TitledInput>
{
props.question.type == 'input' ? <InputNumberQuestion
refMax={props.question.refMax!}
refMin={props.question.refMin!}
onChange={(refMin, refMax) => {
dispatch(setQuestion({index: props.index, question: {...props.question, refMin: refMin, refMax: refMax}}))
}}
></InputNumberQuestion> : <TenChoicesQuestion
names={props.question.names!}
onChange={(names) => {
dispatch(setQuestion({
index: props.index, question: {...props.question, names: names}
}))
}}
/>
}
<Divider></Divider>
</>
}

View File

@ -0,0 +1,53 @@
import { Input, Select, Typography } from 'antd'
import react from 'react'
import { useDispatch, useSelector } from 'react-redux/es/exports'
import { getActiveIndex, getActiveQuestion, questions, setDescriptionActiveQuestions, setIllnessesActiveQuestions, setNameActiveQuestions, setRegularityActiveQuestions } from '../../../store/reducers/questions'
import { RegularitySelect } from '../../RegularitySelect'
import { TitledInput } from '../../TitledInput'
import './style.css'
export const QuestionTitle: react.FC = () => {
const dispatch = useDispatch();
const activeQuestion = useSelector(getActiveQuestion);
return <>
<Typography.Text>Введите общую информацию</Typography.Text>
<div className='title__container title__spaced'>
<TitledInput
title='Название'
inputComponent={
<Input placeholder='Введите название' onChange={(e) => {
dispatch(setNameActiveQuestions(e.target.value))
}} value={activeQuestion.title}/>
}
/>
<TitledInput
title='Описание'
inputComponent={
<Input.TextArea rows={4} placeholder='Введите описание' onChange={(e) => {
dispatch(setDescriptionActiveQuestions(e.target.value))
}}
value={activeQuestion.description}
/>
}
/>
<TitledInput
title='Регулярность'
inputComponent={
<RegularitySelect onChange={
(e) => dispatch(setRegularityActiveQuestions(e))
}
value={activeQuestion.regularity}
/>
}
/>
<TitledInput
title='Нозология'
inputComponent={
<Select mode='tags' value={activeQuestion.illnesses} onChange={(e) => {
dispatch(setIllnessesActiveQuestions(e));
}}/>
}
/>
</div>
</>
}

View File

@ -0,0 +1,11 @@
.title__container{
box-shadow: 0px 0px 25px rgba(0, 0, 0, 0.06);
padding: 20px;
border-radius: 10px;
}
.title__spaced{
display: flex;
flex-direction: column;
gap: 10px;
}

View File

@ -0,0 +1,36 @@
import { Checkbox, Input, Typography } from 'antd'
import react from 'react'
import './style.css'
interface ITenChoices {
names: string[],
onChange: (names: string[]) => void;
}
export const TenChoicesQuestion: react.FC<ITenChoices> = (props) => {
const [checked, setChecked] = react.useState(false);
const [data, setData] = react.useState(props.names)
return <div className="ten-choices__container">
<Checkbox onChange={(e) => {
setChecked(e.target.checked);
console.log(e.target.checked)
}}>Подписать значения шкалы</Checkbox>
{
checked ? ([...(new Array(10)).keys() as any]).map((e: number) => {
return <div className='choices__question-names'>
<Typography.Text style={{width: 20}}>{e+1}</Typography.Text>
<Input
placeholder='Введите значение (необязательно)'
value={props.names[e]}
onChange={(ee) => {
var names = data.map((eee, index) => index == e ? ee.target.value : eee );
setData(names);
props.onChange(names)
}}
/>
</div>
}) : <></>
}
</div>
}

View File

@ -0,0 +1,10 @@
.choices__question-names{
display: flex;
align-items: center;
gap: 5px;
}
.ten-choices__container{
display: flex;
flex-direction: column;
gap: 10px;
}

View File

@ -0,0 +1,20 @@
import { Select } from 'antd'
import react from 'react'
interface IRegularitySelect {
onChange?: (key: string) => void;
value?: string;
}
export const RegularitySelect: react.FC<IRegularitySelect> = (props) => {
return <Select onChange={(e) => {
return props.onChange ? props.onChange(e) : ''
}} value={props.value ? props.value : null}>
<Select.Option key={'1'}>1 раз в день</Select.Option>
<Select.Option key={'2'}>2 раза в день</Select.Option>
<Select.Option key={'3'}>3 раза в день</Select.Option>
<Select.Option key={'5'}>5 раз в день</Select.Option>
<Select.Option key={'10'}>10 раз в день</Select.Option>
</Select>
}

View File

@ -0,0 +1,24 @@
import react from 'react'
import {Tag, Button} from 'antd';
interface ISosComponent {
text: string;
id: string;
onSosClick?: () => void
}
export const SosTableComponent: react.FC<ISosComponent> = (props) => {
var color = 'green';
if (props.text == 'На границе') color = 'yellow';
if (props.text == 'Вне нормы') color = 'red';
return <div className='patients__tag'>
<Tag color={color} style={{height: 30, display: 'flex', alignItems: 'center'}}>{props.text}</Tag>
{
color == 'red' ? <Button danger type='primary' shape='round'
onClick={() => {
return props.onSosClick ? props.onSosClick() : null
}}
>SOS</Button> : <></>
}
</div>
}

View File

@ -0,0 +1,62 @@
import react from 'react';
import { Table, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { SosTableComponent } from '../../SosTableComponent';
interface DiaryCell {
name: string;
regularity: string;
lastVisit: string;
result: string;
key: string;
}
export const TheDiaryTable: react.FC = () => {
const columns: ColumnsType<DiaryCell> = [
{
dataIndex: 'name',
key: 'name',
title: 'Название анкеты',
},
{
dataIndex: 'regularity',
key: 'regularity',
title: 'Регулярность'
},
{
dataIndex: 'lastVisit',
key: 'lastVisit',
title: 'Регулярность'
},
{
dataIndex: 'result',
key: 'result',
title: 'Результаты последней анкеты',
render: (e: string, record: any, id: any) => {
return <div className='diary__results'>
<SosTableComponent text={e} id={record.key}></SosTableComponent>
<Typography.Link
style={{color: '#00ABB5', textDecoration: 'underline'}}
href='1/heatmap'
>Подробнее</Typography.Link>
</div>
}
}
]
const data: DiaryCell[] = [
{
name: 'Биохимический анализ крови',
regularity: '1 раз в месяц',
lastVisit: '07.12.2022, 17:30',
result: 'В норме',
key: '1'
}
]
return <Table style={{marginTop: 30}} columns={columns} dataSource={data}></Table>
}

View File

@ -0,0 +1,62 @@
import { Button, Divider, Table } from 'antd'
import react from 'react'
import type { ColumnsType } from 'antd/es/table';
import { DottedTemplate } from '../../DottedTemplate';
import { CreateUpdateNotificationModal } from '../../Modals/CreateUpdateNotificationModal';
interface INotifiaction {
name: string;
regularity: string;
time: string;
key: string;
}
export const TheNotificationsTable: react.FC = (props) => {
const [opened, setOpened] = react.useState(false);
const columns: ColumnsType<INotifiaction> = [
{
key: 'name',
title: 'Напоминание',
dataIndex: 'name'
},
{
key: 'regularity',
title: 'Регулярность',
dataIndex: 'regularity'
},
{
key: 'time',
title: 'Время',
dataIndex: 'time',
render: (el: string, data: any, id: any) => {
const content = <div className='notif__actions'>
<Button type='text' onClick={() => {setOpened(true)}}>Редактировать</Button>
<Button type='text' danger>Удалить</Button>
</div>
return <div className='notif__time'>
<span>{el}</span>
<DottedTemplate content={content}></DottedTemplate>
</div>
}
},
];
const data: INotifiaction[] = [
{
key: '1',
regularity: '1 раз в день',
time: '10:00',
name: 'Измерить давление'
}
]
return <>
<Table
style={{marginTop: 30}}
columns={columns}
dataSource={data}
></Table>
<CreateUpdateNotificationModal open={opened} setOpened={setOpened}></CreateUpdateNotificationModal>
</>
}

View File

@ -0,0 +1,118 @@
import react from 'react'
import { Button, Divider, Popover, Table, Tag } from 'antd'
import {EllipsisOutlined} from '@ant-design/icons'
import './style.css';
import { useLocation, useNavigate } from 'react-router-dom';
import { SosTableComponent } from '../../SosTableComponent';
import { DottedTemplate } from '../../DottedTemplate';
import { useAppDispatch, useAppSelector } from '../../../store';
import { usersGet } from '../../../store/reducers/patients';
import { getUsers } from '../../../store/reducers/patients/asyncThunks';
import { SosModal } from '../../Modals/SosModal';
export const ThePatientsTable: react.FC = () => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const [sosOpened, setSosOpened] = react.useState(false);
const users = useAppSelector(usersGet);
if (!users.length) {
dispatch(
getUsers()
)
}
console.log(users);
const columns = [
{
title: 'ФИО',
dataIndex: 'fio',
key: 'fio'
},
{
title: 'Пол',
dataIndex: 'male',
key: 'male'
},
{
title: 'Возраст',
dataIndex: 'age',
key: 'age'
},
{
title: 'Результаты последней анкеты',
dataIndex: 'results',
key: 'results',
render: (e: string, record: any, index: any) => {
console.log(e, record, index)
var color = 'green';
if (e == 'На границе') color = 'yellow';
if (e == 'Вне нормы') color = 'red';
const patientContent = <div className='patients__click'>
<Button type='text'>Перейти в чат</Button>
<Button type='text' onClick={() => {
navigate(`/patients/${record.key}/diary`)
}}>Дневник пациента</Button>
<Button type='text'>Отправить анекту</Button>
<Button type='text'>Редактировать</Button>
<Button type='text'>Удалить</Button>
<Divider style={{margin: 0}}></Divider>
<Button type='text' style={{color: '#E30611'}} onClick={() => {
setSosOpened(true);
}}>SOS</Button>
</div>
return <div className='patients__cont'>
<SosTableComponent
text={e}
id={record.key}
onSosClick={() => {
setSosOpened(true)
}}
></SosTableComponent>
<DottedTemplate content={patientContent}></DottedTemplate>
</div>
}
}
]
const data = [
{
fio: 'Васильев Василий Васильевич',
male: 'Мужской',
age: '20',
results: 'На границе',
key: '1'
},
{
fio: 'Васильев Василий Васильевич',
male: 'Мужской',
age: '30',
results: 'Вне нормы',
key: '2'
}
]
return <>
<Table columns={columns} dataSource={users.map(e => {
{
return {
fio: e.fio,
male: e.male,
age: e.dob,
results: e.results == 'ok' ? 'В норме' : 'Вне нормы',
key: e.key
}
}
})} ></Table>
<SosModal
open={sosOpened}
onOpenChagen={setSosOpened}
/>
</>
}

View File

@ -0,0 +1,17 @@
.patients__cont{
display: flex;
justify-content: space-between;
align-items: center;
}
.patients__click{
display: flex;
flex-direction: column;
justify-content: start;
align-items: start;
}
.patients__tag{
display: flex;
flex-direction: row;
gap: 5px;
align-items: center;
}

View File

@ -0,0 +1,58 @@
import { Button, Divider, Table, Tag } from 'antd'
import react from 'react'
import type { ColumnsType } from 'antd/es/table';
import { DottedTemplate } from '../../DottedTemplate';
import './style.css';
interface QuestionCell {
name: string;
regularity: string;
illnesses: string;
}
export const TheQuestionsTable: react.FC = () => {
const columns: ColumnsType<QuestionCell> = [
{
title: 'Название анкеты',
key: 'name',
dataIndex: 'name'
},
{
title: 'Регулярность',
key: 'regularity',
dataIndex: 'regularity'
},
{
title: 'Нозология',
key: 'illnesses',
dataIndex: 'illnesses',
render: (value: string, record: QuestionCell, index: number) => {
const content = <div className='illness__modal'>
<Button type='text'>Отправить пациенту</Button>
<Button type='text'>Редактировать</Button>
<Button type='text'>Сделать копию на основе</Button>
<Button type='text' danger>Удалить</Button>
</div>
return <div className='illness__container'>
<div className="illness__tags">
{value.split(' ').map((e) => <Tag>{e}</Tag>)}
</div>
<DottedTemplate content={content}></DottedTemplate>
</div>
}
}
]
const data: QuestionCell[] = [
{
name: "Состояние при мигрени",
regularity: '1 раз в день',
illnesses: олезнь1 Болезнь2"
}
]
return <Table columns={columns} dataSource={data}></Table>
}

View File

@ -0,0 +1,13 @@
.illness__container{
display: flex;
justify-content: space-between;
align-items: center;
}
.illness__modal{
display: flex;
flex-direction: column;
gap: 5px;
justify-content: start;
align-items: start;
}

View File

@ -0,0 +1,16 @@
import { Input, Typography } from 'antd';
import react from 'react';
import './style.css'
interface ITitledInput {
title: string;
placeholder?: string;
inputComponent: react.ReactNode
}
export const TitledInput: react.FC<ITitledInput> = (props) => {
return <div className='titled-input__container'>
<Typography.Text type='secondary'>{props.title}</Typography.Text>
{props.inputComponent}
</div>
}

View File

@ -0,0 +1,5 @@
.titled-input__container{
display: flex;
flex-direction: column;
gap: 0px;
}

View File

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -1,19 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import router from './router'
import {RouterProvider} from 'react-router-dom';
import 'antd/dist/reset.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App/>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

3
src/network/index.ts Normal file
View File

@ -0,0 +1,3 @@
export const origin = 'https://dev2.akarpov.ru/'
export const doctorToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyQGV4YW1wbGUuY29tIiwiZXhwIjoxNjcxNDUwNDUwfQ.-z1zP0SxBA-BYdV-YrfjO27C_n6tVdkbiF35IYoWLAE'
export const patientToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyNkBleGFtcGxlLmNvbSIsImV4cCI6MTY3MTQzNDU1N30.-ikjYmc2KCMr-ECzn7qO5tYWf77Uw6EmF9ZNWbmEG10'

View File

@ -0,0 +1,38 @@
import { Button, Typography } from 'antd'
import react from 'react'
import { useNavigate } from 'react-router-dom'
import { ArticlePreview } from '../../components/ArticlePreview'
import { Header } from '../../components/Header'
import './style.css'
export const Articles: react.FC = () => {
const navigate = useNavigate();
return <>
<Header></Header>
<div className="centered">
<div className="sized_">
<div className="article__heading">
<Typography.Title level={3}>Рекомендации</Typography.Title>
<Button type='primary' onClick={() => {
navigate('/articles/create')
}}>Написать рекомендацию</Button>
</div>
<div className="articles__list">
<ArticlePreview
title="Частые головные боли. Что делать, если боль становится невыносимой?"
doctorName='Севастьянов А. А.'
tags={['Мигрень', 'Опухоль']}
time='10.11.2022, 07:15'
/>
<ArticlePreview
title="Частые головные боли. Что делать, если боль становится невыносимой?"
doctorName='Севастьянов А. А.'
tags={['Гастрит']}
time='10.11.2022, 07:15'
/>
</div>
</div>
</div>
</>
}

View File

@ -0,0 +1,5 @@
.article__heading{
display: flex;
justify-content: space-between;
align-items: center;
}

View File

@ -0,0 +1,36 @@
import { Button, Input, Typography } from 'antd'
import react from 'react'
import { useNavigate } from 'react-router-dom'
import { Header } from '../../components/Header'
import { TitledInput } from '../../components/TitledInput'
import './style.css'
export const CreateArticle: react.FC = () => {
const navigate = useNavigate();
return <>
<Header></Header>
<div className="centered">
<div className="sized_ ">
<div className="article-create__container">
<Typography.Title level={3}>Опубликовать рекомендацию</Typography.Title>
<TitledInput
title='Введите заголовок'
inputComponent={<Input placeholder='Место для Вашего заголовка'></Input>}
/>
<TitledInput
title='Введите описание'
inputComponent={<Input.TextArea placeholder='Место для Вашего описания' rows={15}></Input.TextArea>}
/>
<Button
type='primary'
style={{width: 200}}
onClick={() => {
navigate(-1)
}}
>Опубликовать запись</Button>
</div>
</div>
</div>
</>
}

View File

@ -0,0 +1,6 @@
.article-create__container{
display: flex;
flex-direction: column;
gap: 30px;
width: 500px;
}

View File

@ -0,0 +1,32 @@
import { Button, Typography } from 'antd'
import react from 'react'
import { CreateQuestionHeader } from '../../components/Header/CreateHeader'
import { Question } from '../../components/Questions/Question'
import { QuestionTitle } from '../../components/Questions/QuestionTitle/QuestionTitle'
import {useSelector} from 'react-redux';
import './style.css'
import { addNewQuestion, getActiveQuestions } from '../../store/reducers/questions'
import { useDispatch } from 'react-redux/es/exports'
export const CreateUpdateQuestion: react.FC = () => {
const questions = useSelector(getActiveQuestions);
const dispatch = useDispatch();
return <>
<CreateQuestionHeader></CreateQuestionHeader>
<div className="centered">
<div className="sized__ question__container">
<QuestionTitle />
<Typography.Text>Сформируйте вопросы для анкеты</Typography.Text>
<div className='title__container title__spaced quest__container'>
{questions.map((e, index) => {
return <Question question={e} index={index}></Question>
})}
<Button onClick={() => {
dispatch(addNewQuestion())
}}>Добавить вопрос</Button>
</div>
</div>
</div>
</>
}

View File

@ -0,0 +1,13 @@
.sized__{
width: 680px;
}
.question__container{
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 40px;
overflow-y: scroll;
max-height: 80vh;
padding: 20px;
}

View File

@ -0,0 +1,52 @@
import { Typography } from 'antd'
import react from 'react'
import { useNavigate } from 'react-router-dom'
import { DoctorAdd } from '../../components/DoctorAdd'
import { DoctorPreview } from '../../components/DoctorPreview'
import { Header } from '../../components/Header'
import { MyNotification } from '../../components/MyNotification'
import { QuestionPreivew } from '../../components/QuerstionsPreview'
import './style.css'
export const MyDiary: react.FC = () => {
const navigate = useNavigate();
return <>
<Header></Header>
<div className="centered">
<div className="sized_ my-diary__container">
<div className="my-diary__header">
<Typography.Title level={3}>
Дневник пациента
</Typography.Title>
</div>
<div className="doctors">
<Typography.Text style={{fontWeight: 'bold'}}>Мой лечащий врач</Typography.Text>
<div className="doctor__blocks">
<DoctorPreview />
<DoctorPreview />
<DoctorAdd />
</div>
</div>
<div className="my-questions">
<Typography.Text style={{fontWeight: 'bold'}}>Мои анкеты</Typography.Text>
<div className="doctor__blocks">
<QuestionPreivew
onHeatMapClick={() => {
navigate('/index/questions/1/heatmap')
}}
onQuestionClick={() => {
navigate('/index/questions/1/assign')
}}
/>
</div>
</div>
<div className="my-notifications">
<Typography.Text style={{fontWeight: 'bold'}}>Мои Напоминания</Typography.Text>
<div className="not__blocks">
<MyNotification />
</div>
</div>
</div>
</div>
</>
}

View File

@ -0,0 +1,16 @@
.doctor__blocks{
margin-top: 20px;
display: flex;
flex-direction: row;
gap: 20px;
}
.my-diary__container{
display: flex;
flex-direction: column;
gap: 30px;
}
.not__blocks{
margin-top: 30px;
}

View File

@ -0,0 +1,38 @@
import react from 'react';
import { Header } from '../../components/Header';
import { Breadcrumb, Typography, Button } from 'antd';
import './style.css';
import { TheNotificationsTable } from '../../components/Tables/TheNotificationsTable';
import { CreateUpdateNotificationModal } from '../../components/Modals/CreateUpdateNotificationModal';
export const Notifications: react.FC = () => {
const [open, setOpened] = react.useState(false);
return <>
<Header></Header>
<div className="centered">
<div className="sized_">
<Breadcrumb>
<Breadcrumb.Item>
Пациенты
</Breadcrumb.Item>
<Breadcrumb.Item>
Дневник пациента Иван Иванович
</Breadcrumb.Item>
<Breadcrumb.Item>
Напоминания
</Breadcrumb.Item>
</Breadcrumb>
<div className="notification__heading">
<Typography.Title level={3}>Напоминания</Typography.Title>
<Button type='primary' onClick={() => {
setOpened(true);
}}>Добавить напоминание</Button>
</div>
<TheNotificationsTable></TheNotificationsTable>
<CreateUpdateNotificationModal open={open} setOpened={setOpened}></CreateUpdateNotificationModal>
</div>
</div>
</>
}

View File

@ -0,0 +1,17 @@
.notification__heading{
display: flex;
flex-direction: row;
justify-content: space-between;
}
.notif__time{
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.notif__actions{
display: flex;
flex-direction: column;
gap: 5px;
align-items: start;
}

View File

@ -0,0 +1,31 @@
import react from 'react'
import { Header } from '../../components/Header'
import {Breadcrumb, Typography, Button} from 'antd';
import './style.css';
import { TheDiaryTable } from '../../components/Tables/TheDiaryTable';
import { useNavigate, useParams } from 'react-router-dom';
export const PatientDiary: react.FC = () => {
const navigate = useNavigate();
const {id} = useParams<{id: string}>();
return <>
<Header></Header>
<div className='centered'>
<div className="sized_">
<Breadcrumb>
<Breadcrumb.Item>Пациенты</Breadcrumb.Item>
<Breadcrumb.Item>Дневник пациента Василий Васильевич</Breadcrumb.Item>
</Breadcrumb>
<div className="diary__headings">
<Typography.Title level={3}>Дневник пациента Василий Васильевич</Typography.Title>
<Button type='primary' onClick={() => {
navigate(`/patients/${id}/notifications`);
}}>Напоминания</Button>
</div>
<TheDiaryTable></TheDiaryTable>
</div>
</div>
</>
}

View File

@ -0,0 +1,19 @@
.centered{
display: flex;
justify-content: center;
align-items: center;
}
.sized_{
width: 1100px;
margin-top: 40px;
}
.diary__headings{
display: flex;
justify-content: space-between;
}
.diary__results{
display: flex;
justify-content: space-between;
}

View File

@ -0,0 +1,31 @@
import react from 'react';
import { Header } from '../../components/Header';
import {Typography, Button} from 'antd';
import { ThePatientsTable } from '../../components/Tables/ThePatientsTable';
import { CreateUpdatePatient } from '../../components/Modals/CreateUpdatePatient';
import './styles.css'
export const Patients: react.FC = () => {
const [opened, setOpened] = react.useState(false);
return <>
<CreateUpdatePatient
open={opened}
setOpened={setOpened}
></CreateUpdatePatient>
<Header></Header>
<div className="sized">
<div className="positioned">
<div className="patients__content">
<div className="patients__head">
<Typography.Title level={3}>Пациенты</Typography.Title>
<Button type="primary" onClick={() => {
setOpened(true)
}}>Добавить пациента</Button>
</div>
<ThePatientsTable></ThePatientsTable>
</div>
</div>
</div>
</>
}

View File

@ -0,0 +1,8 @@
.sized{
display: flex;
justify-content: center;
align-items: center;
}
.positioned{
width: 1100px;
}

View File

@ -0,0 +1,32 @@
import { Button, Table, Typography } from 'antd'
import react from 'react'
import { useDispatch } from 'react-redux/es/exports'
import { useSelector } from 'react-redux/es/hooks/useSelector'
import { useNavigate } from 'react-router-dom'
import { Header } from '../../components/Header'
import { TheQuestionsTable } from '../../components/Tables/TheQuestionsTable'
import { createNewQuestions, questions } from '../../store/reducers/questions'
import './style.css'
export const Questions: react.FC = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const getQuestions = useSelector(questions);
return <>
<Header></Header>
<div className="centered">
<div className="sized_">
<div className="questions__header">
<Typography.Title level={3}>Анкеты</Typography.Title>
<Button type='primary' onClick={() => {
navigate('/questions/create');
dispatch(createNewQuestions())
console.log(getQuestions)
}}>Создать анкету</Button>
</div>
<TheQuestionsTable></TheQuestionsTable>
</div>
</div>
</>
}

View File

@ -0,0 +1,4 @@
.questions__header{
display: flex;
justify-content: space-between;
}

View File

@ -0,0 +1,28 @@
import { Button } from 'antd'
import react from 'react'
import { useNavigate } from 'react-router-dom'
import { AssignQuestion } from '../../components/AssignQuestions/Question'
import { AssignHeader } from '../../components/Header/AssignHeader'
import './style.css'
export const QuestionsAssign: react.FC = () => {
const navigate = useNavigate();
return <>
<AssignHeader></AssignHeader>
<div className="centered">
<div className="sized__ assign-questions__container">
<AssignQuestion type='Colors' title='Тошнота'/>
<AssignQuestion type='Input' title='Уровень гемоглобина'/>
<Button
style={{width: 200}}
type='primary'
onClick={() => {
navigate('/index/diary')
}}
>Отправить</Button>
</div>
</div>
</>
}

View File

@ -0,0 +1,7 @@
.assign-questions__container{
margin-top: 30px;
display: flex;
flex-direction: column;
gap: 20px;
justify-content: flex-end;
}

View File

@ -0,0 +1,26 @@
import { Breadcrumb, Typography } from 'antd'
import react from 'react'
import { Header } from '../../components/Header'
import { Heatmap } from '../../components/Heatmap'
import './style.css'
export const QuestionHeatmap: react.FC = () => {
return <>
<Header />
<div className="centered">
<div className="sized_ q_heatmap__container">
<Breadcrumb>
<Breadcrumb.Item>
Дневник пациента
</Breadcrumb.Item>
<Breadcrumb.Item>
Название анкеты
</Breadcrumb.Item>
</Breadcrumb>
<Typography.Title level={3}>Название анкеты</Typography.Title>
<Heatmap />
</div>
</div>
</>
}

View File

@ -0,0 +1,5 @@
.q_heatmap__container{
display: flex;
flex-direction: column;
gap: 10px;
}

View File

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@ -1,15 +0,0 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

60
src/router.tsx Normal file
View File

@ -0,0 +1,60 @@
import {createBrowserRouter} from 'react-router-dom';
import { Articles } from './pages/Articles';
import { CreateArticle } from './pages/CreateArticle';
import { CreateUpdateQuestion } from './pages/CreateUpdateQuestion';
import { MyDiary } from './pages/MyDiary';
import { Notifications } from './pages/Notifications';
import { PatientDiary } from './pages/PatientDiary';
import { Patients } from './pages/Patients';
import { Questions } from './pages/Questions';
import { QuestionsAssign } from './pages/QuestionsAssign';
import { QuestionHeatmap } from './pages/QuestionsHeatmap';
const router = createBrowserRouter([
{
path: '/patients',
element: <Patients></Patients>
},
{
path: '/patients/:id/diary',
element: <PatientDiary></PatientDiary>
},
{
path: '/patients/:id/notifications',
element: <Notifications></Notifications>
},
{
path: '/questions',
element: <Questions></Questions>
},
{
path: '/questions/create',
element: <CreateUpdateQuestion></CreateUpdateQuestion>
},
{
path: '/articles',
element: <Articles></Articles>
},
{
path: '/articles/create',
element: <CreateArticle></CreateArticle>
},
{
path: '/index/diary',
element: <MyDiary></MyDiary>
},
{
path: '/index/questions/:id/assign',
element: <QuestionsAssign></QuestionsAssign>
},
{
path: '/index/questions/:id/heatmap',
element: <QuestionHeatmap />
},
{
path: '/patients/:id/:question_id/heatmap',
element: <QuestionHeatmap />
}
])
export default router;

View File

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

19
src/store/index.ts Normal file
View File

@ -0,0 +1,19 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import questionReducer from './reducers/questions';
import patientsReducer from './reducers/patients';
const baseReducer = combineReducers({questionReducer, patientsReducer})
const store = configureStore({reducer: baseReducer})
export default store;
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

View File

@ -0,0 +1,65 @@
import {createAsyncThunk} from '@reduxjs/toolkit';
import axios from 'axios';
import { IPatient } from '.';
import { doctorToken, origin } from '../../../network';
export const getUsers = createAsyncThunk(
'patient/getPatients',
async (thunkApi) => {
const response = await axios.get(
origin + 'api/users/list',
{
headers: {
Authorization: 'Bearer ' + doctorToken
}
}
)
console.log(response)
const data = response.data;
return data.map((e: any) => {
return {
fio: e.fio,
male: e.gender,
results: e.latest_form_result,
dob: e.age,
key: e.key,
} as IPatient;
})
}
)
interface IPatientCreate {
fullname: string;
born: string;
email: string;
}
export const createUser = createAsyncThunk(
'patient/createPatient',
async (patientData: IPatientCreate, thunkApi) => {
console.log({
...patientData,
password: "1234",
gender: "Мужской"
})
const response = await axios.post(origin + 'api/auth/signup', {
...patientData,
password: "1234",
gender: "Мужской"
}, {
headers: {
Authorization: 'Bearer ' + doctorToken,
'Content-Type': 'application/json'
}
});
const data = response.data;
return {
fio: data.fullname,
male: "Мужской",
dob: patientData.born,
results: "ok"
} as IPatient;
}
)

View File

@ -0,0 +1,38 @@
import {createSlice, createSelector} from '@reduxjs/toolkit';
import { RootState } from '../..';
import { createUser, getUsers } from './asyncThunks';
export interface IPatient {
fio: string;
male: string;
dob: string;
results: string;
key: string;
}
interface IPatients {
patients: IPatient[]
}
const initialState: IPatients = {
patients: []
}
const patientSlice = createSlice({
initialState: initialState,
name: 'patientSlice',
reducers: {
},
extraReducers: (builder) => {
builder.addCase(getUsers.fulfilled, (state, action) => {
state.patients = action.payload;
});
builder.addCase(createUser.fulfilled, (state, action) => {
state.patients = state.patients.concat([action.payload])
})
}
})
export default patientSlice.reducer
export const usersGet = createSelector((store: RootState) => store.patientsReducer.patients, (a) => a)

View File

@ -0,0 +1,112 @@
import {createSlice, createSelector, PayloadAction} from '@reduxjs/toolkit'
import { RootState } from '../..';
export interface IQuestion {
type: "ten_choices" | "input",
title: string;
names?: string[],
refMin?: number,
refMax?: number,
}
interface Questions {
questions: IQuestion[],
title: string,
description: string,
regularity: string,
illnesses: string[]
}
interface QuestionTable {
questions: Questions[],
active_questions_index: number
}
const initialState: QuestionTable = {
questions: [],
active_questions_index: -1
}
const questionSlice = createSlice({
initialState: initialState,
name: "questions",
reducers: {
createNewQuestions(state) {
state.questions = state.questions.concat([{
questions: [],
title: "",
description: "",
regularity: "",
illnesses: [""]
}]);
state.active_questions_index = state.questions.length - 1
},
setNameActiveQuestions(state, payload: PayloadAction<string>) {
state.questions = state.questions.map((e, index) => {
return index == state.active_questions_index ?
{
...e,
title: payload.payload,
} : e
})
},
setDescriptionActiveQuestions(state, payload: PayloadAction<string>) {
state.questions = state.questions.map((e, index) => {
return index == state.active_questions_index ?
{
...e,
description: payload.payload,
} : e
})
},
setRegularityActiveQuestions(state, payload: PayloadAction<string>) {
state.questions = state.questions.map((e, index) => {
return index == state.active_questions_index ?
{
...e,
regularity: payload.payload,
} : e
})
},
setIllnessesActiveQuestions(state, payload: PayloadAction<string[]>) {
state.questions = state.questions.map((e, index) => {
return index == state.active_questions_index ?
{
...e,
illnesses: payload.payload,
} : e
})
},
addNewQuestion(state) {
state.questions[state.active_questions_index].questions = state.questions[state.active_questions_index].questions.concat([{
title: "",
type: "input",
refMax: -100,
refMin: 100,
names: new Array(10).fill(' ')
}])
},
setQuestion(state, payload: PayloadAction<{index: number, question: IQuestion}>) {
state.questions[state.active_questions_index].questions = state.questions[state.active_questions_index].questions.map((e, index) => {
return index == payload.payload.index ? payload.payload.question : e
})
}
}
})
export default questionSlice.reducer
export const {
createNewQuestions,
setDescriptionActiveQuestions,
setNameActiveQuestions,
setRegularityActiveQuestions,
setIllnessesActiveQuestions,
setQuestion,
addNewQuestion
} = questionSlice.actions;
export const getActiveQuestion = createSelector((store: RootState) => store.questionReducer.questions[store.questionReducer.active_questions_index], (e) => e)
export const questions = createSelector((store: RootState) => store.questionReducer.questions, (a) => a)
export const getActiveIndex = createSelector((store: RootState) => store.questionReducer.active_questions_index, (a) => a)
export const getActiveQuestions = createSelector((store: RootState) => store.questionReducer.questions[store.questionReducer.active_questions_index].questions, (a) => a)