feat: IssuesPage
Добавлены фильтрация, пагинация и поиск. Улучшен UI. Удален useIssuesUI.js. Добавлен PaginatedLensIssuesResponseEntity.
This commit is contained in:
parent
914ae0528f
commit
7a2ef98fd5
@ -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.orm import joinedload
|
||||
|
||||
from app.domain.models import LensIssue
|
||||
from app.domain.models import LensIssue, Patient, User
|
||||
|
||||
|
||||
class LensIssuesRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
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 = (
|
||||
select(LensIssue)
|
||||
.options(joinedload(LensIssue.lens))
|
||||
.options(joinedload(LensIssue.patient))
|
||||
.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)
|
||||
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]:
|
||||
stmt = select(LensIssue).filter_by(id=lens_issue_id)
|
||||
|
||||
@ -10,8 +10,8 @@ class PatientsRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_all(self, skip: int = 0, limit: int = 10, search: str = None,
|
||||
sort_order: Literal["asc", "desc"] = "asc") -> Tuple[Sequence[Patient], int]:
|
||||
async def get_all(self, skip: int = 0, limit: int = 100, search: str = None,
|
||||
sort_order: Literal["asc", "desc"] = "asc", all_params: bool = False) -> Tuple[Sequence[Patient], int]:
|
||||
stmt = select(Patient)
|
||||
|
||||
if search:
|
||||
@ -50,6 +50,12 @@ class PatientsRepository:
|
||||
count_result = await self.db.execute(count_stmt)
|
||||
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
|
||||
|
||||
async def get_by_id(self, patient_id: int) -> Optional[Patient]:
|
||||
|
||||
@ -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 app.database.session import get_db
|
||||
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.lens_issues_service import LensIssuesService
|
||||
|
||||
@ -11,17 +14,34 @@ router = APIRouter()
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=list[LensIssueEntity],
|
||||
response_model=PaginatedLensIssuesResponseEntity,
|
||||
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(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
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),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
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(
|
||||
"/",
|
||||
|
||||
@ -23,11 +23,12 @@ async def get_all_patients(
|
||||
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"),
|
||||
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),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
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}
|
||||
|
||||
|
||||
|
||||
8
api/app/domain/entities/responses/paginated_issue.py
Normal file
8
api/app/domain/entities/responses/paginated_issue.py
Normal 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
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Optional, Literal, Tuple
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@ -22,13 +22,27 @@ class LensIssuesService:
|
||||
self.users_repository = UsersRepository(db)
|
||||
self.lenses_repository = LensesRepository(db)
|
||||
|
||||
async def get_all_lens_issues(self) -> list[LensIssueEntity]:
|
||||
lens_issues = await self.lens_issues_repository.get_all()
|
||||
|
||||
return [
|
||||
self.model_to_entity(lens_issue)
|
||||
for lens_issue in lens_issues
|
||||
]
|
||||
async def get_all_lens_issues(
|
||||
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[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]:
|
||||
patient = await self.patient_repository.get_by_id(lens_issue.patient_id)
|
||||
|
||||
@ -13,10 +13,12 @@ class PatientsService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
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]:
|
||||
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 (
|
||||
[self.model_to_entity(patient) for patient in patients],
|
||||
total_count
|
||||
|
||||
@ -8,9 +8,26 @@ export const lensIssuesApi = createApi({
|
||||
tagTypes: ['LensIssues'],
|
||||
endpoints: (builder) => ({
|
||||
getLensIssues: builder.query({
|
||||
query: () => '/lens_issues/',
|
||||
providesTags: ['LensIssues'],
|
||||
refetchOnMountOrArgChange: 5
|
||||
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: ['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({
|
||||
query: (lensIssues) => ({
|
||||
@ -25,5 +42,5 @@ export const lensIssuesApi = createApi({
|
||||
|
||||
export const {
|
||||
useGetLensIssuesQuery,
|
||||
useAddLensIssuesMutation
|
||||
useAddLensIssuesMutation,
|
||||
} = lensIssuesApi;
|
||||
@ -18,6 +18,20 @@ export const patientsApi = createApi({
|
||||
}),
|
||||
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({
|
||||
query: (patient) => ({
|
||||
url: '/patients/',
|
||||
@ -46,6 +60,7 @@ export const patientsApi = createApi({
|
||||
|
||||
export const {
|
||||
useGetPatientsQuery,
|
||||
useGetAllPatientsQuery,
|
||||
useAddPatientMutation,
|
||||
useUpdatePatientMutation,
|
||||
useDeletePatientMutation,
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import {useGetPatientsQuery} from "../../../../../Api/patientsApi.js";
|
||||
import {useGetAllPatientsQuery, useGetPatientsQuery} from "../../../../../Api/patientsApi.js";
|
||||
import {useGetNotIssuedLensesQuery} from "../../../../../Api/lensesApi.js";
|
||||
|
||||
|
||||
const useLensIssueForm = () => {
|
||||
const {data: patients = [], isLoading: isLoadingPatients, isError: isErrorPatients} = useGetPatientsQuery(undefined, {
|
||||
const {data: patients = [], isLoading: isLoadingPatients, isError: isErrorPatients} = useGetAllPatientsQuery(undefined, {
|
||||
pollingInterval: 10000,
|
||||
});
|
||||
const {data: lenses = [], isLoading: isLoadingLenses, isError: isErrorLenses} = useGetNotIssuedLensesQuery(undefined, {
|
||||
|
||||
@ -10,36 +10,34 @@ import {
|
||||
Typography,
|
||||
Timeline,
|
||||
Grid,
|
||||
Pagination, Result
|
||||
Pagination,
|
||||
Result
|
||||
} 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 dayjs from "dayjs";
|
||||
import LensIssueFormModal from "./Components/LensIssueFormModal/LensIssueFormModal.jsx";
|
||||
import SelectViewMode from "../../Widgets/SelectViewMode/SelectViewMode.jsx";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import useIssues from "./useIssues.js";
|
||||
import useIssuesUI from "./useIssuesUI.js";
|
||||
|
||||
const {Title} = Typography;
|
||||
const {useBreakpoint} = Grid;
|
||||
const { Title } = Typography;
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const IssuesPage = () => {
|
||||
const issuesData = useIssues();
|
||||
const issuesUI = useIssuesUI(issuesData.issues);
|
||||
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const viewModes = [
|
||||
{
|
||||
value: "table",
|
||||
label: "Таблица",
|
||||
icon: <DatabaseOutlined style={issuesUI.viewModIconStyle}/>,
|
||||
icon: <DatabaseOutlined style={issuesData.viewModIconStyle} />,
|
||||
},
|
||||
{
|
||||
value: "timeline",
|
||||
label: "Лента",
|
||||
icon: <UnorderedListOutlined style={issuesUI.viewModIconStyle}/>,
|
||||
icon: <UnorderedListOutlined style={issuesData.viewModIconStyle} />,
|
||||
},
|
||||
];
|
||||
|
||||
@ -73,7 +71,7 @@ const IssuesPage = () => {
|
||||
title: "Действия",
|
||||
key: "actions",
|
||||
render: (_, issue) => (
|
||||
<Button type={"link"} onClick={() => issuesUI.handleSelectIssue(issue)}>Подробнее</Button>
|
||||
<Button type="link" onClick={() => issuesData.handleSelectIssue(issue)}>Подробнее</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
@ -81,31 +79,28 @@ const IssuesPage = () => {
|
||||
const TableView = () => (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={issuesUI.filteredIssues}
|
||||
dataSource={issuesData.issues}
|
||||
rowKey="id"
|
||||
pagination={issuesUI.pagination}
|
||||
pagination={issuesData.pagination}
|
||||
showSorterTooltip={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const timeLineItems = issuesUI.filteredIssues.map(issue => ({
|
||||
const timeLineItems = issuesData.issues.map(issue => ({
|
||||
label: dayjs(issue.issue_date).format("DD.MM.YYYY"),
|
||||
children: (
|
||||
<Row
|
||||
gutter={[16, 16]}
|
||||
align={"middle"}
|
||||
>
|
||||
<Col xs={24} md={24} sm={24} xl={13}>
|
||||
<p style={{textAlign: "right"}}>Пациент: {issue.patient.last_name} {issue.patient.first_name}</p>
|
||||
<Row gutter={[16, 16]} align="middle">
|
||||
<Col xs={24} sm={24} md={13}>
|
||||
<p style={{ textAlign: "right" }}>Пациент: {issue.patient.last_name} {issue.patient.first_name}</p>
|
||||
</Col>
|
||||
<Col xs={24} md={24} sm={24} xl={5}>
|
||||
<p style={{textAlign: "right"}}>Линза: {issue.lens.side} {issue.lens.diameter}</p>
|
||||
<Col xs={24} sm={24} md={5}>
|
||||
<p style={{ textAlign: "right" }}>Линза: {issue.lens.side} {issue.lens.diameter}</p>
|
||||
</Col>
|
||||
<Col xs={24} md={24} sm={24} xl={6}>
|
||||
<Col xs={24} sm={24} md={6}>
|
||||
<Button
|
||||
type={"dashed"}
|
||||
onClick={() => issuesUI.handleSelectIssue(issue)}
|
||||
style={{marginRight: 40}}
|
||||
type="dashed"
|
||||
onClick={() => issuesData.handleSelectIssue(issue)}
|
||||
style={{ marginRight: 40 }}
|
||||
>
|
||||
Подробнее
|
||||
</Button>
|
||||
@ -116,8 +111,8 @@ const IssuesPage = () => {
|
||||
|
||||
const TimeLineView = () => {
|
||||
const paginatedItems = timeLineItems.slice(
|
||||
(issuesUI.currentPage - 1) * issuesUI.pageSize,
|
||||
issuesUI.currentPage * issuesUI.pageSize
|
||||
(issuesData.currentPage - 1) * issuesData.pageSize,
|
||||
issuesData.currentPage * issuesData.pageSize
|
||||
);
|
||||
|
||||
return (
|
||||
@ -127,15 +122,15 @@ const IssuesPage = () => {
|
||||
mode={screens.xs ? "left" : "right"}
|
||||
/>
|
||||
<Row
|
||||
style={{textAlign: "center", marginTop: 20}}
|
||||
align={"middle"}
|
||||
justify={"end"}
|
||||
style={{ textAlign: "center", marginTop: 20 }}
|
||||
align="middle"
|
||||
justify="end"
|
||||
>
|
||||
<Pagination
|
||||
current={issuesUI.currentPage}
|
||||
pageSize={issuesUI.pageSize}
|
||||
current={issuesData.currentPage}
|
||||
pageSize={issuesData.pageSize}
|
||||
total={timeLineItems.length}
|
||||
onChange={issuesUI.handlePaginationChange}
|
||||
onChange={issuesData.handlePaginationChange}
|
||||
showSizeChanger={true}
|
||||
pageSizeOptions={["5", "10", "20", "50"]}
|
||||
/>
|
||||
@ -153,46 +148,38 @@ const IssuesPage = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={issuesUI.containerStyle}>
|
||||
<Title level={1}><DatabaseOutlined/> Выдача линз</Title>
|
||||
<Row gutter={[16, 16]} style={issuesUI.filterBarStyle}>
|
||||
<Col xs={24} md={24} sm={24} xl={12}>
|
||||
<div style={issuesData.containerStyle}>
|
||||
<Title level={1}><DatabaseOutlined /> Выдача линз</Title>
|
||||
<Row gutter={[16, 16]} style={issuesData.filterBarStyle}>
|
||||
<Col xs={24} sm={24} md={12}>
|
||||
<Input
|
||||
placeholder="Поиск по пациенту или врачу"
|
||||
onChange={(e) => issuesUI.handleSetSearchText(e.target.value)}
|
||||
style={issuesUI.formItemStyle}
|
||||
value={issuesData.tempSearchText}
|
||||
onChange={(e) => issuesData.handleSetTempSearchText(e.target.value)}
|
||||
onPressEnter={issuesData.handleSearch}
|
||||
style={issuesData.formItemStyle}
|
||||
allowClear
|
||||
onClear={issuesData.handleClearSearch}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={
|
||||
issuesUI.isFilterDates ? 12 : 16
|
||||
} sm={
|
||||
16
|
||||
} xl={
|
||||
issuesUI.isFilterDates ? 6 : 8
|
||||
}>
|
||||
<Tooltip
|
||||
title="Фильтр по дате выдачи линзы"
|
||||
>
|
||||
<Col xs={24} sm={24} md={issuesData.isFilterDates ? 6 : 8}>
|
||||
<Tooltip title="Фильтр по дате выдачи линзы">
|
||||
<DatePicker.RangePicker
|
||||
allowClear={false}
|
||||
style={issuesUI.formItemStyle}
|
||||
style={issuesData.formItemStyle}
|
||||
placeholder={["Дата начала", "Дата окончания"]}
|
||||
format="DD.MM.YYYY"
|
||||
value={issuesUI.isFilterDates && issuesUI.filterDates}
|
||||
onChange={issuesUI.handleFilterDateChange}
|
||||
value={issuesData.isFilterDates && issuesData.filterDates}
|
||||
onChange={issuesData.handleFilterDateChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
{issuesUI.isFilterDates && (
|
||||
<Col xs={24} md={4} sm={8} xl={2}>
|
||||
<Tooltip
|
||||
title="Cбросить фильтр"
|
||||
>
|
||||
{issuesData.isFilterDates && (
|
||||
<Col xs={24} sm={24} md={2}>
|
||||
<Tooltip title="Cбросить фильтр">
|
||||
<Button
|
||||
onClick={issuesUI.handleResetFilterDate}
|
||||
type={"primary"}
|
||||
onClick={issuesData.handleResetFilterDate}
|
||||
type="primary"
|
||||
block
|
||||
>
|
||||
Сбросить
|
||||
@ -200,47 +187,47 @@ const IssuesPage = () => {
|
||||
</Tooltip>
|
||||
</Col>
|
||||
)}
|
||||
<Col xs={24}
|
||||
md={issuesUI.isFilterDates ? 8 : 8}
|
||||
sm={issuesUI.isFilterDates ? 24 : 8}
|
||||
xl={4}>
|
||||
<Col xs={24} sm={24} md={issuesData.isFilterDates ? 4 : 4}>
|
||||
<SelectViewMode
|
||||
viewMode={issuesUI.viewMode}
|
||||
setViewMode={issuesUI.handleSetViewMode}
|
||||
localStorageKey={"viewModeIssues"}
|
||||
toolTipText={"Формат отображения выдач линз"}
|
||||
viewMode={issuesData.viewMode}
|
||||
setViewMode={issuesData.handleSetViewMode}
|
||||
localStorageKey="viewModeIssues"
|
||||
toolTipText="Формат отображения выдач линз"
|
||||
viewModes={viewModes}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{issuesData.isLoading ? (
|
||||
<LoadingIndicator/>
|
||||
) : issuesUI.viewMode === "table" ? (
|
||||
<TableView/>
|
||||
<LoadingIndicator />
|
||||
) : issuesData.viewMode === "table" ? (
|
||||
<TableView />
|
||||
) : (
|
||||
<TimeLineView/>
|
||||
<TimeLineView />
|
||||
)}
|
||||
|
||||
<FloatButton
|
||||
icon={<PlusOutlined/>}
|
||||
type={"primary"}
|
||||
onClick={issuesUI.handleAddIssue}
|
||||
tooltip={"Добавить выдачу линзы"}
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
onClick={issuesData.handleAddIssue}
|
||||
tooltip="Добавить выдачу линзы"
|
||||
/>
|
||||
|
||||
<LensIssueFormModal
|
||||
visible={issuesUI.isModalVisible}
|
||||
onCancel={issuesUI.handleCloseModal}
|
||||
visible={issuesData.isModalVisible}
|
||||
onCancel={issuesData.handleCloseModal}
|
||||
onSubmit={issuesData.handleSubmitFormModal}
|
||||
isProcessing={issuesData.isProcessing}
|
||||
patients={issuesData.patients}
|
||||
lenses={issuesData.lenses}
|
||||
/>
|
||||
|
||||
<LensIssueViewModal
|
||||
visible={issuesUI.selectedIssue !== null}
|
||||
onCancel={issuesUI.resetSelectedIssue}
|
||||
lensIssue={issuesUI.selectedIssue}
|
||||
visible={issuesData.selectedIssue !== null}
|
||||
onCancel={issuesData.resetSelectedIssue}
|
||||
lensIssue={issuesData.selectedIssue}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IssuesPage;
|
||||
export default IssuesPage;
|
||||
@ -1,22 +1,169 @@
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {useAddLensIssuesMutation, useGetLensIssuesQuery} from "../../../Api/lensIssuesApi.js";
|
||||
import {notification} from "antd";
|
||||
import {closeModal} from "../../../Redux/Slices/lensIssuesSlice.js";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { notification } from "antd";
|
||||
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 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,
|
||||
});
|
||||
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) => {
|
||||
dispatch(closeModal());
|
||||
|
||||
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({
|
||||
message: "Линза выдана",
|
||||
description: "Линза успешно выдана пациенту.",
|
||||
@ -32,10 +179,38 @@ const useIssues = () => {
|
||||
};
|
||||
|
||||
return {
|
||||
issues,
|
||||
issues: issuesData.issues,
|
||||
patients,
|
||||
lenses,
|
||||
isLoading,
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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;
|
||||
@ -37,11 +37,13 @@ const PatientsPage = () => {
|
||||
title: "Фамилия",
|
||||
dataIndex: "last_name",
|
||||
key: "last_name",
|
||||
sorter: (a, b) => a.last_name.localeCompare(b.last_name),
|
||||
},
|
||||
{
|
||||
title: "Имя",
|
||||
dataIndex: "first_name",
|
||||
key: "first_name",
|
||||
sorter: (a, b) => a.first_name.localeCompare(b.first_name),
|
||||
},
|
||||
{
|
||||
title: "Отчество",
|
||||
@ -52,6 +54,7 @@ const PatientsPage = () => {
|
||||
title: "Дата рождения",
|
||||
dataIndex: "birthday",
|
||||
render: patientsData.formatDate,
|
||||
sorter: (a, b) => new Date(b.birthday) - new Date(a.birthday),
|
||||
},
|
||||
{
|
||||
title: "Телефон",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user