From 05bfec81fc0e680aa568143afb3c8a3c465e9b15 Mon Sep 17 00:00:00 2001 From: andrei Date: Sat, 7 Jun 2025 16:07:24 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=81=D0=BE=D1=80=D1=82=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B0=20=D0=BF=D0=B0=D1=86=D0=B8=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=BF=D0=BE=20=D1=84=D0=B0=D0=BC=D0=B8=D0=BB?= =?UTF-8?q?=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/app/application/lenses_repository.py | 80 ++++++++- api/app/controllers/lenses_router.py | 41 ++++- .../entities/responses/paginated_lens.py | 8 + api/app/infrastructure/lenses_service.py | 38 ++++- web-app/src/App/ErrorBoundary.jsx | 9 +- .../Components/LensesTab/LensesTab.jsx | 126 +++++++------- .../Components/LensesTab/useLenses.js | 159 ++++++++++++++++-- .../Components/LensesTab/useLensesUI.js | 128 -------------- 8 files changed, 356 insertions(+), 233 deletions(-) create mode 100644 api/app/domain/entities/responses/paginated_lens.py delete mode 100644 web-app/src/Components/Pages/LensesSetsPage/Components/LensesTab/useLensesUI.js diff --git a/api/app/application/lenses_repository.py b/api/app/application/lenses_repository.py index 718d1ad..d88c519 100644 --- a/api/app/application/lenses_repository.py +++ b/api/app/application/lenses_repository.py @@ -1,6 +1,6 @@ -from typing import Sequence, Optional +from typing import Sequence, Optional, Tuple, Literal -from sqlalchemy import select, desc +from sqlalchemy import select, desc, or_, func from sqlalchemy.ext.asyncio import AsyncSession from app.domain.models import Lens @@ -10,10 +10,80 @@ class LensesRepository: def __init__(self, db: AsyncSession): self.db = db - async def get_all(self) -> Sequence[Lens]: - stmt = select(Lens).order_by(Lens.id) + async def get_all( + self, + skip: int = 0, + limit: int = 10, + search: str = None, + sort_order: Literal["asc", "desc"] = "desc", + tor: float = None, + diameter: float = None, + preset_refraction: float = None, + periphery_toricity: float = None, + side: str = "all", + issued: bool = None, + trial: float = None, + ) -> Tuple[Sequence[Lens], int]: + stmt = select(Lens) + count_stmt = select(func.count()).select_from(Lens) + + if search: + search = f"%{search}%" + stmt = stmt.filter( + or_( + Lens.tor.cast(str).ilike(search), + Lens.diameter.cast(str).ilike(search), + Lens.preset_refraction.cast(str).ilike(search), + Lens.periphery_toricity.cast(str).ilike(search), + Lens.side.ilike(search), + Lens.fvc.cast(str).ilike(search), + Lens.trial.cast(str).ilike(search), + ) + ) + count_stmt = count_stmt.filter( + or_( + Lens.tor.cast(str).ilike(search), + Lens.diameter.cast(str).ilike(search), + Lens.preset_refraction.cast(str).ilike(search), + Lens.periphery_toricity.cast(str).ilike(search), + Lens.side.ilike(search), + Lens.fvc.cast(str).ilike(search), + Lens.trial.cast(str).ilike(search), + ) + ) + + if tor is not None: + stmt = stmt.filter(Lens.tor == tor) + count_stmt = count_stmt.filter(Lens.tor == tor) + if diameter is not None: + stmt = stmt.filter(Lens.diameter == diameter) + count_stmt = count_stmt.filter(Lens.diameter == diameter) + if preset_refraction is not None: + stmt = stmt.filter(Lens.preset_refraction == preset_refraction) + count_stmt = count_stmt.filter(Lens.preset_refraction == preset_refraction) + if periphery_toricity is not None: + stmt = stmt.filter(Lens.periphery_toricity == periphery_toricity) + count_stmt = count_stmt.filter(Lens.periphery_toricity == periphery_toricity) + if side != "all": + stmt = stmt.filter(Lens.side == side) + count_stmt = count_stmt.filter(Lens.side == side) + if issued is not None: + stmt = stmt.filter(Lens.issued == issued) + count_stmt = count_stmt.filter(Lens.issued == issued) + if trial is not None: + stmt = stmt.filter(Lens.trial == trial) + count_stmt = count_stmt.filter(Lens.trial == trial) + + stmt = stmt.order_by(Lens.id.desc() if sort_order == "desc" else Lens.id.asc()) + + stmt = stmt.offset(skip).limit(limit) result = await self.db.execute(stmt) - return result.scalars().all() + lenses = result.scalars().all() + + count_result = await self.db.execute(count_stmt) + total_count = count_result.scalar() + + return lenses, total_count async def get_all_not_issued(self) -> Sequence[Lens]: stmt = select(Lens).filter_by(issued=False).order_by(desc(Lens.id)) diff --git a/api/app/controllers/lenses_router.py b/api/app/controllers/lenses_router.py index 1459489..8ec398e 100644 --- a/api/app/controllers/lenses_router.py +++ b/api/app/controllers/lenses_router.py @@ -1,8 +1,11 @@ -from fastapi import APIRouter, Depends +from typing import 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 import LensEntity +from app.domain.entities.responses.paginated_lens import PaginatedLensesResponseEntity from app.infrastructure.dependencies import get_current_user from app.infrastructure.lenses_service import LensesService @@ -11,16 +14,40 @@ router = APIRouter() @router.get( "/", - response_model=list[LensEntity], - summary="Get all lenses", - description="Returns a list of all lenses", + response_model=PaginatedLensesResponseEntity, + summary="Get all lenses with pagination and filtering", + description="Returns a paginated list of lenses with optional search, sorting, and advanced filtering", ) async def get_all_lenses( - db: AsyncSession = Depends(get_db), - user=Depends(get_current_user), + page: int = Query(1, ge=1, description="Page number"), + page_size: int = Query(10, ge=1, le=100, description="Number of lenses per page"), + search: str = Query(None, description="Search term for filtering lenses"), + sort_order: Literal["asc", "desc"] = Query("desc", description="Sort order by id (asc or desc)"), + tor: float = Query(None, description="Filter by tor"), + diameter: float = Query(None, description="Filter by diameter"), + preset_refraction: float = Query(None, description="Filter by preset refraction"), + periphery_toricity: float = Query(None, description="Filter by periphery toricity"), + side: Literal["левая", "правая", "all"] = Query("all", description="Filter by side"), + issued: bool = Query(None, description="Filter by issued status"), + trial: float = Query(None, description="Filter by trial"), + db: AsyncSession = Depends(get_db), + user=Depends(get_current_user), ): lenses_service = LensesService(db) - return await lenses_service.get_all_lenses() + lenses, total_count = await lenses_service.get_all_lenses( + page=page, + page_size=page_size, + search=search, + sort_order=sort_order, + tor=tor, + diameter=diameter, + preset_refraction=preset_refraction, + periphery_toricity=periphery_toricity, + side=side, + issued=issued, + trial=trial, + ) + return {"lenses": lenses, "total_count": total_count} @router.get( diff --git a/api/app/domain/entities/responses/paginated_lens.py b/api/app/domain/entities/responses/paginated_lens.py new file mode 100644 index 0000000..6db3065 --- /dev/null +++ b/api/app/domain/entities/responses/paginated_lens.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel + +from app.domain.entities.lens import LensEntity + + +class PaginatedLensesResponseEntity(BaseModel): + lenses: list[LensEntity] + total_count: int diff --git a/api/app/infrastructure/lenses_service.py b/api/app/infrastructure/lenses_service.py index af55642..5f2396a 100644 --- a/api/app/infrastructure/lenses_service.py +++ b/api/app/infrastructure/lenses_service.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Tuple, Literal from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession @@ -16,13 +16,35 @@ class LensesService: self.lenses_repository = LensesRepository(db) self.lens_types_repository = LensTypesRepository(db) - async def get_all_lenses(self) -> list[LensEntity]: - lenses = await self.lenses_repository.get_all() - - return [ - self.model_to_entity(lens) - for lens in lenses - ] + async def get_all_lenses( + self, + page: int = 1, + page_size: int = 10, + search: str = None, + sort_order: Literal["asc", "desc"] = "desc", + tor: float = None, + diameter: float = None, + preset_refraction: float = None, + periphery_toricity: float = None, + side: str = "all", + issued: bool = None, + trial: float = None, + ) -> Tuple[list[LensEntity], int]: + skip = (page - 1) * page_size + lenses, total_count = await self.lenses_repository.get_all( + skip=skip, + limit=page_size, + search=search, + sort_order=sort_order, + tor=tor, + diameter=diameter, + preset_refraction=preset_refraction, + periphery_toricity=periphery_toricity, + side=side, + issued=issued, + trial=trial, + ) + return [self.model_to_entity(lens) for lens in lenses], total_count async def get_all_not_issued_lenses(self) -> list[LensEntity]: lenses = await self.lenses_repository.get_all_not_issued() diff --git a/web-app/src/App/ErrorBoundary.jsx b/web-app/src/App/ErrorBoundary.jsx index 51ff4fe..37342ab 100644 --- a/web-app/src/App/ErrorBoundary.jsx +++ b/web-app/src/App/ErrorBoundary.jsx @@ -1,5 +1,5 @@ import { Component } from "react"; -import {Result} from "antd"; +import {Alert, Result} from "antd"; class ErrorBoundary extends Component { state = { hasError: false }; @@ -14,7 +14,12 @@ class ErrorBoundary extends Component { render() { if (this.state.hasError) { - return ; + return ( + <> + + + + ); } return this.props.children; } diff --git a/web-app/src/Components/Pages/LensesSetsPage/Components/LensesTab/LensesTab.jsx b/web-app/src/Components/Pages/LensesSetsPage/Components/LensesTab/LensesTab.jsx index 6c14217..cca4a81 100644 --- a/web-app/src/Components/Pages/LensesSetsPage/Components/LensesTab/LensesTab.jsx +++ b/web-app/src/Components/Pages/LensesSetsPage/Components/LensesTab/LensesTab.jsx @@ -12,7 +12,8 @@ import { Grid, Table, Popconfirm, - Typography, Result + Typography, + Result } from "antd"; import { PlusOutlined, @@ -27,28 +28,25 @@ import LensFormModal from "./Components/LensFormModal/LensFormModal.jsx"; import SelectViewMode from "../../../../Widgets/SelectViewMode/SelectViewMode.jsx"; import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import useLenses from "./useLenses.js"; -import useLensesUI from "./useLensesUI.js"; -const {Option} = Select; -const {useBreakpoint} = Grid; -const {Title} = Typography; +const { Option } = Select; +const { useBreakpoint } = Grid; +const { Title } = Typography; const LensesTab = () => { const lensesData = useLenses(); - const lensesUI = useLensesUI(lensesData.lenses); - const screens = useBreakpoint(); const viewModes = [ { value: "tile", label: "Плитка", - icon: + icon: }, { value: "table", label: "Таблица", - icon: + icon: } ]; @@ -94,8 +92,8 @@ const LensesTab = () => { dataIndex: "side", key: "side", filters: [ - {text: "Левая", value: "левая"}, - {text: "Правая", value: "правая"}, + { text: "Левая", value: "левая" }, + { text: "Правая", value: "правая" }, ], onFilter: (value, record) => record.side === value, }, @@ -105,8 +103,8 @@ const LensesTab = () => { key: "issued", render: (issued) => (issued ? "Да" : "Нет"), filters: [ - {text: "Да", value: true}, - {text: "Нет", value: false}, + { text: "Да", value: true }, + { text: "Нет", value: false }, ], onFilter: (value, record) => record.issued === value, }, @@ -115,9 +113,8 @@ const LensesTab = () => { key: "actions", fixed: 'right', render: (text, record) => ( -
- - +
+ lensesData.handleDeleteLens(record.id)} @@ -145,22 +142,22 @@ const LensesTab = () => { ); return ( -
+
<FolderViewOutlined/> Линзы - + lensesUI.handleSetSearchText(e.target.value)} - style={lensesUI.formItemStyle} + value={lensesData.searchText} + onChange={(e) => lensesData.handleSetSearchText(e.target.value)} + style={lensesData.formItemStyle} allowClear />