From 8b7c42760268482e91ab06ae9d23d84b1e7ab407 Mon Sep 17 00:00:00 2001 From: Andrei Duvakin Date: Tue, 18 Feb 2025 13:05:15 +0500 Subject: [PATCH] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BA=D1=83=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=83=20?= =?UTF-8?q?=D1=81=20=D0=BB=D0=B8=D0=BD=D0=B7=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/app/infrastructure/lenses_service.py | 2 +- api/app/main.py | 2 + web-app/src/AppRouter.jsx | 2 +- web-app/src/api/lenses/AddLens.jsx | 22 +++ web-app/src/api/lenses/DeleteLens.jsx | 21 +++ web-app/src/api/lenses/GetAllLenses.jsx | 20 +++ web-app/src/api/lenses/UpdateLens.jsx | 20 +++ web-app/src/api/patients/AddPatient.jsx | 7 +- web-app/src/api/patients/DeletePatient.jsx | 7 +- web-app/src/api/patients/GetAllPatients.jsx | 7 +- web-app/src/api/patients/UpdatePatient.jsx | 4 - .../{patients => }/PrivateRoute.jsx | 2 +- .../src/components/lenses/LensFormModal.jsx | 0 .../src/components/lenses/LensListCard.jsx | 81 +++++++++ .../src/components/lenses/LensViewModal.jsx | 80 +++++++++ web-app/src/pages/LensPage.jsx | 158 +++++++++++++++++- 16 files changed, 411 insertions(+), 24 deletions(-) create mode 100644 web-app/src/api/lenses/AddLens.jsx create mode 100644 web-app/src/api/lenses/DeleteLens.jsx create mode 100644 web-app/src/api/lenses/GetAllLenses.jsx create mode 100644 web-app/src/api/lenses/UpdateLens.jsx rename web-app/src/components/{patients => }/PrivateRoute.jsx (83%) create mode 100644 web-app/src/components/lenses/LensFormModal.jsx create mode 100644 web-app/src/components/lenses/LensListCard.jsx create mode 100644 web-app/src/components/lenses/LensViewModal.jsx diff --git a/api/app/infrastructure/lenses_service.py b/api/app/infrastructure/lenses_service.py index cc27318..5fa39ef 100644 --- a/api/app/infrastructure/lenses_service.py +++ b/api/app/infrastructure/lenses_service.py @@ -23,7 +23,7 @@ class LensesService: tor=lens.tor, trial=lens.trial, esa=lens.esa, - fvc=lens.fvs, + fvc=lens.fvc, preset_refraction=lens.preset_refraction, diameter=lens.diameter, periphery_toricity=lens.periphery_toricity, diff --git a/api/app/main.py b/api/app/main.py index 7d9a201..600b1e9 100644 --- a/api/app/main.py +++ b/api/app/main.py @@ -4,6 +4,7 @@ from starlette.middleware.cors import CORSMiddleware from app.controllers.auth_router import router as auth_router from app.controllers.register_routes import router as register_router from app.controllers.patients_router import router as patients_router +from app.controllers.lenses_router import router as lens_router from app.settings import settings @@ -21,6 +22,7 @@ def start_app(): api_app.include_router(auth_router, prefix=settings.APP_PREFIX, tags=['auth']) api_app.include_router(register_router, prefix=settings.APP_PREFIX, tags=['register']) api_app.include_router(patients_router, prefix=settings.APP_PREFIX, tags=['patients']) + api_app.include_router(lens_router, prefix=settings.APP_PREFIX, tags=['lenses']) return api_app diff --git a/web-app/src/AppRouter.jsx b/web-app/src/AppRouter.jsx index 213efa7..befba5f 100644 --- a/web-app/src/AppRouter.jsx +++ b/web-app/src/AppRouter.jsx @@ -1,5 +1,5 @@ import {Routes, Route, Navigate} from "react-router-dom"; -import PrivateRoute from "./components/patients/PrivateRoute.jsx"; +import PrivateRoute from "./components/PrivateRoute.jsx"; import LoginPage from "./pages/LoginPage.jsx"; import MainLayout from "./layouts/MainLayout.jsx"; import PatientsPage from "./pages/PatientsPage.jsx"; diff --git a/web-app/src/api/lenses/AddLens.jsx b/web-app/src/api/lenses/AddLens.jsx new file mode 100644 index 0000000..1968d20 --- /dev/null +++ b/web-app/src/api/lenses/AddLens.jsx @@ -0,0 +1,22 @@ +import CONFIG from "../../core/Config.jsx"; +import axios from "axios"; +import {useAuth} from "../../AuthContext.jsx"; + + +const addLens = async (token, lens) => { + try { + const response = await axios.post(`${CONFIG.BASE_URL}/lenses/`, lens, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return response.data; + } catch (error) { + if (error.response?.status === 401) { + throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен"); + } + throw new Error(error.message); + } +}; + +export default addLens; \ No newline at end of file diff --git a/web-app/src/api/lenses/DeleteLens.jsx b/web-app/src/api/lenses/DeleteLens.jsx new file mode 100644 index 0000000..cd64a02 --- /dev/null +++ b/web-app/src/api/lenses/DeleteLens.jsx @@ -0,0 +1,21 @@ +import axios from "axios"; +import CONFIG from "../../core/Config.jsx"; + + +const deleteLens = async (token, lens_id) => { + try { + const response = await axios.delete(`${CONFIG.BASE_URL}/lenses/${lens_id}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return response.data; + } catch (error) { + if (error.response?.status === 401) { + throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен"); + } + throw new Error(error.message); + } +}; + +export default deleteLens; \ No newline at end of file diff --git a/web-app/src/api/lenses/GetAllLenses.jsx b/web-app/src/api/lenses/GetAllLenses.jsx new file mode 100644 index 0000000..c5485fc --- /dev/null +++ b/web-app/src/api/lenses/GetAllLenses.jsx @@ -0,0 +1,20 @@ +import axios from "axios"; +import CONFIG from "../../core/Config.jsx"; + +const getAllLenses = async (token) => { + try { + const response = await axios.get(`${CONFIG.BASE_URL}/lenses/`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return response.data; + } catch (error) { + if (error.response?.status === 401) { + throw Error("Ошибка авторизации: пользователь неяден или токен недействителен"); + } + throw Error(error.message); + } +}; + +export default getAllLenses; \ No newline at end of file diff --git a/web-app/src/api/lenses/UpdateLens.jsx b/web-app/src/api/lenses/UpdateLens.jsx new file mode 100644 index 0000000..fd99876 --- /dev/null +++ b/web-app/src/api/lenses/UpdateLens.jsx @@ -0,0 +1,20 @@ +import axios from "axios"; + + +const updateLens = async (token, lensId, lensData) => { + try { + const response = await axios.put(`${CONFIG.BASE_URL}/lenses/${lensId}/`, lensData, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return response.data; + } catch (error) { + if (error.response?.status === 401) { + throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен"); + } + throw new Error(error.message); + } +}; + +export default updateLens; \ No newline at end of file diff --git a/web-app/src/api/patients/AddPatient.jsx b/web-app/src/api/patients/AddPatient.jsx index c97b5fa..3b47afd 100644 --- a/web-app/src/api/patients/AddPatient.jsx +++ b/web-app/src/api/patients/AddPatient.jsx @@ -2,7 +2,7 @@ import axios from "axios"; import CONFIG from "../../core/Config.jsx"; -const AddPatient = async (token, patient) => { +const addPatient = async (token, patient) => { try { const response = await axios.post(`${CONFIG.BASE_URL}/patients/`, patient, { headers: { @@ -11,8 +11,11 @@ const AddPatient = async (token, patient) => { }); return response.data; } catch (error) { + if (error.response?.status === 401) { + throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен"); + } throw new Error(error.message); } }; -export default AddPatient; \ No newline at end of file +export default addPatient; \ No newline at end of file diff --git a/web-app/src/api/patients/DeletePatient.jsx b/web-app/src/api/patients/DeletePatient.jsx index a359557..c2ddbff 100644 --- a/web-app/src/api/patients/DeletePatient.jsx +++ b/web-app/src/api/patients/DeletePatient.jsx @@ -2,7 +2,7 @@ import axios from "axios"; import CONFIG from "../../core/Config.jsx"; -const DeletePatient = async (token, patient_id) => { +const deletePatient = async (token, patient_id) => { try { const response = await axios.delete(`${CONFIG.BASE_URL}/patients/${patient_id}/`, { headers: { @@ -11,9 +11,12 @@ const DeletePatient = async (token, patient_id) => { }); return response.data; } catch (error) { + if (error.response?.status === 401) { + throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен"); + } throw new Error(error.message); } } -export default DeletePatient; \ No newline at end of file +export default deletePatient; \ No newline at end of file diff --git a/web-app/src/api/patients/GetAllPatients.jsx b/web-app/src/api/patients/GetAllPatients.jsx index f6b7954..3f05354 100644 --- a/web-app/src/api/patients/GetAllPatients.jsx +++ b/web-app/src/api/patients/GetAllPatients.jsx @@ -3,11 +3,6 @@ import CONFIG from "../../core/Config.jsx"; const getAllPatients = async (token) => { - - if (!token) { - throw new Error("Ошибка авторизации: пользователь не аутентифицирован"); - } - try { const response = await axios.get(`${CONFIG.BASE_URL}/patients/`, { headers: { @@ -23,4 +18,4 @@ const getAllPatients = async (token) => { } }; -export default getAllPatients; +export default getAllPatients; \ No newline at end of file diff --git a/web-app/src/api/patients/UpdatePatient.jsx b/web-app/src/api/patients/UpdatePatient.jsx index 14f4545..bd9003b 100644 --- a/web-app/src/api/patients/UpdatePatient.jsx +++ b/web-app/src/api/patients/UpdatePatient.jsx @@ -3,10 +3,6 @@ import CONFIG from "../../core/Config.jsx"; const updatePatient = async (token, patientId, patientData) => { - if (!token) { - throw new Error("Ошибка авторизации: пользователь не аутентифицирован"); - } - try { const response = await axios.put(`${CONFIG.BASE_URL}/patients/${patientId}/`, patientData, { headers: { diff --git a/web-app/src/components/patients/PrivateRoute.jsx b/web-app/src/components/PrivateRoute.jsx similarity index 83% rename from web-app/src/components/patients/PrivateRoute.jsx rename to web-app/src/components/PrivateRoute.jsx index 792ad81..e9553ad 100644 --- a/web-app/src/components/patients/PrivateRoute.jsx +++ b/web-app/src/components/PrivateRoute.jsx @@ -1,5 +1,5 @@ import {Navigate, Outlet} from "react-router-dom"; -import {useAuth} from "../../AuthContext.jsx"; +import {useAuth} from "../AuthContext.jsx"; const PrivateRoute = () => { const {user} = useAuth(); diff --git a/web-app/src/components/lenses/LensFormModal.jsx b/web-app/src/components/lenses/LensFormModal.jsx new file mode 100644 index 0000000..e69de29 diff --git a/web-app/src/components/lenses/LensListCard.jsx b/web-app/src/components/lenses/LensListCard.jsx new file mode 100644 index 0000000..499c0e9 --- /dev/null +++ b/web-app/src/components/lenses/LensListCard.jsx @@ -0,0 +1,81 @@ +import { Card, Modal, Tooltip } from "antd"; +import PropTypes from "prop-types"; +import { DeleteOutlined, EditOutlined, EyeOutlined } from "@ant-design/icons"; +import { useState } from "react"; +import LensViewModal from "./LensViewModal.jsx"; + +const LensListCard = ({ lens, handleEditLens, handleDeleteLens }) => { + const [showModalInfo, setShowModalInfo] = useState(false); + + const deleteLensConfirm = () => { + Modal.confirm({ + title: "Удаление линзы", + content: `Вы уверены, что хотите удалить линзу с параметрами ${lens.side} ${lens.diameter}мм?`, + okText: "Да, удалить", + cancelText: "Отмена", + onOk: () => handleDeleteLens(lens.id), + }); + }; + + const handleViewLens = () => { + setShowModalInfo(true); + }; + + const actions = [ + + + , + + handleEditLens(lens)} /> + , + + + , + ]; + + return ( + <> + +

🔬 Тор: {lens.tor} D

+

📏 Диаметр: {lens.diameter} мм

+

🔄 Пресетная рефракция: {lens.preset_refraction} D

+

⚙️ Перефирийная торичность: {lens.periphery_toricity} D

+ {lens.issued &&

✅ Линза выдана

} +
+ + {showModalInfo && ( + setShowModalInfo(false)} + lens={lens} + /> + )} + + ); +}; + +LensListCard.propTypes = { + lens: PropTypes.shape({ + id: PropTypes.number.isRequired, + tor: PropTypes.number.isRequired, + trial: PropTypes.number, + esa: PropTypes.number, + fvc: PropTypes.number, + preset_refraction: PropTypes.number.isRequired, + diameter: PropTypes.number.isRequired, + periphery_toricity: PropTypes.number.isRequired, + side: PropTypes.string.isRequired, + issued: PropTypes.bool.isRequired, + }).isRequired, + handleEditLens: PropTypes.func.isRequired, + handleDeleteLens: PropTypes.func.isRequired, +}; + +export default LensListCard; diff --git a/web-app/src/components/lenses/LensViewModal.jsx b/web-app/src/components/lenses/LensViewModal.jsx new file mode 100644 index 0000000..6a3aad9 --- /dev/null +++ b/web-app/src/components/lenses/LensViewModal.jsx @@ -0,0 +1,80 @@ +import {Button, Col, Modal, Row, Typography, Divider} from "antd"; +import PropTypes from "prop-types"; + +const {Text, Title} = Typography; + +const LensViewModal = ({visible, onCancel, lens}) => { + if (!lens) return null; + + return ( + + Закрыть + + } + > + + +
+ 🔬 Тор + {lens.tor} D +
+ +
+ 📏 Диаметр + {lens.diameter} мм +
+ +
+ 🔄 Пресетная рефракция + {lens.preset_refraction} D +
+ + + +
+ ⚙️ Перефирийная торичность + {lens.periphery_toricity} D +
+ +
+ ⛓ Сторона + {lens.side} +
+ +
+ ✅ Статус выдачи + {lens.issued ? 'Выдана' : 'Не выдана'} +
+ +
+ + + +
+ ⚖️ Пробная линза (Trial) + {lens.trial.toFixed(2)} D +
+
+ ); +}; + +LensViewModal.propTypes = { + visible: PropTypes.bool.isRequired, + onCancel: PropTypes.func.isRequired, + lens: PropTypes.shape({ + tor: PropTypes.number.isRequired, + diameter: 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, // Указываем, что это число + }), +}; + +export default LensViewModal; diff --git a/web-app/src/pages/LensPage.jsx b/web-app/src/pages/LensPage.jsx index 90d539f..97c9798 100644 --- a/web-app/src/pages/LensPage.jsx +++ b/web-app/src/pages/LensPage.jsx @@ -1,12 +1,156 @@ +import {useState, useEffect} from "react"; +import {Input, Select, List, FloatButton, Row, Col, Spin} 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 [selectedLens, setSelectedLens] = useState(null); + + useEffect(() => { + 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()) + ) + ).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); + } + }; -const LensPage = () => { return ( - <> - - - ) -} +
+ + + setSearchText(e.target.value)} + style={{width: "100%"}} + allowClear + /> + + + + + -export default LensPage; + {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;