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

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,27 +1,35 @@
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([]);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [current, setCurrent] = useState(1); const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(10); 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 () => { const fetchPatients = async () => {
if (!user || !user.token) return; if (!user || !user.token) return;
@ -33,111 +41,115 @@ const PatientsPage = () => {
} }
}; };
fetchPatients(); const filteredPatients = patients
}, [user]); .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 const handleAddPatient = () => {
.filter((patient) => setSelectedPatient(null);
`${patient.first_name} ${patient.last_name}`.toLowerCase().includes(searchText.toLowerCase()) setIsModalVisible(true);
) };
.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 = () => { const handleEditPatient = (patient) => {
setSelectedPatient(null); setSelectedPatient(patient);
setIsModalVisible(true); setIsModalVisible(true);
}; };
const handleEditPatient = (patient) => { const handleCancel = () => {
setSelectedPatient(patient); setIsModalVisible(false);
setIsModalVisible(true); };
};
const handleCancel = () => { const handleSubmit = async (newPatient) => {
setIsModalVisible(false); if (selectedPatient) {
};
const handleSubmit = (newPatient) => { try {
if (selectedPatient) { await updatePatient(user.token, selectedPatient.id, newPatient);
setPatients((prevPatients) => } catch (error) {
prevPatients.map((p) => if (error.response?.status === 401) {
p.id === selectedPatient.id ? { ...p, ...newPatient } : p throw new Error("Ошибка авторизации: пользователь неяден или токен недействителен");
) }
); throw new Error(error.message);
message.success("Пациент успешно обновлен!"); }
} else { }
setPatients((prevPatients) => [...prevPatients, { id: Date.now(), ...newPatient }]);
message.success("Пациент успешно добавлен!");
}
setIsModalVisible(false);
};
return ( if (!selectedPatient) {
<div style={{ padding: 20 }}> setPatients((prevPatients) => [...prevPatients, {id: Date.now(), ...newPatient}]);
<Row gutter={[16, 16]} style={{ marginBottom: 20 }}> message.success("Пациент успешно добавлен!");
<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 setIsModalVisible(false);
grid={{ gutter: 16, column: 1 }} };
dataSource={filteredPatients}
renderItem={(patient) => ( return (
<List.Item <div style={{padding: 20}}>
onClick={() => { <Row gutter={[16, 16]} style={{marginBottom: 20}}>
handleEditPatient(patient); <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 <FloatButton
icon={<PlusOutlined />} icon={<PlusOutlined/>}
style={{ position: "fixed", bottom: 20, right: 20 }} style={{position: "fixed", bottom: 20, right: 20}}
onClick={handleAddPatient} onClick={handleAddPatient}
/> />
<PatientModal <PatientModal
visible={isModalVisible} visible={isModalVisible}
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={handleSubmit} onSubmit={handleSubmit}
patient={selectedPatient} patient={selectedPatient}
/> />
</div> </div>
); );
}; }
;
export default PatientsPage; export default PatientsPage;