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 + /> + + + + + + + {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="Добавить пациента" />