feat: Пациенты: Добавлена пагинация на backend и frontend

This commit is contained in:
Андрей Дувакин 2025-06-07 14:48:45 +05:00
parent 01a27978e6
commit 67fa9db57a
8 changed files with 70 additions and 57 deletions

View File

@ -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)

View File

@ -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(

View 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

View File

@ -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)

View File

@ -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) => ({

View File

@ -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>

View File

@ -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,

View File

@ -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,