feat: IssuesPage

Добавлены фильтрация, пагинация и поиск.
Улучшен UI.
Удален useIssuesUI.js.
Добавлен PaginatedLensIssuesResponseEntity.
This commit is contained in:
Андрей Дувакин 2025-06-08 10:14:41 +05:00
parent 914ae0528f
commit 7a2ef98fd5
14 changed files with 425 additions and 272 deletions

View File

@ -1,26 +1,78 @@
from typing import Optional, Sequence from typing import Optional, Sequence, Tuple, Literal
from sqlalchemy import select, desc from sqlalchemy import select, desc, or_, func, asc
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from app.domain.models import LensIssue from app.domain.models import LensIssue, Patient, User
class LensIssuesRepository: class LensIssuesRepository:
def __init__(self, db: AsyncSession): def __init__(self, db: AsyncSession):
self.db = db self.db = db
async def get_all(self) -> Sequence[LensIssue]: async def get_all(
self,
skip: int = 0,
limit: int = 10,
search: Optional[str] = None,
sort_order: Literal["asc", "desc"] = "desc",
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> Tuple[Sequence[LensIssue], int]:
stmt = ( stmt = (
select(LensIssue) select(LensIssue)
.options(joinedload(LensIssue.lens)) .options(joinedload(LensIssue.lens))
.options(joinedload(LensIssue.patient)) .options(joinedload(LensIssue.patient))
.options(joinedload(LensIssue.doctor)) .options(joinedload(LensIssue.doctor))
.order_by(desc(LensIssue.issue_date)) .join(Patient)
.join(User)
) )
if search:
search = f"%{search}%"
stmt = stmt.filter(
or_(
Patient.last_name.ilike(search),
Patient.first_name.ilike(search),
User.last_name.ilike(search),
User.first_name.ilike(search)
)
)
if start_date:
stmt = stmt.filter(LensIssue.issue_date >= start_date)
if end_date:
stmt = stmt.filter(LensIssue.issue_date <= end_date)
stmt = stmt.order_by(
desc(LensIssue.issue_date) if sort_order == "desc" else asc(LensIssue.issue_date)
)
count_stmt = select(func.count()).select_from(LensIssue).join(Patient).join(User)
if search:
search = f"%{search}%"
count_stmt = count_stmt.filter(
or_(
Patient.last_name.ilike(search),
Patient.first_name.ilike(search),
User.last_name.ilike(search),
User.first_name.ilike(search)
)
)
if start_date:
count_stmt = count_stmt.filter(LensIssue.issue_date >= start_date)
if end_date:
count_stmt = count_stmt.filter(LensIssue.issue_date <= end_date)
stmt = stmt.offset(skip).limit(limit)
result = await self.db.execute(stmt) result = await self.db.execute(stmt)
return result.scalars().all() issues = result.scalars().all()
count_result = await self.db.execute(count_stmt)
total_count = count_result.scalar()
return issues, total_count
async def get_by_id(self, lens_issue_id: int) -> Optional[LensIssue]: async def get_by_id(self, lens_issue_id: int) -> Optional[LensIssue]:
stmt = select(LensIssue).filter_by(id=lens_issue_id) stmt = select(LensIssue).filter_by(id=lens_issue_id)

View File

@ -10,8 +10,8 @@ class PatientsRepository:
def __init__(self, db: AsyncSession): def __init__(self, db: AsyncSession):
self.db = db self.db = db
async def get_all(self, skip: int = 0, limit: int = 10, search: str = None, async def get_all(self, skip: int = 0, limit: int = 100, search: str = None,
sort_order: Literal["asc", "desc"] = "asc") -> Tuple[Sequence[Patient], int]: sort_order: Literal["asc", "desc"] = "asc", all_params: bool = False) -> Tuple[Sequence[Patient], int]:
stmt = select(Patient) stmt = select(Patient)
if search: if search:
@ -50,6 +50,12 @@ class PatientsRepository:
count_result = await self.db.execute(count_stmt) count_result = await self.db.execute(count_stmt)
total_count = count_result.scalar() total_count = count_result.scalar()
if not all_params:
stmt = stmt.offset(skip).limit(limit)
result = await self.db.execute(stmt)
patients = result.scalars().all()
return patients, total_count 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]:

View File

@ -1,8 +1,11 @@
from fastapi import APIRouter, Depends from typing import Optional, Literal
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.lens_issues import LensIssueEntity from app.domain.entities.lens_issues import LensIssueEntity
from app.domain.entities.responses.paginated_issue import PaginatedLensIssuesResponseEntity
from app.infrastructure.dependencies import get_current_user from app.infrastructure.dependencies import get_current_user
from app.infrastructure.lens_issues_service import LensIssuesService from app.infrastructure.lens_issues_service import LensIssuesService
@ -11,17 +14,34 @@ router = APIRouter()
@router.get( @router.get(
"/", "/",
response_model=list[LensIssueEntity], response_model=PaginatedLensIssuesResponseEntity,
summary="Get all lens issues", summary="Get all lens issues",
description="Returns a list of all lens issues", description="Returns a paginated list of lens issues with optional filtering and sorting",
) )
async def get_all_lens_issues( async def get_all_lens_issues(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
search: Optional[str] = Query(None),
sort_order: Literal["asc", "desc"] = Query("desc"),
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(get_current_user), user=Depends(get_current_user),
): ):
lens_issues_service = LensIssuesService(db) lens_issues_service = LensIssuesService(db)
return await lens_issues_service.get_all_lens_issues() skip = (page - 1) * page_size
issues, total_count = await lens_issues_service.get_all_lens_issues(
skip=skip,
limit=page_size,
search=search,
sort_order=sort_order,
start_date=start_date,
end_date=end_date
)
return PaginatedLensIssuesResponseEntity(
issues=issues,
total_count=total_count
)
@router.post( @router.post(
"/", "/",

View File

@ -23,11 +23,12 @@ async def get_all_patients(
page_size: int = Query(10, ge=1, le=100, description="Number of patients per page"), page_size: int = Query(10, ge=1, le=100, description="Number of patients per page"),
search: str = Query(None, description="Search term for filtering patients"), search: str = Query(None, description="Search term for filtering patients"),
sort_order: Literal["asc", "desc"] = Query("asc", description="Sort order by first_name (asc or desc)"), sort_order: Literal["asc", "desc"] = Query("asc", description="Sort order by first_name (asc or desc)"),
all_params: bool = Query(False, description="Get all patients"),
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)
patients, total_count = await patients_service.get_all_patients(page, page_size, search, sort_order) patients, total_count = await patients_service.get_all_patients(page, page_size, search, sort_order, all_params)
return {"patients": patients, "total_count": total_count} return {"patients": patients, "total_count": total_count}

View File

@ -0,0 +1,8 @@
from pydantic import BaseModel
from app.domain.entities.lens_issues import LensIssueEntity
class PaginatedLensIssuesResponseEntity(BaseModel):
issues: list[LensIssueEntity]
total_count: int

View File

@ -1,4 +1,4 @@
from typing import Optional from typing import Optional, Literal, Tuple
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -22,13 +22,27 @@ class LensIssuesService:
self.users_repository = UsersRepository(db) self.users_repository = UsersRepository(db)
self.lenses_repository = LensesRepository(db) self.lenses_repository = LensesRepository(db)
async def get_all_lens_issues(self) -> list[LensIssueEntity]: async def get_all_lens_issues(
lens_issues = await self.lens_issues_repository.get_all() self,
skip: int = 0,
return [ limit: int = 10,
self.model_to_entity(lens_issue) search: Optional[str] = None,
for lens_issue in lens_issues sort_order: Literal["asc", "desc"] = "desc",
] start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> Tuple[list[LensIssueEntity], int]:
lens_issues, total_count = await self.lens_issues_repository.get_all(
skip=skip,
limit=limit,
search=search,
sort_order=sort_order,
start_date=start_date,
end_date=end_date
)
return (
[self.model_to_entity(lens_issue) for lens_issue in lens_issues],
total_count
)
async def create_lens_issue(self, lens_issue: LensIssueEntity, user_id: int) -> Optional[LensIssueEntity]: async def create_lens_issue(self, lens_issue: LensIssueEntity, user_id: int) -> Optional[LensIssueEntity]:
patient = await self.patient_repository.get_by_id(lens_issue.patient_id) patient = await self.patient_repository.get_by_id(lens_issue.patient_id)

View File

@ -13,10 +13,12 @@ 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, page: int = 1, page_size: int = 10, search: str = None, sort_order: Literal["asc", "desc"] = "asc") -> Tuple[ async def get_all_patients(self, page: int = 1, page_size: int = 10, search: str = None,
sort_order: Literal["asc", "desc"] = "asc", all_params: bool = False) -> Tuple[
list[PatientEntity], int]: list[PatientEntity], int]:
skip = (page - 1) * page_size skip = (page - 1) * page_size
patients, total_count = await self.patient_repository.get_all(skip=skip, limit=page_size, search=search, sort_order=sort_order) patients, total_count = await self.patient_repository.get_all(skip=skip, limit=page_size, search=search,
sort_order=sort_order, all_params=all_params)
return ( return (
[self.model_to_entity(patient) for patient in patients], [self.model_to_entity(patient) for patient in patients],
total_count total_count

View File

@ -8,9 +8,26 @@ export const lensIssuesApi = createApi({
tagTypes: ['LensIssues'], tagTypes: ['LensIssues'],
endpoints: (builder) => ({ endpoints: (builder) => ({
getLensIssues: builder.query({ getLensIssues: builder.query({
query: () => '/lens_issues/', query: ({ page, pageSize, search, sortOrder, startDate, endDate }) => ({
url: '/lens_issues/',
params: {
page,
page_size: pageSize,
search: search || undefined,
sort_order: sortOrder || undefined,
start_date: startDate || undefined,
end_date: endDate || undefined,
},
providesTags: ['LensIssues'], providesTags: ['LensIssues'],
refetchOnMountOrArgChange: 5 }),
providesTags: ['LensIssue'],
transformResponse: (response) => {
if (!response || !Array.isArray(response.issues)) {
console.warn('Unexpected lens issues API response:', response);
return { issues: [], total_count: 0 };
}
return response;
},
}), }),
addLensIssues: builder.mutation({ addLensIssues: builder.mutation({
query: (lensIssues) => ({ query: (lensIssues) => ({
@ -25,5 +42,5 @@ export const lensIssuesApi = createApi({
export const { export const {
useGetLensIssuesQuery, useGetLensIssuesQuery,
useAddLensIssuesMutation useAddLensIssuesMutation,
} = lensIssuesApi; } = lensIssuesApi;

View File

@ -18,6 +18,20 @@ export const patientsApi = createApi({
}), }),
providesTags: ['Patients'], providesTags: ['Patients'],
}), }),
getAllPatients: builder.query({
query: () => ({
url: '/patients/',
params: { all_params: true },
}),
providesTags: ['Patient'],
transformResponse: (response) => {
if (!response || !Array.isArray(response.patients)) {
console.warn('Unexpected patients API response:', response);
return [];
}
return response.patients;
},
}),
addPatient: builder.mutation({ addPatient: builder.mutation({
query: (patient) => ({ query: (patient) => ({
url: '/patients/', url: '/patients/',
@ -46,6 +60,7 @@ export const patientsApi = createApi({
export const { export const {
useGetPatientsQuery, useGetPatientsQuery,
useGetAllPatientsQuery,
useAddPatientMutation, useAddPatientMutation,
useUpdatePatientMutation, useUpdatePatientMutation,
useDeletePatientMutation, useDeletePatientMutation,

View File

@ -1,9 +1,9 @@
import {useGetPatientsQuery} from "../../../../../Api/patientsApi.js"; import {useGetAllPatientsQuery, useGetPatientsQuery} from "../../../../../Api/patientsApi.js";
import {useGetNotIssuedLensesQuery} from "../../../../../Api/lensesApi.js"; import {useGetNotIssuedLensesQuery} from "../../../../../Api/lensesApi.js";
const useLensIssueForm = () => { const useLensIssueForm = () => {
const {data: patients = [], isLoading: isLoadingPatients, isError: isErrorPatients} = useGetPatientsQuery(undefined, { const {data: patients = [], isLoading: isLoadingPatients, isError: isErrorPatients} = useGetAllPatientsQuery(undefined, {
pollingInterval: 10000, pollingInterval: 10000,
}); });
const {data: lenses = [], isLoading: isLoadingLenses, isError: isErrorLenses} = useGetNotIssuedLensesQuery(undefined, { const {data: lenses = [], isLoading: isLoadingLenses, isError: isErrorLenses} = useGetNotIssuedLensesQuery(undefined, {

View File

@ -10,7 +10,8 @@ import {
Typography, Typography,
Timeline, Timeline,
Grid, Grid,
Pagination, Result Pagination,
Result
} from "antd"; } from "antd";
import { DatabaseOutlined, PlusOutlined, UnorderedListOutlined } from "@ant-design/icons"; import { DatabaseOutlined, PlusOutlined, UnorderedListOutlined } from "@ant-design/icons";
import LensIssueViewModal from "./Components/LensIssueViewModal/LensIssueViewModal.jsx"; import LensIssueViewModal from "./Components/LensIssueViewModal/LensIssueViewModal.jsx";
@ -19,27 +20,24 @@ import LensIssueFormModal from "./Components/LensIssueFormModal/LensIssueFormMod
import SelectViewMode from "../../Widgets/SelectViewMode/SelectViewMode.jsx"; import SelectViewMode from "../../Widgets/SelectViewMode/SelectViewMode.jsx";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import useIssues from "./useIssues.js"; import useIssues from "./useIssues.js";
import useIssuesUI from "./useIssuesUI.js";
const { Title } = Typography; const { Title } = Typography;
const { useBreakpoint } = Grid; const { useBreakpoint } = Grid;
const IssuesPage = () => { const IssuesPage = () => {
const issuesData = useIssues(); const issuesData = useIssues();
const issuesUI = useIssuesUI(issuesData.issues);
const screens = useBreakpoint(); const screens = useBreakpoint();
const viewModes = [ const viewModes = [
{ {
value: "table", value: "table",
label: "Таблица", label: "Таблица",
icon: <DatabaseOutlined style={issuesUI.viewModIconStyle}/>, icon: <DatabaseOutlined style={issuesData.viewModIconStyle} />,
}, },
{ {
value: "timeline", value: "timeline",
label: "Лента", label: "Лента",
icon: <UnorderedListOutlined style={issuesUI.viewModIconStyle}/>, icon: <UnorderedListOutlined style={issuesData.viewModIconStyle} />,
}, },
]; ];
@ -73,7 +71,7 @@ const IssuesPage = () => {
title: "Действия", title: "Действия",
key: "actions", key: "actions",
render: (_, issue) => ( render: (_, issue) => (
<Button type={"link"} onClick={() => issuesUI.handleSelectIssue(issue)}>Подробнее</Button> <Button type="link" onClick={() => issuesData.handleSelectIssue(issue)}>Подробнее</Button>
), ),
}, },
]; ];
@ -81,30 +79,27 @@ const IssuesPage = () => {
const TableView = () => ( const TableView = () => (
<Table <Table
columns={columns} columns={columns}
dataSource={issuesUI.filteredIssues} dataSource={issuesData.issues}
rowKey="id" rowKey="id"
pagination={issuesUI.pagination} pagination={issuesData.pagination}
showSorterTooltip={false} showSorterTooltip={false}
/> />
); );
const timeLineItems = issuesUI.filteredIssues.map(issue => ({ const timeLineItems = issuesData.issues.map(issue => ({
label: dayjs(issue.issue_date).format("DD.MM.YYYY"), label: dayjs(issue.issue_date).format("DD.MM.YYYY"),
children: ( children: (
<Row <Row gutter={[16, 16]} align="middle">
gutter={[16, 16]} <Col xs={24} sm={24} md={13}>
align={"middle"}
>
<Col xs={24} md={24} sm={24} xl={13}>
<p style={{ textAlign: "right" }}>Пациент: {issue.patient.last_name} {issue.patient.first_name}</p> <p style={{ textAlign: "right" }}>Пациент: {issue.patient.last_name} {issue.patient.first_name}</p>
</Col> </Col>
<Col xs={24} md={24} sm={24} xl={5}> <Col xs={24} sm={24} md={5}>
<p style={{ textAlign: "right" }}>Линза: {issue.lens.side} {issue.lens.diameter}</p> <p style={{ textAlign: "right" }}>Линза: {issue.lens.side} {issue.lens.diameter}</p>
</Col> </Col>
<Col xs={24} md={24} sm={24} xl={6}> <Col xs={24} sm={24} md={6}>
<Button <Button
type={"dashed"} type="dashed"
onClick={() => issuesUI.handleSelectIssue(issue)} onClick={() => issuesData.handleSelectIssue(issue)}
style={{ marginRight: 40 }} style={{ marginRight: 40 }}
> >
Подробнее Подробнее
@ -116,8 +111,8 @@ const IssuesPage = () => {
const TimeLineView = () => { const TimeLineView = () => {
const paginatedItems = timeLineItems.slice( const paginatedItems = timeLineItems.slice(
(issuesUI.currentPage - 1) * issuesUI.pageSize, (issuesData.currentPage - 1) * issuesData.pageSize,
issuesUI.currentPage * issuesUI.pageSize issuesData.currentPage * issuesData.pageSize
); );
return ( return (
@ -128,14 +123,14 @@ const IssuesPage = () => {
/> />
<Row <Row
style={{ textAlign: "center", marginTop: 20 }} style={{ textAlign: "center", marginTop: 20 }}
align={"middle"} align="middle"
justify={"end"} justify="end"
> >
<Pagination <Pagination
current={issuesUI.currentPage} current={issuesData.currentPage}
pageSize={issuesUI.pageSize} pageSize={issuesData.pageSize}
total={timeLineItems.length} total={timeLineItems.length}
onChange={issuesUI.handlePaginationChange} onChange={issuesData.handlePaginationChange}
showSizeChanger={true} showSizeChanger={true}
pageSizeOptions={["5", "10", "20", "50"]} pageSizeOptions={["5", "10", "20", "50"]}
/> />
@ -153,46 +148,38 @@ const IssuesPage = () => {
); );
return ( return (
<div style={issuesUI.containerStyle}> <div style={issuesData.containerStyle}>
<Title level={1}><DatabaseOutlined /> Выдача линз</Title> <Title level={1}><DatabaseOutlined /> Выдача линз</Title>
<Row gutter={[16, 16]} style={issuesUI.filterBarStyle}> <Row gutter={[16, 16]} style={issuesData.filterBarStyle}>
<Col xs={24} md={24} sm={24} xl={12}> <Col xs={24} sm={24} md={12}>
<Input <Input
placeholder="Поиск по пациенту или врачу" placeholder="Поиск по пациенту или врачу"
onChange={(e) => issuesUI.handleSetSearchText(e.target.value)} value={issuesData.tempSearchText}
style={issuesUI.formItemStyle} onChange={(e) => issuesData.handleSetTempSearchText(e.target.value)}
onPressEnter={issuesData.handleSearch}
style={issuesData.formItemStyle}
allowClear allowClear
onClear={issuesData.handleClearSearch}
/> />
</Col> </Col>
<Col xs={24} sm={24} md={issuesData.isFilterDates ? 6 : 8}>
<Col xs={24} md={ <Tooltip title="Фильтр по дате выдачи линзы">
issuesUI.isFilterDates ? 12 : 16
} sm={
16
} xl={
issuesUI.isFilterDates ? 6 : 8
}>
<Tooltip
title="Фильтр по дате выдачи линзы"
>
<DatePicker.RangePicker <DatePicker.RangePicker
allowClear={false} allowClear={false}
style={issuesUI.formItemStyle} style={issuesData.formItemStyle}
placeholder={["Дата начала", "Дата окончания"]} placeholder={["Дата начала", "Дата окончания"]}
format="DD.MM.YYYY" format="DD.MM.YYYY"
value={issuesUI.isFilterDates && issuesUI.filterDates} value={issuesData.isFilterDates && issuesData.filterDates}
onChange={issuesUI.handleFilterDateChange} onChange={issuesData.handleFilterDateChange}
/> />
</Tooltip> </Tooltip>
</Col> </Col>
{issuesUI.isFilterDates && ( {issuesData.isFilterDates && (
<Col xs={24} md={4} sm={8} xl={2}> <Col xs={24} sm={24} md={2}>
<Tooltip <Tooltip title="Cбросить фильтр">
title="Cбросить фильтр"
>
<Button <Button
onClick={issuesUI.handleResetFilterDate} onClick={issuesData.handleResetFilterDate}
type={"primary"} type="primary"
block block
> >
Сбросить Сбросить
@ -200,22 +187,19 @@ const IssuesPage = () => {
</Tooltip> </Tooltip>
</Col> </Col>
)} )}
<Col xs={24} <Col xs={24} sm={24} md={issuesData.isFilterDates ? 4 : 4}>
md={issuesUI.isFilterDates ? 8 : 8}
sm={issuesUI.isFilterDates ? 24 : 8}
xl={4}>
<SelectViewMode <SelectViewMode
viewMode={issuesUI.viewMode} viewMode={issuesData.viewMode}
setViewMode={issuesUI.handleSetViewMode} setViewMode={issuesData.handleSetViewMode}
localStorageKey={"viewModeIssues"} localStorageKey="viewModeIssues"
toolTipText={"Формат отображения выдач линз"} toolTipText="Формат отображения выдач линз"
viewModes={viewModes} viewModes={viewModes}
/> />
</Col> </Col>
</Row> </Row>
{issuesData.isLoading ? ( {issuesData.isLoading ? (
<LoadingIndicator /> <LoadingIndicator />
) : issuesUI.viewMode === "table" ? ( ) : issuesData.viewMode === "table" ? (
<TableView /> <TableView />
) : ( ) : (
<TimeLineView /> <TimeLineView />
@ -223,21 +207,24 @@ const IssuesPage = () => {
<FloatButton <FloatButton
icon={<PlusOutlined />} icon={<PlusOutlined />}
type={"primary"} type="primary"
onClick={issuesUI.handleAddIssue} onClick={issuesData.handleAddIssue}
tooltip={"Добавить выдачу линзы"} tooltip="Добавить выдачу линзы"
/> />
<LensIssueFormModal <LensIssueFormModal
visible={issuesUI.isModalVisible} visible={issuesData.isModalVisible}
onCancel={issuesUI.handleCloseModal} onCancel={issuesData.handleCloseModal}
onSubmit={issuesData.handleSubmitFormModal} onSubmit={issuesData.handleSubmitFormModal}
isProcessing={issuesData.isProcessing}
patients={issuesData.patients}
lenses={issuesData.lenses}
/> />
<LensIssueViewModal <LensIssueViewModal
visible={issuesUI.selectedIssue !== null} visible={issuesData.selectedIssue !== null}
onCancel={issuesUI.resetSelectedIssue} onCancel={issuesData.resetSelectedIssue}
lensIssue={issuesUI.selectedIssue} lensIssue={issuesData.selectedIssue}
/> />
</div> </div>
); );

View File

@ -1,22 +1,169 @@
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import {useAddLensIssuesMutation, useGetLensIssuesQuery} from "../../../Api/lensIssuesApi.js";
import { notification } from "antd"; import { notification } from "antd";
import {closeModal} from "../../../Redux/Slices/lensIssuesSlice.js"; import {
useAddLensIssuesMutation,
useGetLensIssuesQuery,
} from "../../../Api/lensIssuesApi.js";
import { useGetAllPatientsQuery } from "../../../Api/patientsApi.js";
import {
closeModal,
openModal,
selectIssue,
setCurrentPage,
setEndFilterDate,
setPageSize,
setSearchText,
setStartFilterDate,
setViewMode
} from "../../../Redux/Slices/lensIssuesSlice.js";
import { getCachedInfo } from "../../../Utils/cachedInfoUtils.js";
import dayjs from "dayjs";
import {useGetNotIssuedLensesQuery} from "../../../Api/lensesApi.js";
const useIssues = () => { const useIssues = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const {
searchText,
currentPage,
pageSize,
selectedIssue,
isModalVisible,
viewMode,
startFilterDate,
endFilterDate,
} = useSelector(state => state.lensIssuesUI);
const {data: issues = [], isLoading, isError, error} = useGetLensIssuesQuery(undefined, { const [tempSearchText, setTempSearchText] = useState(searchText);
const {
data: issuesData = { issues: [], total_count: 0 },
isLoading: isIssuesLoading,
isError: isIssuesError,
error: issuesError
} = useGetLensIssuesQuery({
page: currentPage,
pageSize,
search: searchText || undefined,
sortOrder: 'desc',
startDate: startFilterDate || undefined,
endDate: endFilterDate || undefined,
}, {
pollingInterval: 20000, pollingInterval: 20000,
}); });
const [addIssue] = useAddLensIssuesMutation(); const { data: patients = [], isLoading: isPatientsLoading, isError: isPatientsError } = useGetAllPatientsQuery();
const { data: lenses = [], isLoading: isLensesLoading, isError: isLensesError } = useGetNotIssuedLensesQuery();
const [addIssue, { isLoading: isAdding }] = useAddLensIssuesMutation();
const isLoading = isIssuesLoading || isPatientsLoading || isLensesLoading;
const isError = isIssuesError || isPatientsError || isLensesError;
useEffect(() => {
document.title = "Выдача линз";
const cachedViewMode = getCachedInfo("viewModeIssues");
if (cachedViewMode) dispatch(setViewMode(cachedViewMode));
}, [dispatch]);
useEffect(() => {
if (isIssuesError) {
notification.error({
message: "Ошибка загрузки выдач",
description: issuesError?.data?.detail || "Не удалось загрузить выдачи линз",
placement: "topRight",
});
}
if (isPatientsError) {
notification.error({
message: "Ошибка загрузки пациентов",
description: "Не удалось загрузить список пациентов",
placement: "topRight",
});
}
if (isLensesError) {
notification.error({
message: "Ошибка загрузки линз",
description: "Не удалось загрузить список линз",
placement: "topRight",
});
}
}, [isIssuesError, isPatientsError, isLensesError, issuesError]);
const startFilterDateConverted = startFilterDate ? dayjs(startFilterDate) : null;
const endFilterDateConverted = endFilterDate ? dayjs(endFilterDate) : null;
const filterDates = [startFilterDateConverted, endFilterDateConverted];
const isFilterDates = startFilterDate && endFilterDate;
const containerStyle = { padding: 20 };
const filterBarStyle = { marginBottom: 20 };
const formItemStyle = { width: "100%" };
const viewModIconStyle = { marginRight: 8 };
const advancedSearchCardStyle = {
marginBottom: 20,
boxShadow: "0 1px 6px rgba(0, 0, 0, 0.15)",
borderRadius: 8
};
const handleSetTempSearchText = (value) => setTempSearchText(value);
const handleSearch = () => {
dispatch(setSearchText(tempSearchText));
dispatch(setCurrentPage(1));
};
const handleClearSearch = () => {
setTempSearchText('');
dispatch(setSearchText(''));
dispatch(setCurrentPage(1));
};
const handleSetViewMode = (mode) => dispatch(setViewMode(mode));
const handleSetCurrentPage = (page) => dispatch(setCurrentPage(page));
const handleSetPageSize = (size) => {
dispatch(setPageSize(size));
dispatch(setCurrentPage(1));
};
const handleCloseModal = () => dispatch(closeModal());
const handleSelectIssue = (issue) => dispatch(selectIssue(issue));
const resetSelectedIssue = () => dispatch(selectIssue(null));
const handleAddIssue = () => {
dispatch(selectIssue(null));
dispatch(openModal());
};
const handlePaginationChange = (page, pageSize) => {
handleSetCurrentPage(page);
handleSetPageSize(pageSize);
};
const handleFilterDateChange = (dates) => {
if (dates) {
const [start, end] = dates;
dispatch(setStartFilterDate(start.toISOString()));
dispatch(setEndFilterDate(end.toISOString()));
dispatch(setCurrentPage(1));
}
};
const handleResetFilterDate = () => {
dispatch(setStartFilterDate(null));
dispatch(setEndFilterDate(null));
dispatch(setCurrentPage(1));
};
const pagination = {
current: currentPage,
pageSize,
total: issuesData.total_count,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: handlePaginationChange,
};
const handleSubmitFormModal = async (issueDate, patientId, lensId) => { const handleSubmitFormModal = async (issueDate, patientId, lensId) => {
dispatch(closeModal()); dispatch(closeModal());
try { try {
await addIssue({issue_date: issueDate, patient_id: patientId, lens_id: lensId}); await addIssue({ issue_date: issueDate, patient_id: patientId, lens_id: lensId }).unwrap();
notification.success({ notification.success({
message: "Линза выдана", message: "Линза выдана",
description: "Линза успешно выдана пациенту.", description: "Линза успешно выдана пациенту.",
@ -32,10 +179,38 @@ const useIssues = () => {
}; };
return { return {
issues, issues: issuesData.issues,
patients,
lenses,
isLoading, isLoading,
isError, isError,
error, isProcessing: isAdding,
searchText,
tempSearchText,
selectedIssue,
isModalVisible,
viewMode,
currentPage,
pageSize,
filterDates,
isFilterDates,
containerStyle,
filterBarStyle,
formItemStyle,
viewModIconStyle,
advancedSearchCardStyle,
pagination,
handleAddIssue,
handlePaginationChange,
handleSetTempSearchText,
handleSearch,
handleClearSearch,
handleSetViewMode,
handleCloseModal,
handleFilterDateChange,
resetSelectedIssue,
handleSelectIssue,
handleResetFilterDate,
handleSubmitFormModal, handleSubmitFormModal,
}; };
}; };

View File

@ -1,147 +0,0 @@
import {useDispatch, useSelector} from "react-redux";
import {getCachedInfo} from "../../../Utils/cachedInfoUtils.js";
import {
closeModal,
openModal,
selectIssue,
setCurrentPage,
setEndFilterDate,
setPageSize,
setSearchText,
setStartFilterDate,
setViewMode
} from "../../../Redux/Slices/lensIssuesSlice.js";
import {useEffect, useMemo} from "react";
import dayjs from "dayjs";
const useIssuesUI = (issues) => {
const dispatch = useDispatch();
const {
searchText,
currentPage,
pageSize,
selectedIssue,
isModalVisible,
viewMode,
startFilterDate,
endFilterDate,
} = useSelector(state => state.lensIssuesUI);
useEffect(() => {
document.title = "Выдача линз";
const cachedViewMode = getCachedInfo("viewModeIssues");
if (cachedViewMode) dispatch(setViewMode(cachedViewMode));
}, [dispatch])
const startFilterDateConverted = startFilterDate ? dayjs(startFilterDate) : null;
const endFilterDateConverted = endFilterDate ? dayjs(endFilterDate) : null;
const filterDates = [startFilterDateConverted, endFilterDateConverted];
const isFilterDates = startFilterDate && endFilterDate;
const containerStyle = { padding: 20 };
const filterBarStyle = { marginBottom: 20 };
const formItemStyle = { width: "100%" };
const viewModIconStyle = { marginRight: 8 };
const advancedSearchCardStyle = {
marginBottom: 20,
boxShadow: "0 1px 6px rgba(0, 0, 0, 0.15)",
borderRadius: 8
};
const handleSetSearchText = (value) => dispatch(setSearchText(value));
const handleSetViewMode = (mode) => dispatch(setViewMode(mode));
const handleSetCurrentPage = (page) => dispatch(setCurrentPage(page));
const handleSetPageSize = (size) => dispatch(setPageSize(size));
const handleCloseModal = () => dispatch(closeModal());
const handleSelectIssue = (issue) => dispatch(selectIssue(issue));
const resetSelectedIssue = () => dispatch(selectIssue(null));
const handleAddIssue = () => {
dispatch(selectIssue(null));
dispatch(openModal());
};
const handlePaginationChange = (page, pageSize) => {
handleSetCurrentPage(page);
handleSetPageSize(pageSize);
};
const handleFilterDateChange = (dates) => {
if (dates) {
const [start, end] = dates;
dispatch(setStartFilterDate(start.toISOString()));
dispatch(setEndFilterDate(end.toISOString()));
}
};
const handleResetFilterDate = () => {
dispatch(setStartFilterDate(null));
dispatch(setEndFilterDate(null));
};
const filteredIssues = useMemo(() => {
return issues.filter(issue => {
let dateFilter = true;
if (startFilterDateConverted && endFilterDateConverted) {
const issueDate = dayjs(issue.issue_date);
dateFilter = issueDate.isAfter(startFilterDateConverted) && issueDate.isBefore(endFilterDateConverted);
}
return (
(
issue.patient.last_name.toLowerCase().includes(searchText) ||
issue.patient.first_name.toLowerCase().includes(searchText) ||
issue.doctor.last_name.toLowerCase().includes(searchText) ||
issue.doctor.first_name.toLowerCase().includes(searchText)
) &&
dateFilter
)
});
}, [issues, searchText, startFilterDateConverted, endFilterDateConverted]);
const pagination = {
current: currentPage,
pageSize: pageSize,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => {
setCurrentPage(page);
setPageSize(newPageSize);
},
};
return {
filteredIssues,
pagination,
searchText,
selectedIssue,
isModalVisible,
viewMode,
currentPage,
pageSize,
filterDates,
isFilterDates,
containerStyle,
filterBarStyle,
formItemStyle,
viewModIconStyle,
advancedSearchCardStyle,
handleAddIssue,
handlePaginationChange,
handleSetSearchText,
handleSetViewMode,
handleSetCurrentPage,
handleSetPageSize,
handleCloseModal,
handleFilterDateChange,
resetSelectedIssue,
handleSelectIssue,
handleResetFilterDate,
};
};
export default useIssuesUI;

View File

@ -37,11 +37,13 @@ const PatientsPage = () => {
title: "Фамилия", title: "Фамилия",
dataIndex: "last_name", dataIndex: "last_name",
key: "last_name", key: "last_name",
sorter: (a, b) => a.last_name.localeCompare(b.last_name),
}, },
{ {
title: "Имя", title: "Имя",
dataIndex: "first_name", dataIndex: "first_name",
key: "first_name", key: "first_name",
sorter: (a, b) => a.first_name.localeCompare(b.first_name),
}, },
{ {
title: "Отчество", title: "Отчество",
@ -52,6 +54,7 @@ const PatientsPage = () => {
title: "Дата рождения", title: "Дата рождения",
dataIndex: "birthday", dataIndex: "birthday",
render: patientsData.formatDate, render: patientsData.formatDate,
sorter: (a, b) => new Date(b.birthday) - new Date(a.birthday),
}, },
{ {
title: "Телефон", title: "Телефон",