сделал редактирование пациента

This commit is contained in:
Андрей Дувакин 2025-02-12 20:44:22 +05:00
parent 0ca1978c81
commit 44c092ecb9
8 changed files with 240 additions and 132 deletions

View File

@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db from app.database.session import get_db
from app.domain.entities.patient import PatientEntity
from app.infrastructure.dependencies import get_current_user from app.infrastructure.dependencies import get_current_user
from app.infrastructure.patients_service import PatientsService from app.infrastructure.patients_service import PatientsService
@ -15,3 +16,24 @@ async def get_all_patients(
): ):
patients_service = PatientsService(db) patients_service = PatientsService(db)
return await patients_service.get_all_patients() return await patients_service.get_all_patients()
@router.post("/patients/")
async def create_patient(
patient: PatientEntity,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user)
):
patients_service = PatientsService(db)
return await patients_service.create_patient(patient)
@router.put("/patients/{patient_id}/")
async def update_patient(
patient_id: int,
patient: PatientEntity,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user)
):
patients_service = PatientsService(db)
return await patients_service.update_patient(patient_id, patient)

View File

@ -5,13 +5,13 @@ from pydantic import BaseModel
class PatientEntity(BaseModel): class PatientEntity(BaseModel):
id: Optional[int] id: Optional[int] = None
first_name: str first_name: str
last_name: str last_name: str
patronymic: Optional[str] patronymic: Optional[str] = None
birthday: datetime.date birthday: datetime.date
address: Optional[str] address: Optional[str] = None
email: Optional[str] email: Optional[str] = None
phone: Optional[str] phone: Optional[str] = None
diagnosis: Optional[str] diagnosis: Optional[str] = None
correction: Optional[str] correction: Optional[str] = None

View File

@ -55,8 +55,8 @@ class PatientsService:
correction=patient_model.correction, correction=patient_model.correction,
) )
async def update_patient(self, user_id: int, patient: PatientEntity) -> Optional[PatientEntity]: async def update_patient(self, patient_id: int, patient: PatientEntity) -> Optional[PatientEntity]:
patient_model = await self.patient_repository.get_by_id(user_id) patient_model = await self.patient_repository.get_by_id(patient_id)
if patient_model: if patient_model:
patient_model.first_name = patient.first_name patient_model.first_name = patient.first_name
patient_model.last_name = patient.last_name patient_model.last_name = patient.last_name

View File

@ -12,12 +12,14 @@
"@react-buddy/ide-toolbox": "^2.4.0", "@react-buddy/ide-toolbox": "^2.4.0",
"@react-buddy/palette-antd": "^5.3.0", "@react-buddy/palette-antd": "^5.3.0",
"antd": "^5.23.1", "antd": "^5.23.1",
"antd-mask-input": "^2.0.7",
"axios": "^1.7.9", "axios": "^1.7.9",
"moment": "^2.30.1", "dayjs": "^1.11.13",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^7.1.1" "react-router-dom": "^7.1.1",
"validator": "^13.12.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",
@ -1955,6 +1957,19 @@
"react-dom": ">=16.9.0" "react-dom": ">=16.9.0"
} }
}, },
"node_modules/antd-mask-input": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/antd-mask-input/-/antd-mask-input-2.0.7.tgz",
"integrity": "sha512-EFv/Hjar5nzYuzvgCcIeEcbVzw5+4RynUQTl2S8bLMaR9iajiWoeV/VYvENaP6X+ytnM0OqemE2pHY9ZRR6hEw==",
"license": "MIT",
"dependencies": {
"imask": "6.4.2"
},
"peerDependencies": {
"antd": ">=4.19.0",
"react": ">=16.0.0"
}
},
"node_modules/argparse": { "node_modules/argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@ -3361,6 +3376,15 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/imask": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/imask/-/imask-6.4.2.tgz",
"integrity": "sha512-xvEgbTdk6y2dW2UAysq0NRPmO6PuaXM5NHIt4TXEJEwXUHj26M0p/fXqyrSJdNXFaGVOtqYjPRnNdrjQQhDuuA==",
"license": "MIT",
"engines": {
"npm": ">=4.0.0"
}
},
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.0.3", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
@ -4012,6 +4036,8 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": "*" "node": "*"
} }
@ -5766,6 +5792,15 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/validator": {
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
"integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "6.0.7", "version": "6.0.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz",

View File

@ -14,12 +14,14 @@
"@react-buddy/ide-toolbox": "^2.4.0", "@react-buddy/ide-toolbox": "^2.4.0",
"@react-buddy/palette-antd": "^5.3.0", "@react-buddy/palette-antd": "^5.3.0",
"antd": "^5.23.1", "antd": "^5.23.1",
"antd-mask-input": "^2.0.7",
"axios": "^1.7.9", "axios": "^1.7.9",
"moment": "^2.30.1", "dayjs": "^1.11.13",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^7.1.1" "react-router-dom": "^7.1.1",
"validator": "^13.12.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",

View File

@ -0,0 +1,25 @@
import axios from "axios";
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: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.response?.status === 401) {
throw new Error("Ошибка авторизации: пользователь не найден или токен недействителен");
}
throw new Error(error.message);
}
}
export default updatePatient;

View File

@ -1,8 +1,10 @@
import {useEffect} from "react"; import {useEffect} from "react";
import {Modal, Form, Input, DatePicker} from "antd"; import {Modal, Form, Input, DatePicker} from "antd";
import moment from "moment";
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 {MaskedInput} from "antd-mask-input";
import dayjs from "dayjs";
const {TextArea} = Input; const {TextArea} = Input;
@ -11,16 +13,15 @@ const PatientModal = ({visible, onCancel, onSubmit, patient}) => {
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
form.resetFields();
if (patient) { if (patient) {
form.setFieldsValue({ form.setFieldsValue({
...patient, ...patient,
birthday: patient.birthday ? moment(patient.birthday) : null, birthday: patient.birthday ? dayjs(patient.birthday, "YYYY-MM-DD") : null,
}); });
} else {
form.resetFields();
} }
} }
}, [visible, patient, form]); }, [visible, patient]);
const handleOk = async () => { const handleOk = async () => {
try { try {
@ -49,7 +50,6 @@ const PatientModal = ({visible, onCancel, onSubmit, patient}) => {
centered centered
maskClosable={false} maskClosable={false}
forceRender={true} forceRender={true}
styles={{body: {padding: 24}}}
style={{top: 20}} style={{top: 20}}
> >
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
@ -89,15 +89,27 @@ const PatientModal = ({visible, onCancel, onSubmit, patient}) => {
<Form.Item <Form.Item
name="email" name="email"
label="Email" label="Email"
rules={[{type: "email", message: "Введите корректный email"}]} rules={[
{
validator: (_, value) => {
if (value && !validator.isEmail(value)) {
return Promise.reject("Некорректный email");
}
return Promise.resolve();
},
},
]}
> >
<Input placeholder="Введите email"/> <Input placeholder="Введите email"/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="phone" name="phone"
label="Телефон" label="Телефон"
rules={[
{required: true, message: "Введите телефон"},
]}
> >
<Input placeholder="Введите телефон"/> <MaskedInput placeholder="Введите номер телефона" mask="+7 (000) 000-00-00"/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="diagnosis" name="diagnosis"

View File

@ -1,15 +1,16 @@
import { useEffect, useState } from "react"; import {useEffect, useState} from "react";
import { Input, Select, List, FloatButton, Row, Col, message } from "antd"; import {Input, Select, List, FloatButton, Row, Col, message} from "antd";
import { PlusOutlined } from "@ant-design/icons"; import {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";
import PatientListCard from "../components/PatientListCard.jsx"; import PatientListCard from "../components/PatientListCard.jsx";
import PatientModal from "../components/PatientModal.jsx"; // Подключаем модальное окно import PatientModal from "../components/PatientModal.jsx";
import updatePatient from "../api/patients/UpdatePatient.jsx"; // Подключаем модальное окно
const { Option } = Select; const {Option} = Select;
const PatientsPage = () => { const PatientsPage = () => {
const { user } = useAuth(); const {user} = useAuth();
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [sortOrder, setSortOrder] = useState("asc"); const [sortOrder, setSortOrder] = useState("asc");
const [patients, setPatients] = useState([]); const [patients, setPatients] = useState([]);
@ -22,6 +23,13 @@ const PatientsPage = () => {
const [selectedPatient, setSelectedPatient] = useState(null); const [selectedPatient, setSelectedPatient] = useState(null);
useEffect(() => { useEffect(() => {
if (!isModalVisible) {
const intervalId = setInterval(fetchPatients, 5000);
return () => clearInterval(intervalId);
}
}, [user, isModalVisible]);
const fetchPatients = async () => { const fetchPatients = async () => {
if (!user || !user.token) return; if (!user || !user.token) return;
@ -33,9 +41,6 @@ const PatientsPage = () => {
} }
}; };
fetchPatients();
}, [user]);
const filteredPatients = patients const filteredPatients = patients
.filter((patient) => .filter((patient) =>
`${patient.first_name} ${patient.last_name}`.toLowerCase().includes(searchText.toLowerCase()) `${patient.first_name} ${patient.last_name}`.toLowerCase().includes(searchText.toLowerCase())
@ -62,16 +67,22 @@ const PatientsPage = () => {
setIsModalVisible(false); setIsModalVisible(false);
}; };
const handleSubmit = (newPatient) => { const handleSubmit = async (newPatient) => {
if (selectedPatient) { if (selectedPatient) {
setPatients((prevPatients) =>
prevPatients.map((p) => try {
p.id === selectedPatient.id ? { ...p, ...newPatient } : p await updatePatient(user.token, selectedPatient.id, newPatient);
) } catch (error) {
); if (error.response?.status === 401) {
message.success("Пациент успешно обновлен!"); throw new Error("Ошибка авторизации: пользователь неяден или токен недействителен");
} else { }
setPatients((prevPatients) => [...prevPatients, { id: Date.now(), ...newPatient }]); throw new Error(error.message);
}
}
if (!selectedPatient) {
setPatients((prevPatients) => [...prevPatients, {id: Date.now(), ...newPatient}]);
message.success("Пациент успешно добавлен!"); message.success("Пациент успешно добавлен!");
} }
@ -79,20 +90,20 @@ const PatientsPage = () => {
}; };
return ( return (
<div style={{ padding: 20 }}> <div style={{padding: 20}}>
<Row gutter={[16, 16]} style={{ marginBottom: 20 }}> <Row gutter={[16, 16]} style={{marginBottom: 20}}>
<Col xs={24} sm={16}> <Col xs={24} sm={16}>
<Input <Input
placeholder="Поиск пациента" placeholder="Поиск пациента"
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
style={{ width: "100%" }} style={{width: "100%"}}
/> />
</Col> </Col>
<Col xs={24} sm={8}> <Col xs={24} sm={8}>
<Select <Select
value={sortOrder} value={sortOrder}
onChange={(value) => setSortOrder(value)} onChange={(value) => setSortOrder(value)}
style={{ width: "100%" }} style={{width: "100%"}}
> >
<Option value="asc">А-Я</Option> <Option value="asc">А-Я</Option>
<Option value="desc">Я-А</Option> <Option value="desc">Я-А</Option>
@ -101,7 +112,7 @@ const PatientsPage = () => {
</Row> </Row>
<List <List
grid={{ gutter: 16, column: 1 }} grid={{gutter: 16, column: 1}}
dataSource={filteredPatients} dataSource={filteredPatients}
renderItem={(patient) => ( renderItem={(patient) => (
<List.Item <List.Item
@ -125,8 +136,8 @@ const PatientsPage = () => {
/> />
<FloatButton <FloatButton
icon={<PlusOutlined />} icon={<PlusOutlined/>}
style={{ position: "fixed", bottom: 20, right: 20 }} style={{position: "fixed", bottom: 20, right: 20}}
onClick={handleAddPatient} onClick={handleAddPatient}
/> />
@ -138,6 +149,7 @@ const PatientsPage = () => {
/> />
</div> </div>
); );
}; }
;
export default PatientsPage; export default PatientsPage;