сделал загатовку модального окна по добавление/изменению линз. сделал страницу с линзами
This commit is contained in:
parent
b08c469950
commit
ee2f82d062
@ -4,7 +4,7 @@ import LoginPage from "./pages/LoginPage.jsx";
|
|||||||
import MainLayout from "./layouts/MainLayout.jsx";
|
import MainLayout from "./layouts/MainLayout.jsx";
|
||||||
import PatientsPage from "./pages/PatientsPage.jsx";
|
import PatientsPage from "./pages/PatientsPage.jsx";
|
||||||
import HomePage from "./pages/HomePage.jsx";
|
import HomePage from "./pages/HomePage.jsx";
|
||||||
import LensPage from "./pages/LensPage.jsx";
|
import LensPage from "./pages/LensesPage.jsx";
|
||||||
|
|
||||||
|
|
||||||
const AppRouter = () => (
|
const AppRouter = () => (
|
||||||
|
|||||||
21
web-app/src/api/lens_types/GetAllLensTypes.jsx
Normal file
21
web-app/src/api/lens_types/GetAllLensTypes.jsx
Normal 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;
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import CONFIG from "../../core/Config.jsx";
|
import CONFIG from "../../core/Config.jsx";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {useAuth} from "../../AuthContext.jsx";
|
|
||||||
|
|
||||||
|
|
||||||
const addLens = async (token, lens) => {
|
const addLens = async (token, lens) => {
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import CONFIG from "../../core/Config.jsx";
|
||||||
|
|
||||||
|
|
||||||
const updateLens = async (token, lensId, lensData) => {
|
const updateLens = async (token, lensId, lensData) => {
|
||||||
|
console.log(lensId, lensData);
|
||||||
try {
|
try {
|
||||||
const response = await axios.put(`${CONFIG.BASE_URL}/lenses/${lensId}/`, lensData, {
|
const response = await axios.put(`${CONFIG.BASE_URL}/lenses/${lensId}/`, lensData, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const {Text, Title} = Typography;
|
const {Text, Title} = Typography;
|
||||||
@ -29,10 +29,20 @@ const LensViewModal = ({visible, onCancel, lens}) => {
|
|||||||
<Text>{lens.diameter} мм</Text>
|
<Text>{lens.diameter} мм</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{marginBottom: 12}}>
|
||||||
|
<Title level={5}>🔭 FVC</Title>
|
||||||
|
<Text>{lens.fvc} мм</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{marginBottom: 12}}>
|
<div style={{marginBottom: 12}}>
|
||||||
<Title level={5}>🔄 Пресетная рефракция</Title>
|
<Title level={5}>🔄 Пресетная рефракция</Title>
|
||||||
<Text>{lens.preset_refraction} D</Text>
|
<Text>{lens.preset_refraction} D</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{marginBottom: 12}}>
|
||||||
|
<Title level={5}>👀 Острота зрения (Trial)</Title>
|
||||||
|
<Text>{lens.trial.toFixed(2)} D</Text>
|
||||||
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
@ -46,19 +56,17 @@ const LensViewModal = ({visible, onCancel, lens}) => {
|
|||||||
<Text>{lens.side}</Text>
|
<Text>{lens.side}</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{marginBottom: 12}}>
|
||||||
|
<Title level={5}>👓 Esa</Title>
|
||||||
|
<Text>{lens.esa}</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{marginBottom: 12}}>
|
<div style={{marginBottom: 12}}>
|
||||||
<Title level={5}>{lens.issued ? '✅' : '❌'} Статус выдачи</Title>
|
<Title level={5}>{lens.issued ? '✅' : '❌'} Статус выдачи</Title>
|
||||||
<Text>{lens.issued ? 'Выдана' : 'Не выдана'}</Text>
|
<Text>{lens.issued ? 'Выдана' : 'Не выдана'}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Divider/>
|
|
||||||
|
|
||||||
<div style={{marginBottom: 12}}>
|
|
||||||
<Title level={5}>👀 Острота зрения (Trial)</Title>
|
|
||||||
<Text>{lens.trial.toFixed(2)} D</Text>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -69,11 +77,13 @@ LensViewModal.propTypes = {
|
|||||||
lens: PropTypes.shape({
|
lens: PropTypes.shape({
|
||||||
tor: PropTypes.number.isRequired,
|
tor: PropTypes.number.isRequired,
|
||||||
diameter: PropTypes.number.isRequired,
|
diameter: PropTypes.number.isRequired,
|
||||||
|
esa: PropTypes.number.isRequired,
|
||||||
|
fvc: PropTypes.number.isRequired,
|
||||||
preset_refraction: PropTypes.number.isRequired,
|
preset_refraction: PropTypes.number.isRequired,
|
||||||
periphery_toricity: PropTypes.number.isRequired,
|
periphery_toricity: PropTypes.number.isRequired,
|
||||||
side: PropTypes.string.isRequired,
|
side: PropTypes.string.isRequired,
|
||||||
issued: PropTypes.bool.isRequired,
|
issued: PropTypes.bool.isRequired,
|
||||||
trial: PropTypes.number.isRequired, // Указываем, что это число
|
trial: PropTypes.number.isRequired,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {useEffect} from "react";
|
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 PropTypes from "prop-types";
|
||||||
import locale from "antd/es/date-picker/locale/ru_RU";
|
import locale from "antd/es/date-picker/locale/ru_RU";
|
||||||
import validator from "validator";
|
import validator from "validator";
|
||||||
@ -31,8 +31,13 @@ const PatientFormModal = ({visible, onCancel, onSubmit, patient}) => {
|
|||||||
}
|
}
|
||||||
onSubmit(values);
|
onSubmit(values);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
} catch (errorInfo) {
|
} catch (error) {
|
||||||
console.log("Validation Failed:", errorInfo);
|
console.log("Validation Failed:", error);
|
||||||
|
notification.error({
|
||||||
|
message: "Ошибка валидации",
|
||||||
|
description: "Проверьте правильность заполнения полей.",
|
||||||
|
placement: "topRight",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,14 +50,14 @@ const PatientFormModal = ({visible, onCancel, onSubmit, patient}) => {
|
|||||||
onCancel();
|
onCancel();
|
||||||
}}
|
}}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
okText="Сохранить"
|
okText={"Сохранить"}
|
||||||
cancelText="Отмена"
|
cancelText={"Отмена"}
|
||||||
centered
|
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
forceRender={true}
|
forceRender={true}
|
||||||
style={{top: 20}}
|
style={{top: 20}}
|
||||||
|
centered
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout={"vertical"}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="first_name"
|
name="first_name"
|
||||||
label="Имя"
|
label="Имя"
|
||||||
@ -78,7 +83,11 @@ const PatientFormModal = ({visible, onCancel, onSubmit, patient}) => {
|
|||||||
label="Дата рождения"
|
label="Дата рождения"
|
||||||
rules={[{required: true, message: "Выберите дату рождения"}]}
|
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>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="address"
|
name="address"
|
||||||
|
|||||||
@ -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;
|
|
||||||
382
web-app/src/pages/LensesPage.jsx
Normal file
382
web-app/src/pages/LensesPage.jsx
Normal 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;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import {useEffect, useState} from "react";
|
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 {LoadingOutlined, PlusOutlined} from "@ant-design/icons";
|
||||||
import {useAuth} from "../AuthContext.jsx";
|
import {useAuth} from "../AuthContext.jsx";
|
||||||
import getAllPatients from "../api/patients/GetAllPatients.jsx";
|
import getAllPatients from "../api/patients/GetAllPatients.jsx";
|
||||||
@ -162,6 +162,9 @@ const PatientsPage = () => {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={8}>
|
<Col xs={24} sm={8}>
|
||||||
|
<Tooltip
|
||||||
|
title={"Сортировка пациентов"}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
value={sortOrder}
|
value={sortOrder}
|
||||||
onChange={(value) => setSortOrder(value)}
|
onChange={(value) => setSortOrder(value)}
|
||||||
@ -170,6 +173,7 @@ const PatientsPage = () => {
|
|||||||
<Option value="asc">А-Я</Option>
|
<Option value="asc">А-Я</Option>
|
||||||
<Option value="desc">Я-А</Option>
|
<Option value="desc">Я-А</Option>
|
||||||
</Select>
|
</Select>
|
||||||
|
</Tooltip>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
@ -218,8 +222,10 @@ const PatientsPage = () => {
|
|||||||
|
|
||||||
<FloatButton
|
<FloatButton
|
||||||
icon={<PlusOutlined/>}
|
icon={<PlusOutlined/>}
|
||||||
style={{position: "fixed", bottom: 20, right: 20}}
|
type="primary"
|
||||||
|
style={{position: "fixed", bottom: 40, right: 40}}
|
||||||
onClick={handleAddPatient}
|
onClick={handleAddPatient}
|
||||||
|
tooltip="Добавить пациента"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PatientFormModal
|
<PatientFormModal
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user