diff --git a/api/.dockerignore b/api/.dockerignore index 95d9b1f..329ed4e 100644 --- a/api/.dockerignore +++ b/api/.dockerignore @@ -1,3 +1,4 @@ .venv k8s -.idea \ No newline at end of file +.idea +.env \ No newline at end of file diff --git a/api/app/application/lenses_repository.py b/api/app/application/lenses_repository.py index d88c519..9061964 100644 --- a/api/app/application/lenses_repository.py +++ b/api/app/application/lenses_repository.py @@ -1,6 +1,6 @@ from typing import Sequence, Optional, Tuple, Literal -from sqlalchemy import select, desc, or_, func +from sqlalchemy import select, desc, or_, func, String from sqlalchemy.ext.asyncio import AsyncSession from app.domain.models import Lens @@ -11,18 +11,18 @@ class LensesRepository: self.db = db 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, + 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) @@ -31,24 +31,26 @@ class LensesRepository: 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), + Lens.tor.cast(String).ilike(search), + Lens.diameter.cast(String).ilike(search), + Lens.preset_refraction.cast(String).ilike(search), + Lens.periphery_toricity.cast(String).ilike(search), + Lens.side.cast(String).ilike(search), + Lens.fvc.cast(String).ilike(search), + Lens.trial.cast(String).ilike(search), + Lens.esa.cast(String).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), + Lens.tor.cast(String).ilike(search), + Lens.diameter.cast(String).ilike(search), + Lens.preset_refraction.cast(String).ilike(search), + Lens.periphery_toricity.cast(String).ilike(search), + Lens.side.cast(String).ilike(search), + Lens.fvc.cast(String).ilike(search), + Lens.trial.cast(String).ilike(search), + Lens.esa.cast(String).ilike(search), ) ) diff --git a/api/app/infrastructure/appointment_files_service.py b/api/app/infrastructure/appointment_files_service.py index 0a3d75c..176a64f 100644 --- a/api/app/infrastructure/appointment_files_service.py +++ b/api/app/infrastructure/appointment_files_service.py @@ -3,7 +3,6 @@ import uuid from typing import Optional import aiofiles -import magic from fastapi import UploadFile, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from starlette.responses import FileResponse diff --git a/api/k8s/helm/visus-api/templates/deployment.yaml b/api/k8s/helm/visus-api/templates/deployment.yaml index d0741e2..8b31812 100644 --- a/api/k8s/helm/visus-api/templates/deployment.yaml +++ b/api/k8s/helm/visus-api/templates/deployment.yaml @@ -24,6 +24,16 @@ spec: secretKeyRef: name: visus-api-secret key: SECRET_KEY + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: visus-api-secret + key: DATABASE_URL + - name: ALGORITHM + valueFrom: + secretKeyRef: + name: visus-api-secret + key: ALGORITHM volumeMounts: - name: uploads-volume mountPath: {{ .Values.persistence.uploads.containerPath }} diff --git a/web-app/.dockerignore b/web-app/.dockerignore index 6c26e73..593eedb 100644 --- a/web-app/.dockerignore +++ b/web-app/.dockerignore @@ -3,4 +3,5 @@ npm-debug.log build .dockerignore .git -k8s \ No newline at end of file +k8s +.env \ No newline at end of file diff --git a/web-app/k8s/helm/visus-web/templates/secrets-example.yaml b/web-app/k8s/helm/visus-web/templates/secrets-example.yaml deleted file mode 100644 index bd58ee5..0000000 --- a/web-app/k8s/helm/visus-web/templates/secrets-example.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: visus-api-secret -type: Opaque -data: - SECRET_KEY: - DATABASE_URL: diff --git a/web-app/src/Api/lensesApi.js b/web-app/src/Api/lensesApi.js index 52e932d..1d6953c 100644 --- a/web-app/src/Api/lensesApi.js +++ b/web-app/src/Api/lensesApi.js @@ -8,9 +8,30 @@ export const lensesApi = createApi({ tagTypes: ['Lens'], endpoints: (builder) => ({ getLenses: builder.query({ - query: () => '/lenses/', - providesTags: ['Lens'], - refetchOnMountOrArgChange: 5 + query: ({page, pageSize, search, sortOrder, searchParams}) => ({ + url: '/lenses/', + params: { + page, + page_size: pageSize, + search: search || undefined, + sort_order: sortOrder || undefined, + tor: searchParams.tor || undefined, + diameter: searchParams.diameter || undefined, + preset_refraction: searchParams.preset_refraction || undefined, + periphery_toricity: searchParams.periphery_toricity || undefined, + side: searchParams.side !== 'all' ? searchParams.side : undefined, + issued: searchParams.issued !== 'all' ? searchParams.issued : undefined, + trial: searchParams.trial || undefined, + }, + }), + providesTags: ['Lenses'], + transformResponse: (response) => { + if (!response || !Array.isArray(response.lenses)) { + console.warn('Unexpected lenses API response:', response); + return {lenses: [], total_count: 0}; + } + return response; + }, }), getNotIssuedLenses: builder.query({ query: () => '/lenses/not_issued/', 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 cca4a81..8ff82c5 100644 --- a/web-app/src/Components/Pages/LensesSetsPage/Components/LensesTab/LensesTab.jsx +++ b/web-app/src/Components/Pages/LensesSetsPage/Components/LensesTab/LensesTab.jsx @@ -148,8 +148,8 @@ const LensesTab = () => { lensesData.handleSetSearchText(e.target.value)} + value={lensesData.tempSearchText} + onChange={(e) => lensesData.handleSetTempSearchText(e.target.value)} style={lensesData.formItemStyle} allowClear /> diff --git a/web-app/src/Components/Pages/LensesSetsPage/Components/LensesTab/useLenses.js b/web-app/src/Components/Pages/LensesSetsPage/Components/LensesTab/useLenses.js index 9bc8923..786da08 100644 --- a/web-app/src/Components/Pages/LensesSetsPage/Components/LensesTab/useLenses.js +++ b/web-app/src/Components/Pages/LensesSetsPage/Components/LensesTab/useLenses.js @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from "react"; +import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { notification } from "antd"; import { @@ -16,14 +16,14 @@ import { setSearchParams, setSearchText, setShowAdvancedSearch, - setViewMode + setViewMode, + setSortOrder } from "../../../../../Redux/Slices/lensesSlice.js"; import { getCachedInfo } from "../../../../../Utils/cachedInfoUtils.js"; const useLenses = () => { const dispatch = useDispatch(); const { - searchText, viewMode, currentPage, pageSize, @@ -31,16 +31,36 @@ const useLenses = () => { isModalVisible, showAdvancedSearch, searchParams, + sortOrder, } = useSelector(state => state.lensesUI); - const { data = { lenses: [], total_count: 0 }, isLoading, isError } = useGetLensesQuery( - { page: currentPage, pageSize, search: searchText || undefined }, + const [tempSearchText, setTempSearchText] = useState(""); + + const { data = { lenses: [], total_count: 0 }, isLoading, isError, error } = useGetLensesQuery( + { + page: currentPage, + pageSize, + search: tempSearchText || undefined, + sortOrder, + searchParams, + }, { pollingInterval: 20000, refetchOnMountOrArgChange: true, } ); + useEffect(() => { + if (isError) { + console.error('useGetLensesQuery error:', error); + notification.error({ + message: "Ошибка загрузки данных", + description: error?.data?.detail || "Не удалось загрузить линзы", + placement: "topRight", + }); + } + }, [isError, error]); + const [addLens, { isLoading: isAdding }] = useAddLensMutation(); const [updateLens, { isLoading: isUpdating }] = useUpdateLensMutation(); const [deleteLens, { isLoading: isDeleting }] = useDeleteLensMutation(); @@ -58,9 +78,24 @@ const useLenses = () => { const tableActionButtonsStyle = { display: "flex", gap: "8px" }; const advancedSearchCardStyle = { marginBottom: 20 }; - const handleSetSearchText = (value) => { - dispatch(setSearchText(value)); - dispatch(setCurrentPage(1)); // Сбрасываем на первую страницу при изменении поиска + const handleSetTempSearchText = (value) => { + setTempSearchText(value); + }; + + const handleSearch = () => { + dispatch(setSearchText(tempSearchText)); + dispatch(setCurrentPage(1)); + }; + + const handleClearSearch = () => { + setTempSearchText(''); + dispatch(setSearchText('')); + dispatch(setCurrentPage(1)); + }; + + const handleSetSortOrder = (value) => { + dispatch(setSortOrder(value)); + dispatch(setCurrentPage(1)); }; const handleCloseModal = () => dispatch(closeModal()); @@ -85,6 +120,7 @@ const useLenses = () => { } else { dispatch(setSearchParams({ ...searchParams, [param]: value })); } + dispatch(setCurrentPage(1)); }; const toggleAdvancedSearch = () => { @@ -140,34 +176,10 @@ const useLenses = () => { } }; - const filteredLenses = useMemo(() => { - return data.lenses.filter((lens) => { - const textMatch = Object.values(lens).some((value) => - value?.toString().toLowerCase().includes(searchText.toLowerCase()) - ); - - const advancedMatch = Object.entries(searchParams).every(([key, value]) => { - if (value === null || value === '') return true; - if (key === 'side') { - if (value === 'all') return true; - return lens.side === value; - } - if (key === 'issued') { - return lens.issued === value || value === "all"; - } - return lens[key] === value; - }); - - return textMatch && advancedMatch && (searchParams.issued || lens.issued === false); - }).sort((a, b) => { - return b.id - a.id; // Сортировка по убыванию id - }); - }, [data.lenses, searchText, searchParams]); - const pagination = { current: currentPage, pageSize: pageSize, - total: data.total_count, + total: data.total_count || 0, showSizeChanger: true, pageSizeOptions: ["5", "10", "20", "50"], onChange: (page, newPageSize) => { @@ -176,12 +188,12 @@ const useLenses = () => { }; return { - lenses: data.lenses, - filteredLenses: filteredLenses.map(lens => ({ ...lens, key: lens.id })), + lenses: data.lenses || [], + filteredLenses: (data.lenses || []).map(lens => ({ ...lens, key: lens.id })), isLoading, isError, isProcessing: isAdding || isUpdating || isDeleting, - searchText, + tempSearchText, viewMode, currentPage, pageSize, @@ -189,6 +201,7 @@ const useLenses = () => { isModalVisible, showAdvancedSearch, searchParams, + sortOrder, pagination, containerStyle, filterBarStyle, @@ -196,7 +209,10 @@ const useLenses = () => { viewModIconStyle, tableActionButtonsStyle, advancedSearchCardStyle, - handleSetSearchText, + handleSetTempSearchText, + handleSearch, + handleClearSearch, + handleSetSortOrder, handleAddLens, handleEditLens, handleCloseModal, diff --git a/web-app/src/Redux/Slices/lensesSlice.js b/web-app/src/Redux/Slices/lensesSlice.js index b4930a3..5060299 100644 --- a/web-app/src/Redux/Slices/lensesSlice.js +++ b/web-app/src/Redux/Slices/lensesSlice.js @@ -1,12 +1,11 @@ -import {createSlice} from '@reduxjs/toolkit' -import {cacheInfo} from "../../Utils/cachedInfoUtils.js"; - +import { createSlice } from '@reduxjs/toolkit'; const initialState = { searchText: '', viewMode: 'tile', currentPage: 1, - pageSize: 10, + pageSize: 5, + sortOrder: 'desc', selectedLens: null, isModalVisible: false, showAdvancedSearch: false, @@ -16,57 +15,59 @@ const initialState = { preset_refraction: null, periphery_toricity: null, side: 'all', - issued: false, - trial: null + issued: 'all', + trial: null, }, }; const lensesSlice = createSlice({ - name: 'lensesUI', + name: 'lenses', initialState, reducers: { - setSearchText: (state, action) => { + setSearchText(state, action) { state.searchText = action.payload; }, - setCurrentPage: (state, action) => { + setSortOrder(state, action) { + state.sortOrder = action.payload; + }, + setViewMode(state, action) { + state.viewMode = action.payload; + }, + setCurrentPage(state, action) { state.currentPage = action.payload; }, - setPageSize: (state, action) => { + setPageSize(state, action) { state.pageSize = action.payload; }, - setViewMode: (state, action) => { - state.viewMode = action.payload; - cacheInfo("viewModeLenses", action.payload); - }, - openModal: (state) => { - state.isModalVisible = true; - }, - closeModal: (state) => { - state.isModalVisible = false; - state.selectedLens = null; - }, - selectLens: (state, action) => { + selectLens(state, action) { state.selectedLens = action.payload; }, - setSearchParams: (state, action) => { - state.searchParams = action.payload; + openModal(state) { + state.isModalVisible = true; }, - setShowAdvancedSearch: (state, action) => { + closeModal(state) { + state.isModalVisible = false; + }, + setShowAdvancedSearch(state, action) { state.showAdvancedSearch = action.payload; }, - } + setSearchParams(state, action) { + state.searchParams = action.payload; + }, + }, }); export const { setSearchText, + setSortOrder, + setViewMode, setCurrentPage, setPageSize, - setViewMode, + selectLens, openModal, closeModal, - selectLens, + setShowAdvancedSearch, setSearchParams, - setShowAdvancedSearch } = lensesSlice.actions; export default lensesSlice.reducer; \ No newline at end of file