diff --git a/web-app/src/AppRouter.jsx b/web-app/src/AppRouter.jsx
index befba5f..39ff9e8 100644
--- a/web-app/src/AppRouter.jsx
+++ b/web-app/src/AppRouter.jsx
@@ -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 = () => (
diff --git a/web-app/src/api/lens_types/GetAllLensTypes.jsx b/web-app/src/api/lens_types/GetAllLensTypes.jsx
new file mode 100644
index 0000000..9fefd97
--- /dev/null
+++ b/web-app/src/api/lens_types/GetAllLensTypes.jsx
@@ -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;
\ No newline at end of file
diff --git a/web-app/src/api/lenses/AddLens.jsx b/web-app/src/api/lenses/AddLens.jsx
index 1968d20..94d8fec 100644
--- a/web-app/src/api/lenses/AddLens.jsx
+++ b/web-app/src/api/lenses/AddLens.jsx
@@ -1,6 +1,5 @@
import CONFIG from "../../core/Config.jsx";
import axios from "axios";
-import {useAuth} from "../../AuthContext.jsx";
const addLens = async (token, lens) => {
diff --git a/web-app/src/api/lenses/UpdateLens.jsx b/web-app/src/api/lenses/UpdateLens.jsx
index fd99876..4e0acdb 100644
--- a/web-app/src/api/lenses/UpdateLens.jsx
+++ b/web-app/src/api/lenses/UpdateLens.jsx
@@ -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: {
diff --git a/web-app/src/components/lenses/LensFormModal.jsx b/web-app/src/components/lenses/LensFormModal.jsx
index e69de29..9cc808a 100644
--- a/web-app/src/components/lenses/LensFormModal.jsx
+++ b/web-app/src/components/lenses/LensFormModal.jsx
@@ -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 (
+ {
+ form.resetFields();
+ onCancel();
+ }}
+ onOk={handleOk}
+ okText={"Сохранить"}
+ cancelText={"Отмена"}
+ maskClosable={false}
+ forceRender={true}
+ style={{top: 20}}
+ centered
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+};
+
+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;
\ No newline at end of file
diff --git a/web-app/src/components/lenses/LensViewModal.jsx b/web-app/src/components/lenses/LensViewModal.jsx
index 5c40833..e29d719 100644
--- a/web-app/src/components/lenses/LensViewModal.jsx
+++ b/web-app/src/components/lenses/LensViewModal.jsx
@@ -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}) => {
{lens.diameter} мм
+
+
🔭 FVC
+ {lens.fvc} мм
+
+
🔄 Пресетная рефракция
{lens.preset_refraction} D
+
+
+
👀 Острота зрения (Trial)
+ {lens.trial.toFixed(2)} D
+
@@ -46,19 +56,17 @@ const LensViewModal = ({visible, onCancel, lens}) => {
{lens.side}
+
+
👓 Esa
+ {lens.esa}
+
+
{lens.issued ? '✅' : '❌'} Статус выдачи
{lens.issued ? 'Выдана' : 'Не выдана'}
-
-
-
-
-
👀 Острота зрения (Trial)
- {lens.trial.toFixed(2)} D
-
);
};
@@ -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,
}),
};
diff --git a/web-app/src/components/patients/PatientFormModal.jsx b/web-app/src/components/patients/PatientFormModal.jsx
index 11f00e3..6aad3c5 100644
--- a/web-app/src/components/patients/PatientFormModal.jsx
+++ b/web-app/src/components/patients/PatientFormModal.jsx
@@ -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
>
- {
label="Дата рождения"
rules={[{required: true, message: "Выберите дату рождения"}]}
>
-
+
{
- 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 (
-
-
-
- setSearchText(e.target.value)}
- style={{width: "100%"}}
- allowClear
- />
-
-
-
-
-
- {
- setShowIssuedLenses(e.target.checked);
- }}
- >
- Показать выданные
-
-
-
-
- {loading ? (
-
- }/>
-
- ) : (
-
(
-
- handleDeleteLens(lens.id)}
- handleEditLens={() => handleEditLens(lens)}
- />
-
- )}
- pagination={{
- current,
- pageSize,
- showSizeChanger: true,
- pageSizeOptions: ["5", "10", "20", "50"],
- onChange: (page, newPageSize) => {
- setCurrent(page);
- setPageSize(newPageSize);
- },
- }}
- />
- )}
-
- }
- style={{position: "fixed", bottom: 20, right: 20}}
- onClick={handleAddLens}
- />
-
- {/* setIsModalVisible(false)}*/}
- {/* onSubmit={handleModalSubmit}*/}
- {/* lens={selectedLens}*/}
- {/*/>*/}
-
- );
-};
-
-export default LensesPage;
diff --git a/web-app/src/pages/LensesPage.jsx b/web-app/src/pages/LensesPage.jsx
new file mode 100644
index 0000000..cd333cc
--- /dev/null
+++ b/web-app/src/pages/LensesPage.jsx
@@ -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 (
+
+
+
+ setSearchText(e.target.value)}
+ style={{width: "100%"}}
+ allowClear
+ />
+
+
+ : }
+ block
+ >
+ Расширенный поиск
+
+
+
+
+ {showAdvancedSearch && (
+
+
+
+
+ handleParamChange("tor", value)}
+ style={{width: "100%"}}
+ defaultValue={0}
+ step={0.1}
+ />
+
+
+
+ handleParamChange("diameter", value)}
+ style={{width: "100%"}}
+ defaultValue={0}
+ step={0.1}
+ />
+
+
+
+ handleParamChange("preset_refraction", value)}
+ style={{width: "100%"}}
+ defaultValue={0}
+ step={0.1}
+ />
+
+
+
+ handleParamChange("periphery_toricity", value)}
+ style={{width: "100%"}}
+ defaultValue={0}
+ step={0.1}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ handleParamChange("trial", value)}
+ style={{width: "100%"}}
+ defaultValue={0}
+ step={0.1}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {loading ? (
+
+ }/>
+
+ ) : (
+
(
+
+ handleDeleteLens(lens.id)}
+ handleEditLens={() => handleEditLens(lens)}
+ />
+
+ )}
+ pagination={{
+ current,
+ pageSize,
+ showSizeChanger: true,
+ pageSizeOptions: ["5", "10", "20", "50"],
+ onChange: (page, newPageSize) => {
+ setCurrent(page);
+ setPageSize(newPageSize);
+ },
+ }}
+ />
+ )}
+
+ }
+ type="primary"
+ style={{position: "fixed", bottom: 40, right: 40}}
+ onClick={handleAddLens}
+ tooltip="Добавить линзу"
+ />
+
+
+
+ );
+};
+
+export default LensesPage;
\ No newline at end of file
diff --git a/web-app/src/pages/PatientsPage.jsx b/web-app/src/pages/PatientsPage.jsx
index 9d53008..e81bc92 100644
--- a/web-app/src/pages/PatientsPage.jsx
+++ b/web-app/src/pages/PatientsPage.jsx
@@ -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 = () => {
/>
-
+
+
@@ -218,8 +222,10 @@ const PatientsPage = () => {
}
- style={{position: "fixed", bottom: 20, right: 20}}
+ type="primary"
+ style={{position: "fixed", bottom: 40, right: 40}}
onClick={handleAddPatient}
+ tooltip="Добавить пациента"
/>