feat: Пациенты: Добавлена пагинация на backend и frontend
This commit is contained in:
parent
01a27978e6
commit
67fa9db57a
@ -1,6 +1,6 @@
|
|||||||
from typing import Sequence, Optional
|
from typing import Sequence, Optional, Tuple
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, func
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.domain.models import Patient
|
from app.domain.models import Patient
|
||||||
@ -10,10 +10,16 @@ class PatientsRepository:
|
|||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
async def get_all(self) -> Sequence[Patient]:
|
async def get_all(self, skip: int = 0, limit: int = 10) -> Tuple[Sequence[Patient], int]:
|
||||||
stmt = select(Patient)
|
stmt = select(Patient).offset(skip).limit(limit)
|
||||||
result = await self.db.execute(stmt)
|
result = await self.db.execute(stmt)
|
||||||
return result.scalars().all()
|
patients = result.scalars().all()
|
||||||
|
|
||||||
|
count_stmt = select(func.count()).select_from(Patient)
|
||||||
|
count_result = await self.db.execute(count_stmt)
|
||||||
|
total_count = count_result.scalar()
|
||||||
|
|
||||||
|
return patients, total_count
|
||||||
|
|
||||||
async def get_by_id(self, patient_id: int) -> Optional[Patient]:
|
async def get_by_id(self, patient_id: int) -> Optional[Patient]:
|
||||||
stmt = select(Patient).filter_by(id=patient_id)
|
stmt = select(Patient).filter_by(id=patient_id)
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, Query
|
||||||
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.domain.entities.patient import PatientEntity
|
||||||
|
from app.domain.entities.responses.paginated_patient import PaginatedPatientsResponseEntity
|
||||||
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
|
||||||
|
|
||||||
@ -11,16 +12,19 @@ router = APIRouter()
|
|||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/",
|
"/",
|
||||||
response_model=list[PatientEntity],
|
response_model=PaginatedPatientsResponseEntity,
|
||||||
summary="Get all patients",
|
summary="Get all patients with pagination",
|
||||||
description="Returns a list of all patients",
|
description="Returns a paginated list of patients and total count",
|
||||||
)
|
)
|
||||||
async def get_all_patients(
|
async def get_all_patients(
|
||||||
|
page: int = Query(1, ge=1, description="Page number"),
|
||||||
|
page_size: int = Query(10, ge=1, le=100, description="Number of patients per page"),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
patients_service = PatientsService(db)
|
patients_service = PatientsService(db)
|
||||||
return await patients_service.get_all_patients()
|
patients, total_count = await patients_service.get_all_patients(page, page_size)
|
||||||
|
return {"patients": patients, "total_count": total_count}
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
|||||||
8
api/app/domain/entities/responses/paginated_patient.py
Normal file
8
api/app/domain/entities/responses/paginated_patient.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from app.domain.entities.patient import PatientEntity
|
||||||
|
|
||||||
|
|
||||||
|
class PaginatedPatientsResponseEntity(BaseModel):
|
||||||
|
patients: list[PatientEntity]
|
||||||
|
total_count: int
|
||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Optional
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@ -13,12 +13,13 @@ class PatientsService:
|
|||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.patient_repository = PatientsRepository(db)
|
self.patient_repository = PatientsRepository(db)
|
||||||
|
|
||||||
async def get_all_patients(self) -> list[PatientEntity]:
|
async def get_all_patients(self, page: int = 1, page_size: int = 10) -> Tuple[list[PatientEntity], int]:
|
||||||
patients = await self.patient_repository.get_all()
|
skip = (page - 1) * page_size
|
||||||
return [
|
patients, total_count = await self.patient_repository.get_all(skip=skip, limit=page_size)
|
||||||
self.model_to_entity(patient)
|
return (
|
||||||
for patient in patients
|
[self.model_to_entity(patient) for patient in patients],
|
||||||
]
|
total_count
|
||||||
|
)
|
||||||
|
|
||||||
async def create_patient(self, patient: PatientEntity) -> PatientEntity:
|
async def create_patient(self, patient: PatientEntity) -> PatientEntity:
|
||||||
patient_model = self.entity_to_model(patient)
|
patient_model = self.entity_to_model(patient)
|
||||||
|
|||||||
@ -7,9 +7,11 @@ export const patientsApi = createApi({
|
|||||||
tagTypes: ['Patient'],
|
tagTypes: ['Patient'],
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
getPatients: builder.query({
|
getPatients: builder.query({
|
||||||
query: () => '/patients/',
|
query: ({ page, pageSize }) => ({
|
||||||
providesTags: ['Patient'],
|
url: '/patients/',
|
||||||
refetchOnMountOrArgChange: 5
|
params: { page, page_size: pageSize },
|
||||||
|
}),
|
||||||
|
providesTags: ['Patients'],
|
||||||
}),
|
}),
|
||||||
addPatient: builder.mutation({
|
addPatient: builder.mutation({
|
||||||
query: (patient) => ({
|
query: (patient) => ({
|
||||||
|
|||||||
@ -25,39 +25,36 @@ import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.js
|
|||||||
import usePatients from "./usePatients.js";
|
import usePatients from "./usePatients.js";
|
||||||
import usePatientsUI from "./usePatientsUI.js";
|
import usePatientsUI from "./usePatientsUI.js";
|
||||||
|
|
||||||
const {Option} = Select;
|
const { Option } = Select;
|
||||||
const {Title} = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
const PatientsPage = () => {
|
const PatientsPage = () => {
|
||||||
const patientsData = usePatients();
|
const patientsData = usePatients();
|
||||||
const patientsUI = usePatientsUI(patientsData.patients);
|
const patientsUI = usePatientsUI(patientsData.patients, patientsData.totalCount);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: "Фамилия",
|
title: "Фамилия",
|
||||||
dataIndex: "last_name",
|
dataIndex: "last_name",
|
||||||
key: "last_name",
|
key: "last_name",
|
||||||
sorter: (a, b) => a.last_name.localeCompare(b.last_name),
|
sorter: true, // Сортировка будет на сервере, если добавить
|
||||||
sortDirections: ["ascend", "descend"],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Имя",
|
title: "Имя",
|
||||||
dataIndex: "first_name",
|
dataIndex: "first_name",
|
||||||
key: "first_name",
|
key: "first_name",
|
||||||
sorter: (a, b) => a.first_name.localeCompare(b.first_name),
|
sorter: true,
|
||||||
sortDirections: ["ascend", "descend"],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Отчество",
|
title: "Отчество",
|
||||||
dataIndex: "patronymic",
|
dataIndex: "patronymic",
|
||||||
key: "patronymic",
|
key: "patronymic",
|
||||||
sortDirections: ["ascend", "descend"],
|
sorter: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Дата рождения",
|
title: "Дата рождения",
|
||||||
dataIndex: "birthday",
|
dataIndex: "birthday",
|
||||||
sorter: (a, b) => new Date(a.birthday).getTime() - new Date(b.birthday).getTime(),
|
sorter: true,
|
||||||
sortDirections: ["ascend", "descend"],
|
|
||||||
render: patientsUI.formatDate,
|
render: patientsUI.formatDate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -76,7 +73,6 @@ const PatientsPage = () => {
|
|||||||
<Col xs={24} xl={12}>
|
<Col xs={24} xl={12}>
|
||||||
<Button block onClick={() => patientsUI.handleEditPatient(record)}>Изменить</Button>
|
<Button block onClick={() => patientsUI.handleEditPatient(record)}>Изменить</Button>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} xl={12}>
|
<Col xs={24} xl={12}>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="Вы уверены, что хотите удалить пациента?"
|
title="Вы уверены, что хотите удалить пациента?"
|
||||||
@ -113,6 +109,8 @@ const PatientsPage = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(patientsData.isLoading)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={patientsUI.containerStyle}>
|
<div style={patientsUI.containerStyle}>
|
||||||
<Title level={1}><TeamOutlined/> Пациенты</Title>
|
<Title level={1}><TeamOutlined/> Пациенты</Title>
|
||||||
@ -126,7 +124,6 @@ const PatientsPage = () => {
|
|||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{patientsUI.viewMode === "tile" && (
|
{patientsUI.viewMode === "tile" && (
|
||||||
<Col xs={24} md={5} sm={6} xl={3} xxl={2}>
|
<Col xs={24} md={5} sm={6} xl={3} xxl={2}>
|
||||||
<Tooltip title={"Сортировка пациентов"}>
|
<Tooltip title={"Сортировка пациентов"}>
|
||||||
@ -141,7 +138,6 @@ const PatientsPage = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Col xs={24} md={patientsUI.viewMode === "tile" ? 5 : 10}
|
<Col xs={24} md={patientsUI.viewMode === "tile" ? 5 : 10}
|
||||||
sm={patientsUI.viewMode === "tile" ? 8 : 14}
|
sm={patientsUI.viewMode === "tile" ? 8 : 14}
|
||||||
xl={patientsUI.viewMode === "tile" ? 3 : 5}
|
xl={patientsUI.viewMode === "tile" ? 3 : 5}
|
||||||
@ -158,7 +154,7 @@ const PatientsPage = () => {
|
|||||||
|
|
||||||
{patientsData.isLoading ? <LoadingIndicator/> : patientsUI.viewMode === "tile" ? (
|
{patientsData.isLoading ? <LoadingIndicator/> : patientsUI.viewMode === "tile" ? (
|
||||||
<List
|
<List
|
||||||
grid={{gutter: 16, column: 3}}
|
grid={{ gutter: 16, column: 3 }}
|
||||||
dataSource={patientsUI.filteredPatients}
|
dataSource={patientsUI.filteredPatients}
|
||||||
renderItem={patient => (
|
renderItem={patient => (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
|
|||||||
@ -1,13 +1,21 @@
|
|||||||
import {notification} from "antd";
|
import { notification } from "antd";
|
||||||
import {
|
import {
|
||||||
useDeletePatientMutation,
|
useDeletePatientMutation,
|
||||||
useGetPatientsQuery,
|
useGetPatientsQuery,
|
||||||
} from "../../../Api/patientsApi.js";
|
} from "../../../Api/patientsApi.js";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
const usePatients = () => {
|
const usePatients = () => {
|
||||||
const {data: patients = [], isLoading, isError} = useGetPatientsQuery(undefined, {
|
const { currentPage, pageSize } = useSelector(state => state.patientsUI);
|
||||||
pollingInterval: 20000,
|
|
||||||
});
|
const { data = { patients: [], total_count: 0 }, isLoading, isError } = useGetPatientsQuery(
|
||||||
|
{ page: currentPage, pageSize },
|
||||||
|
{
|
||||||
|
pollingInterval: 20000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
const [deletePatient] = useDeletePatientMutation();
|
const [deletePatient] = useDeletePatientMutation();
|
||||||
|
|
||||||
@ -29,7 +37,8 @@ const usePatients = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
patients,
|
patients: data.patients,
|
||||||
|
totalCount: data.total_count,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
handleDeletePatient,
|
handleDeletePatient,
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
} from "../../../Redux/Slices/patientsSlice.js";
|
} from "../../../Redux/Slices/patientsSlice.js";
|
||||||
import { getCachedInfo } from "../../../Utils/cachedInfoUtils.js";
|
import { getCachedInfo } from "../../../Utils/cachedInfoUtils.js";
|
||||||
|
|
||||||
const usePatientsUI = (patients) => {
|
const usePatientsUI = (patients, totalCount) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const {
|
const {
|
||||||
searchText,
|
searchText,
|
||||||
@ -56,25 +56,12 @@ const usePatientsUI = (patients) => {
|
|||||||
dispatch(openModal());
|
dispatch(openModal());
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredPatients = useMemo(() => {
|
|
||||||
return patients
|
|
||||||
.filter(patient =>
|
|
||||||
Object.values(patient)
|
|
||||||
.filter(value => typeof value === "string")
|
|
||||||
.some(value => value.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);
|
|
||||||
});
|
|
||||||
}, [patients, searchText, sortOrder]);
|
|
||||||
|
|
||||||
const formatDate = (date) => new Date(date).toLocaleDateString();
|
const formatDate = (date) => new Date(date).toLocaleDateString();
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
currentPage: currentPage,
|
current: currentPage,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
|
total: totalCount,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
pageSizeOptions: ["5", "10", "20", "50"],
|
pageSizeOptions: ["5", "10", "20", "50"],
|
||||||
onChange: (page, newPageSize) => {
|
onChange: (page, newPageSize) => {
|
||||||
@ -94,7 +81,7 @@ const usePatientsUI = (patients) => {
|
|||||||
formItemStyle,
|
formItemStyle,
|
||||||
viewModIconStyle,
|
viewModIconStyle,
|
||||||
pagination,
|
pagination,
|
||||||
filteredPatients: filteredPatients.map(p => ({ ...p, key: p.id })),
|
filteredPatients: patients.map(p => ({ ...p, key: p.id })),
|
||||||
handleSetSearchText,
|
handleSetSearchText,
|
||||||
handleSetSortOrder,
|
handleSetSortOrder,
|
||||||
handleSetViewMode,
|
handleSetViewMode,
|
||||||
@ -106,4 +93,4 @@ const usePatientsUI = (patients) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default usePatientsUI;
|
export default usePatientsUI;
|
||||||
Loading…
x
Reference in New Issue
Block a user