сделал загатовку модального окна по добавление/изменению линз. сделал страницу с линзами

This commit is contained in:
Андрей Дувакин 2025-02-19 20:16:47 +05:00
parent b08c469950
commit ee2f82d062
10 changed files with 635 additions and 223 deletions

View File

@ -4,7 +4,7 @@ import LoginPage from "./pages/LoginPage.jsx";
import MainLayout from "./layouts/MainLayout.jsx";
import PatientsPage from "./pages/PatientsPage.jsx";
import HomePage from "./pages/HomePage.jsx";
import LensPage from "./pages/LensPage.jsx";
import LensPage from "./pages/LensesPage.jsx";
const AppRouter = () => (

View File

@ -0,0 +1,21 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const getAllLensTypes = async (token) => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/lens_types/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.response?.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
};
export default getAllLensTypes;

View File

@ -1,6 +1,5 @@
import CONFIG from "../../core/Config.jsx";
import axios from "axios";
import {useAuth} from "../../AuthContext.jsx";
const addLens = async (token, lens) => {

View File

@ -1,7 +1,9 @@
import axios from "axios";
import CONFIG from "../../core/Config.jsx";
const updateLens = async (token, lensId, lensData) => {
console.log(lensId, lensData);
try {
const response = await axios.put(`${CONFIG.BASE_URL}/lenses/${lensId}/`, lensData, {
headers: {

View File

@ -0,0 +1,178 @@
import {Form, InputNumber, Modal, notification, Select} from "antd";
import {useEffect, useState} from "react";
import PropTypes from "prop-types";
import getAllLensTypes from "../../api/lens_types/GetAllLensTypes.jsx";
import {useAuth} from "../../AuthContext.jsx";
const LensFormModal = ({visible, onCancel, onSubmit, lens}) => {
const {user} = useAuth();
const [form] = Form.useForm();
const [lensTypes, setLensTypes] = useState([]);
useEffect(() => {
fetchLensTypes();
}, [])
useEffect(() => {
if (visible) {
form.resetFields();
if (lens) {
form.setFieldsValue({
...lens,
})
}
}
}, [visible, lens]);
const fetchLensTypes = async () => {
try {
const data = await getAllLensTypes(user.token);
setLensTypes(data);
} catch (error) {
console.log(error);
notification.error({
message: "Ошибка загрузки типов линз",
description: "Проверьте подключение к сети.",
placement: "topRight",
});
}
}
const handleOk = async () => {
try {
const values = await form.validateFields();
onSubmit(values);
form.resetFields();
} catch (error) {
console.log("Validation Failed:", error)
}
};
return (
<Modal
title={lens ? "Редактировать лину" : "Добавить линзу"}
open={visible}
onCancel={() => {
form.resetFields();
onCancel();
}}
onOk={handleOk}
okText={"Сохранить"}
cancelText={"Отмена"}
maskClosable={false}
forceRender={true}
style={{top: 20}}
centered
>
<Form form={form} layout={"vertical"}>
<Form.Item
name="tor"
label="Тор"
rules={[{required: true, message: "Введите тор"}]}
>
<InputNumber
step={0.1}
/>
</Form.Item>
<Form.Item
name="trial"
label="Острота зрения (Trial)"
rules={[{required: true, message: "Введите остроту зрения"}]}
>
<InputNumber
step={0.1}
/>
</Form.Item>
<Form.Item
name="esa"
label="Esa"
rules={[{required: true, message: "Введите esa"}]}
>
<InputNumber/>
</Form.Item>
<Form.Item
name="fvc"
label="FVC"
rules={[{required: true, message: "Введите fvc"}]}
>
<InputNumber/>
</Form.Item>
<Form.Item
name="preset_refraction"
label="Пресетная рефракция"
rules={[{required: true, message: "Введите пресетную рефракцию"}]}
>
<InputNumber/>
</Form.Item>
<Form.Item
name="diameter"
label="Диаметр"
rules={[{required: true, message: "Введите диаметр"}]}
>
<InputNumber/>
</Form.Item>
<Form.Item
name="periphery_toricity"
label="Периферия торичность"
rules={[{required: true, message: "Введите периферию торичность"}]}
>
<InputNumber/>
</Form.Item>
<Form.Item
name="side"
label="Сторона"
rules={[{required: true, message: "Выберите сторону"}]}
>
<Select>
<Select.Option value="left">Левая</Select.Option>
<Select.Option value="right">Правая</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="type_id"
label="Тип линзы"
rules={[{required: true, message: "Выберите тип линзы"}]}
>
<Select>
{lensTypes.map((type) => (
<Select.Option key={type.id} value={type.id}>
{type.title}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
)
};
LensFormModal.propTypes = {
visible: PropTypes.bool.isRequired,
onCancel: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
lens: PropTypes.shape({
tor: PropTypes.number.isRequired,
trial: PropTypes.number.isRequired,
esa: PropTypes.number.isRequired,
fvc: PropTypes.number.isRequired,
preset_refraction: PropTypes.number.isRequired,
diameter: PropTypes.number.isRequired,
periphery_toricity: PropTypes.number.isRequired,
side: PropTypes.string.isRequired,
issued: PropTypes.bool.isRequired,
type_id: PropTypes.number.isRequired,
}),
}
export default LensFormModal;

View File

@ -1,4 +1,4 @@
import {Button, Col, Modal, Row, Typography, Divider} from "antd";
import {Button, Col, Modal, Row, Typography} from "antd";
import PropTypes from "prop-types";
const {Text, Title} = Typography;
@ -29,10 +29,20 @@ const LensViewModal = ({visible, onCancel, lens}) => {
<Text>{lens.diameter} мм</Text>
</div>
<div style={{marginBottom: 12}}>
<Title level={5}>🔭 FVC</Title>
<Text>{lens.fvc} мм</Text>
</div>
<div style={{marginBottom: 12}}>
<Title level={5}>🔄 Пресетная рефракция</Title>
<Text>{lens.preset_refraction} D</Text>
</div>
<div style={{marginBottom: 12}}>
<Title level={5}>👀 Острота зрения (Trial)</Title>
<Text>{lens.trial.toFixed(2)} D</Text>
</div>
</Col>
<Col xs={24} md={12}>
@ -46,19 +56,17 @@ const LensViewModal = ({visible, onCancel, lens}) => {
<Text>{lens.side}</Text>
</div>
<div style={{marginBottom: 12}}>
<Title level={5}>👓 Esa</Title>
<Text>{lens.esa}</Text>
</div>
<div style={{marginBottom: 12}}>
<Title level={5}>{lens.issued ? '✅' : '❌'} Статус выдачи</Title>
<Text>{lens.issued ? 'Выдана' : 'Не выдана'}</Text>
</div>
</Col>
</Row>
<Divider/>
<div style={{marginBottom: 12}}>
<Title level={5}>👀 Острота зрения (Trial)</Title>
<Text>{lens.trial.toFixed(2)} D</Text>
</div>
</Modal>
);
};
@ -69,11 +77,13 @@ LensViewModal.propTypes = {
lens: PropTypes.shape({
tor: PropTypes.number.isRequired,
diameter: PropTypes.number.isRequired,
esa: PropTypes.number.isRequired,
fvc: PropTypes.number.isRequired,
preset_refraction: PropTypes.number.isRequired,
periphery_toricity: PropTypes.number.isRequired,
side: PropTypes.string.isRequired,
issued: PropTypes.bool.isRequired,
trial: PropTypes.number.isRequired, // Указываем, что это число
trial: PropTypes.number.isRequired,
}),
};

View File

@ -1,5 +1,5 @@
import {useEffect} from "react";
import {Modal, Form, Input, DatePicker} from "antd";
import {Modal, Form, Input, DatePicker, notification} from "antd";
import PropTypes from "prop-types";
import locale from "antd/es/date-picker/locale/ru_RU";
import validator from "validator";
@ -31,8 +31,13 @@ const PatientFormModal = ({visible, onCancel, onSubmit, patient}) => {
}
onSubmit(values);
form.resetFields();
} catch (errorInfo) {
console.log("Validation Failed:", errorInfo);
} catch (error) {
console.log("Validation Failed:", error);
notification.error({
message: "Ошибка валидации",
description: "Проверьте правильность заполнения полей.",
placement: "topRight",
});
}
};
@ -45,14 +50,14 @@ const PatientFormModal = ({visible, onCancel, onSubmit, patient}) => {
onCancel();
}}
onOk={handleOk}
okText="Сохранить"
cancelText="Отмена"
centered
okText={"Сохранить"}
cancelText={"Отмена"}
maskClosable={false}
forceRender={true}
style={{top: 20}}
centered
>
<Form form={form} layout="vertical">
<Form form={form} layout={"vertical"}>
<Form.Item
name="first_name"
label="Имя"
@ -78,7 +83,11 @@ const PatientFormModal = ({visible, onCancel, onSubmit, patient}) => {
label="Дата рождения"
rules={[{required: true, message: "Выберите дату рождения"}]}
>
<DatePicker style={{width: "100%"}} locale={locale} format="DD.MM.YYYY"/>
<DatePicker
style={{width: "100%"}}
locale={locale} format="DD.MM.YYYY"
maxDate={dayjs(new Date())}
/>
</Form.Item>
<Form.Item
name="address"

View File

@ -1,195 +0,0 @@
import {useState, useEffect} from "react";
import {Input, Select, List, FloatButton, Row, Col, Spin, Checkbox} from "antd";
import {LoadingOutlined, PlusOutlined} from "@ant-design/icons";
import LensCard from "../components/lenses/LensListCard.jsx";
import getAllLenses from "../api/lenses/GetAllLenses.jsx";
import addLens from "../api/lenses/AddLens.jsx";
import updateLens from "../api/lenses/UpdateLens.jsx";
import deleteLens from "../api/lenses/DeleteLens.jsx";
import {useAuth} from "../AuthContext.jsx";
const {Option} = Select;
const LensesPage = () => {
const {user} = useAuth();
const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [searchText, setSearchText] = useState("");
const [sortOrder, setSortOrder] = useState("asc");
const [lenses, setLenses] = useState([]);
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [showIssuedLenses, setShowIssuedLenses] = useState(false);
const [selectedLens, setSelectedLens] = useState(null);
useEffect(() => {
fetchLensWithCache();
}, []);
useEffect(() => {
if (!isModalVisible) {
const intervalId = setInterval(fetchLenses, 5000);
return () => clearInterval(intervalId);
}
}, [user, isModalVisible]);
const fetchLensWithCache = async () => {
const cachedData = localStorage.getItem("lensData");
const cacheTimestamp = localStorage.getItem("lensTimestamp");
if (cachedData && cacheTimestamp && (Date.now() - parseInt(cacheTimestamp)) < 60 * 1000) {
setLenses(JSON.parse(cachedData));
setLoading(false);
return;
}
await fetchLenses();
};
const fetchLenses = async () => {
if (!user || !user.token) return;
try {
const data = await getAllLenses(user.token);
setLenses(data);
setLoading(false);
} catch (error) {
console.error("Ошибка загрузки линз:", error);
setLoading(false);
}
};
const filteredLenses = lenses.filter((lens) =>
Object.values(lens).some((value) =>
value?.toString().toLowerCase().includes(searchText.toLowerCase())
) &&
(showIssuedLenses || lens.issued === false)
).sort((a, b) => {
return sortOrder === "asc"
? a.preset_refraction - b.preset_refraction
: b.preset_refraction - a.preset_refraction;
});
const handleAddLens = () => {
setSelectedLens(null);
setIsModalVisible(true);
};
const handleEditLens = (lens) => {
setSelectedLens(lens);
setIsModalVisible(true);
};
const handleDeleteLens = async (lensId) => {
if (!user || !user.token) return;
try {
await deleteLens(lensId, user.token);
fetchLenses(user.token);
} catch (error) {
console.error("Ошибка удаления линзы:", error);
}
};
const handleModalSubmit = async (lensData) => {
try {
if (selectedLens) {
await updateLens(selectedLens.id, lensData);
} else {
await addLens(lensData);
}
setIsModalVisible(false);
fetchLenses();
} catch (error) {
console.error("Ошибка сохранения линзы:", error);
}
};
return (
<div style={{padding: 20}}>
<Row gutter={[16, 16]} style={{marginBottom: 20}}>
<Col xs={24} sm={12}>
<Input
placeholder="Поиск линзы"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{width: "100%"}}
allowClear
/>
</Col>
<Col xs={24} sm={8}>
<Select
value={sortOrder}
onChange={(value) => setSortOrder(value)}
style={{width: "100%"}}
>
<Option value="asc">По возрастанию рефракции</Option>
<Option value="desc">По убыванию рефракции</Option>
</Select>
</Col>
<Col xs={24} sm={4}>
<Checkbox
onChange={(e) => {
setShowIssuedLenses(e.target.checked);
}}
>
Показать выданные
</Checkbox>
</Col>
</Row>
{loading ? (
<div style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
}}>
<Spin indicator={<LoadingOutlined style={{fontSize: 64, color: "#1890ff"}} spin/>}/>
</div>
) : (
<List
grid={{gutter: 16, xs: 1, sm: 1, md: 2, lg: 3, xl: 4}}
dataSource={filteredLenses}
renderItem={(lens) => (
<List.Item>
<LensCard
lens={lens}
handleDeleteLens={() => handleDeleteLens(lens.id)}
handleEditLens={() => handleEditLens(lens)}
/>
</List.Item>
)}
pagination={{
current,
pageSize,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => {
setCurrent(page);
setPageSize(newPageSize);
},
}}
/>
)}
<FloatButton
icon={<PlusOutlined/>}
style={{position: "fixed", bottom: 20, right: 20}}
onClick={handleAddLens}
/>
{/*<LensModal*/}
{/* visible={isModalVisible}*/}
{/* onCancel={() => setIsModalVisible(false)}*/}
{/* onSubmit={handleModalSubmit}*/}
{/* lens={selectedLens}*/}
{/*/>*/}
</div>
);
};
export default LensesPage;

View File

@ -0,0 +1,382 @@
import {useState, useEffect} from "react";
import {
Input,
Select,
List,
FloatButton,
Row,
Col,
Spin,
Button,
Form,
InputNumber,
Card, Grid, notification
} from "antd";
import {LoadingOutlined, PlusOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import LensCard from "../components/lenses/LensListCard.jsx";
import getAllLenses from "../api/lenses/GetAllLenses.jsx";
import addLens from "../api/lenses/AddLens.jsx";
import updateLens from "../api/lenses/UpdateLens.jsx";
import deleteLens from "../api/lenses/DeleteLens.jsx";
import {useAuth} from "../AuthContext.jsx";
import LensFormModal from "../components/lenses/LensFormModal.jsx";
const {Option} = Select;
const {useBreakpoint} = Grid;
const LensesPage = () => {
const {user} = useAuth();
const screens = useBreakpoint();
const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [searchText, setSearchText] = useState("");
const [lenses, setLenses] = useState([]);
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedLens, setSelectedLens] = useState(null);
const [showAdvancedSearch, setShowAdvancedSearch] = useState(false);
const [searchParams, setSearchParams] = useState({
tor: null,
diameter: null,
preset_refraction: null,
periphery_toricity: null,
side: 'all',
issued: false,
trial: null
});
useEffect(() => {
fetchLensWithCache();
}, []);
useEffect(() => {
if (!isModalVisible) {
const intervalId = setInterval(fetchLenses, 5000);
return () => clearInterval(intervalId);
}
}, [user, isModalVisible]);
const fetchLensWithCache = async () => {
const cachedData = localStorage.getItem("lensData");
const cacheTimestamp = localStorage.getItem("lensTimestamp");
if (cachedData && cacheTimestamp && (Date.now() - parseInt(cacheTimestamp)) < 60 * 1000) {
setLenses(JSON.parse(cachedData));
setLoading(false);
return;
}
await fetchLenses();
};
const fetchLenses = async () => {
if (!user || !user.token) return;
try {
const data = await getAllLenses(user.token);
setLenses(data);
setLoading(false);
} catch (error) {
console.error("Ошибка загрузки линз:", error);
notification.error({
message: "Ошибка загрузки линз",
description: "Проверьте подключение к сети.",
placement: "topRight",
})
setLoading(false);
}
};
const filteredLenses = lenses.filter((lens) => {
const textMatch = Object.values(lens).some((value) =>
value?.toString().toLowerCase().includes(searchText.toLowerCase())
);
const advancedMatch = Object.entries(searchParams).every(([key, value]) => {
if (value === null || value === '') return true;
if (key === 'side') {
if (value === 'all') return true;
return lens.side === value;
}
if (key === 'issued') {
return lens.issued === value || value === "all";
}
return lens[key] === value;
});
return textMatch && advancedMatch && (searchParams.issued || lens.issued === false);
}).sort((a, b) => {
return a.preset_refraction - b.preset_refraction;
});
const handleAddLens = () => {
setSelectedLens(null);
setIsModalVisible(true);
};
const handleEditLens = (lens) => {
setSelectedLens(lens);
setIsModalVisible(true);
};
const handleDeleteLens = async (lensId) => {
if (!user || !user.token) return;
try {
await deleteLens(user.token, lensId);
fetchLenses(user.token);
notification.success({
message: "Линза удалена",
description: "Линза успешно удалена.",
placement: "topRight",
})
} catch (error) {
console.error("Ошибка удаления линзы:", error);
notification.error({
message: "Ошибка удаления линзы",
description: "Проверьте подключение к сети.",
placement: "topRight",
});
}
};
const handleModalSubmit = async (lensData) => {
try {
if (selectedLens) {
await updateLens(user.token, selectedLens.id, lensData);
notification.success({
message: "Линза обновлена",
description: "Линза успешно обновлена.",
placement: "topRight",
});
} else {
await addLens(user.token, lensData);
notification.success({
message: "Линза добавлена",
description: "Линза успешно добавлена.",
placement: "topRight",
});
}
setIsModalVisible(false);
await fetchLenses();
} catch (error) {
console.error("Ошибка сохранения линзы:", error);
notification.error({
message: "Ошибка сохранения линзы",
description: "Проверьте подключение к сети.",
placement: "topRight",
});
}
};
const toggleAdvancedSearch = () => {
setShowAdvancedSearch(!showAdvancedSearch);
};
const handleParamChange = (param, value) => {
setSearchParams({...searchParams, [param]: value});
};
const handleCancel = () => {
setIsModalVisible(false);
};
return (
<div style={{padding: 20}}>
<Row gutter={[16, 16]} style={{marginBottom: 20}}>
<Col xs={24} md={17} sm={14} xl={20}>
<Input
placeholder="Поиск линзы"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{width: "100%"}}
allowClear
/>
</Col>
<Col xs={24} md={7} sm={10} xl={4}>
<Button
onClick={toggleAdvancedSearch} icon={showAdvancedSearch ? <UpOutlined/> : <DownOutlined/>}
block
>
Расширенный поиск
</Button>
</Col>
</Row>
{showAdvancedSearch && (
<Card
title="Расширенный поиск"
style={{
marginBottom: 20,
boxShadow: "0 1px 6px rgba(0, 0, 0, 0.15)",
borderRadius: 8
}}
type={"inner"}
>
<Row gutter={16}>
<Col xs={24} sm={12}>
<Form layout="vertical">
<Form.Item label="Тор">
<InputNumber
value={searchParams.tor || 0}
onChange={(value) => handleParamChange("tor", value)}
style={{width: "100%"}}
defaultValue={0}
step={0.1}
/>
</Form.Item>
<Form.Item label="Диаметр">
<InputNumber
value={searchParams.diameter || 0}
onChange={(value) => handleParamChange("diameter", value)}
style={{width: "100%"}}
defaultValue={0}
step={0.1}
/>
</Form.Item>
<Form.Item label="Рефракция">
<InputNumber
value={searchParams.preset_refraction || 0}
onChange={(value) => handleParamChange("preset_refraction", value)}
style={{width: "100%"}}
defaultValue={0}
step={0.1}
/>
</Form.Item>
<Form.Item label="Периферическая торичность">
<InputNumber
value={searchParams.periphery_toricity || 0}
onChange={(value) => handleParamChange("periphery_toricity", value)}
style={{width: "100%"}}
defaultValue={0}
step={0.1}
/>
</Form.Item>
</Form>
</Col>
<Col xs={24} sm={12}>
<Form layout="vertical">
<Form.Item label="Сторона">
<Select
value={searchParams.side}
onChange={(value) => handleParamChange("side", value)}
style={{width: "100%"}}
>
<Option value="all">Все</Option>
<Option value="левая">Левая</Option>
<Option value="правая">Правая</Option>
</Select>
</Form.Item>
<Form.Item label="Выдана">
<Select
value={searchParams.issued}
onChange={(value) => handleParamChange("issued", value)}
style={{width: "100%"}}
>
<Option value="all">Все</Option>
<Option value={true}>Да</Option>
<Option value={false}>Нет</Option>
</Select>
</Form.Item>
<Form.Item label="Острота зрения (Trial)">
<InputNumber
value={searchParams.trial || 0}
onChange={(value) => handleParamChange("trial", value)}
style={{width: "100%"}}
defaultValue={0}
step={0.1}
/>
</Form.Item>
<Row>
<Col xs={24} sm={12} offset={screens.sm ? 12 : 0}
style={{textAlign: screens.sm ? "right" : "center", marginTop: "1.7rem"}}>
<Button
type="primary"
block={!screens.sm}
onClick={() => {
setSearchParams({
tor: null,
diameter: null,
preset_refraction: null,
periphery_toricity: null,
side: 'all',
issued: false,
trial: null
});
}}
>
Сбросить
</Button>
</Col>
</Row>
</Form>
</Col>
</Row>
</Card>
)}
{loading ? (
<div style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
}}>
<Spin indicator={<LoadingOutlined style={{fontSize: 64, color: "#1890ff"}} spin/>}/>
</div>
) : (
<List
grid={{gutter: 16, xs: 1, sm: 1, md: 2, lg: 3, xl: 4}}
dataSource={filteredLenses}
renderItem={(lens) => (
<List.Item>
<LensCard
lens={lens}
handleDeleteLens={() => handleDeleteLens(lens.id)}
handleEditLens={() => handleEditLens(lens)}
/>
</List.Item>
)}
pagination={{
current,
pageSize,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => {
setCurrent(page);
setPageSize(newPageSize);
},
}}
/>
)}
<FloatButton
icon={<PlusOutlined/>}
type="primary"
style={{position: "fixed", bottom: 40, right: 40}}
onClick={handleAddLens}
tooltip="Добавить линзу"
/>
<LensFormModal
visible={isModalVisible}
onCancel={handleCancel}
onSubmit={handleModalSubmit}
lens={selectedLens}
/>
</div>
);
};
export default LensesPage;

View File

@ -1,5 +1,5 @@
import {useEffect, useState} from "react";
import {Input, Select, List, FloatButton, Row, Col, Spin, notification} from "antd";
import {Input, Select, List, FloatButton, Row, Col, Spin, notification, Tooltip} from "antd";
import {LoadingOutlined, PlusOutlined} from "@ant-design/icons";
import {useAuth} from "../AuthContext.jsx";
import getAllPatients from "../api/patients/GetAllPatients.jsx";
@ -162,14 +162,18 @@ const PatientsPage = () => {
/>
</Col>
<Col xs={24} sm={8}>
<Select
value={sortOrder}
onChange={(value) => setSortOrder(value)}
style={{width: "100%"}}
<Tooltip
title={"Сортировка пациентов"}
>
<Option value="asc">А-Я</Option>
<Option value="desc">Я-А</Option>
</Select>
<Select
value={sortOrder}
onChange={(value) => setSortOrder(value)}
style={{width: "100%"}}
>
<Option value="asc">А-Я</Option>
<Option value="desc">Я-А</Option>
</Select>
</Tooltip>
</Col>
</Row>
@ -218,8 +222,10 @@ const PatientsPage = () => {
<FloatButton
icon={<PlusOutlined/>}
style={{position: "fixed", bottom: 20, right: 20}}
type="primary"
style={{position: "fixed", bottom: 40, right: 40}}
onClick={handleAddPatient}
tooltip="Добавить пациента"
/>
<PatientFormModal