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

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 app.database.session import get_db
from app.domain.entities.patient import PatientEntity
from app.infrastructure.dependencies import get_current_user
from app.infrastructure.patients_service import PatientsService
@ -15,3 +16,24 @@ async def get_all_patients(
):
patients_service = PatientsService(db)
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):
id: Optional[int]
id: Optional[int] = None
first_name: str
last_name: str
patronymic: Optional[str]
patronymic: Optional[str] = None
birthday: datetime.date
address: Optional[str]
email: Optional[str]
phone: Optional[str]
diagnosis: Optional[str]
correction: Optional[str]
address: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
diagnosis: Optional[str] = None
correction: Optional[str] = None

View File

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

View File

@ -12,12 +12,14 @@
"@react-buddy/ide-toolbox": "^2.4.0",
"@react-buddy/palette-antd": "^5.3.0",
"antd": "^5.23.1",
"antd-mask-input": "^2.0.7",
"axios": "^1.7.9",
"moment": "^2.30.1",
"dayjs": "^1.11.13",
"prop-types": "^15.8.1",
"react": "^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": {
"@eslint/js": "^9.17.0",
@ -1955,6 +1957,19 @@
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@ -3361,6 +3376,15 @@
"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": {
"version": "5.0.3",
"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",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": "*"
}
@ -5766,6 +5792,15 @@
"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": {
"version": "6.0.7",
"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/palette-antd": "^5.3.0",
"antd": "^5.23.1",
"antd-mask-input": "^2.0.7",
"axios": "^1.7.9",
"moment": "^2.30.1",
"dayjs": "^1.11.13",
"prop-types": "^15.8.1",
"react": "^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": {
"@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 {Modal, Form, Input, DatePicker} from "antd";
import moment from "moment";
import PropTypes from "prop-types";
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;
@ -11,16 +13,15 @@ const PatientModal = ({visible, onCancel, onSubmit, patient}) => {
useEffect(() => {
if (visible) {
form.resetFields();
if (patient) {
form.setFieldsValue({
...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 () => {
try {
@ -49,7 +50,6 @@ const PatientModal = ({visible, onCancel, onSubmit, patient}) => {
centered
maskClosable={false}
forceRender={true}
styles={{body: {padding: 24}}}
style={{top: 20}}
>
<Form form={form} layout="vertical">
@ -89,15 +89,27 @@ const PatientModal = ({visible, onCancel, onSubmit, patient}) => {
<Form.Item
name="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"/>
</Form.Item>
<Form.Item
name="phone"
label="Телефон"
rules={[
{required: true, message: "Введите телефон"},
]}
>
<Input placeholder="Введите телефон"/>
<MaskedInput placeholder="Введите номер телефона" mask="+7 (000) 000-00-00"/>
</Form.Item>
<Form.Item
name="diagnosis"

View File

@ -1,27 +1,35 @@
import { useEffect, useState } from "react";
import { Input, Select, List, FloatButton, Row, Col, message } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import { useAuth } from "../AuthContext.jsx";
import {useEffect, useState} from "react";
import {Input, Select, List, FloatButton, Row, Col, message} from "antd";
import {PlusOutlined} from "@ant-design/icons";
import {useAuth} from "../AuthContext.jsx";
import getAllPatients from "../api/patients/GetAllPatients.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 { user } = useAuth();
const [searchText, setSearchText] = useState("");
const [sortOrder, setSortOrder] = useState("asc");
const [patients, setPatients] = useState([]);
const [error, setError] = useState(null);
const {user} = useAuth();
const [searchText, setSearchText] = useState("");
const [sortOrder, setSortOrder] = useState("asc");
const [patients, setPatients] = useState([]);
const [error, setError] = useState(null);
const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedPatient, setSelectedPatient] = useState(null);
useEffect(() => {
if (!isModalVisible) {
const intervalId = setInterval(fetchPatients, 5000);
return () => clearInterval(intervalId);
}
}, [user, isModalVisible]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedPatient, setSelectedPatient] = useState(null);
useEffect(() => {
const fetchPatients = async () => {
if (!user || !user.token) return;
@ -33,111 +41,115 @@ const PatientsPage = () => {
}
};
fetchPatients();
}, [user]);
const filteredPatients = patients
.filter((patient) =>
`${patient.first_name} ${patient.last_name}`.toLowerCase().includes(searchText.toLowerCase())
)
.sort((a, b) => {
const fullNameA = `${a.last_name} ${a.first_name}`;
const fullNameB = `${b.last_name} ${b.first_name}`;
return sortOrder === "asc"
? fullNameA.localeCompare(fullNameB)
: fullNameB.localeCompare(fullNameA);
});
const filteredPatients = patients
.filter((patient) =>
`${patient.first_name} ${patient.last_name}`.toLowerCase().includes(searchText.toLowerCase())
)
.sort((a, b) => {
const fullNameA = `${a.last_name} ${a.first_name}`;
const fullNameB = `${b.last_name} ${b.first_name}`;
return sortOrder === "asc"
? fullNameA.localeCompare(fullNameB)
: fullNameB.localeCompare(fullNameA);
});
const handleAddPatient = () => {
setSelectedPatient(null);
setIsModalVisible(true);
};
const handleAddPatient = () => {
setSelectedPatient(null);
setIsModalVisible(true);
};
const handleEditPatient = (patient) => {
setSelectedPatient(patient);
setIsModalVisible(true);
};
const handleEditPatient = (patient) => {
setSelectedPatient(patient);
setIsModalVisible(true);
};
const handleCancel = () => {
setIsModalVisible(false);
};
const handleCancel = () => {
setIsModalVisible(false);
};
const handleSubmit = async (newPatient) => {
if (selectedPatient) {
const handleSubmit = (newPatient) => {
if (selectedPatient) {
setPatients((prevPatients) =>
prevPatients.map((p) =>
p.id === selectedPatient.id ? { ...p, ...newPatient } : p
)
);
message.success("Пациент успешно обновлен!");
} else {
setPatients((prevPatients) => [...prevPatients, { id: Date.now(), ...newPatient }]);
message.success("Пациент успешно добавлен!");
}
try {
await updatePatient(user.token, selectedPatient.id, newPatient);
} catch (error) {
if (error.response?.status === 401) {
throw new Error("Ошибка авторизации: пользователь неяден или токен недействителен");
}
throw new Error(error.message);
}
}
setIsModalVisible(false);
};
return (
<div style={{ padding: 20 }}>
<Row gutter={[16, 16]} style={{ marginBottom: 20 }}>
<Col xs={24} sm={16}>
<Input
placeholder="Поиск пациента"
onChange={(e) => setSearchText(e.target.value)}
style={{ width: "100%" }}
/>
</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>
</Row>
if (!selectedPatient) {
setPatients((prevPatients) => [...prevPatients, {id: Date.now(), ...newPatient}]);
message.success("Пациент успешно добавлен!");
}
<List
grid={{ gutter: 16, column: 1 }}
dataSource={filteredPatients}
renderItem={(patient) => (
<List.Item
onClick={() => {
handleEditPatient(patient);
setIsModalVisible(false);
};
return (
<div style={{padding: 20}}>
<Row gutter={[16, 16]} style={{marginBottom: 20}}>
<Col xs={24} sm={16}>
<Input
placeholder="Поиск пациента"
onChange={(e) => setSearchText(e.target.value)}
style={{width: "100%"}}
/>
</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>
</Row>
<List
grid={{gutter: 16, column: 1}}
dataSource={filteredPatients}
renderItem={(patient) => (
<List.Item
onClick={() => {
handleEditPatient(patient);
}}
>
<PatientListCard patient={patient}/>
</List.Item>
)}
pagination={{
current,
pageSize,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => {
setCurrent(page);
setPageSize(newPageSize);
},
}}
>
<PatientListCard patient={patient}/>
</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={handleAddPatient}
/>
<FloatButton
icon={<PlusOutlined/>}
style={{position: "fixed", bottom: 20, right: 20}}
onClick={handleAddPatient}
/>
<PatientModal
visible={isModalVisible}
onCancel={handleCancel}
onSubmit={handleSubmit}
patient={selectedPatient}
/>
</div>
);
};
<PatientModal
visible={isModalVisible}
onCancel={handleCancel}
onSubmit={handleSubmit}
patient={selectedPatient}
/>
</div>
);
}
;
export default PatientsPage;