From b51b8dc0a5c092fdacf1c41cf44e1e1bfed0615a Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Fri, 21 Aug 2026 12:37:07 +0300 Subject: [PATCH] filter for project status --- web/src/constants/constants.jsx | 8 ++ .../AuditLogsPage/utils/transformers.jsx | 9 +- web/src/pages/ProjectPages/SummaryPage.jsx | 127 ++++++++++++++++-- .../components/EditModal/EditModal.jsx | 6 +- .../components/StatusBadge/StatusBadge.jsx | 8 +- .../components/TableFilters/TableFilters.jsx | 37 ++++- 6 files changed, 163 insertions(+), 32 deletions(-) diff --git a/web/src/constants/constants.jsx b/web/src/constants/constants.jsx index 093d1d6..9d719f3 100644 --- a/web/src/constants/constants.jsx +++ b/web/src/constants/constants.jsx @@ -46,6 +46,14 @@ export const DIRECTION_TRANSLATE = { Support: 'Поддержка', Development: export const PROJECT_TYPES = ['Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ']; +export const PROJECT_STATUSES = { + created: 'Создан', + agreed: 'Согласован', + approved: 'Согласован', + archived: 'В архиве', + deleted: 'Удалён', +}; + export const ORG_UNIT_TYPE_OPTIONS = [ { value: 'ssp', label: 'ССП' }, { value: 'rf', label: 'Региональный филиал' }, diff --git a/web/src/pages/AdminPanel/AuditLogsPage/utils/transformers.jsx b/web/src/pages/AdminPanel/AuditLogsPage/utils/transformers.jsx index bd1fd52..0932701 100644 --- a/web/src/pages/AdminPanel/AuditLogsPage/utils/transformers.jsx +++ b/web/src/pages/AdminPanel/AuditLogsPage/utils/transformers.jsx @@ -8,6 +8,7 @@ import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../../ProjectPages/con import { DIRECTION_TRANSLATE, FORM_TYPE_TRANSLATE, + PROJECT_STATUSES, ROLES_ID_RUSSIAN_NAME, SHEET_NAME, STAGE_ROLE_RUSSIAN_NAME, @@ -50,14 +51,6 @@ export const VALUE_TRANSFORMERS = { 'Дата открытия / переезда / закрытия': transformDateChange, }; -const PROJECT_STATUSES = { - created: 'Создан', - agreed: 'Согласован', - approved: 'Согласован', - archived: 'В архиве', - deleted: 'Удалён', -}; - const PLACEMENT_TYPES = { own: 'Собственность', rent: 'Аренда', diff --git a/web/src/pages/ProjectPages/SummaryPage.jsx b/web/src/pages/ProjectPages/SummaryPage.jsx index 6eb7390..572fac1 100644 --- a/web/src/pages/ProjectPages/SummaryPage.jsx +++ b/web/src/pages/ProjectPages/SummaryPage.jsx @@ -42,6 +42,15 @@ const SummaryPage = () => { // Состояния для фильтров const [selectedBranch, setSelectedBranch] = useState(''); const [searchQuery, setSearchQuery] = useState(''); + const [selectedStatus, setSelectedStatus] = useState(''); + const [archivedData, setArchivedData] = useState([]); + const [archivedTotalCount, setArchivedTotalCount] = useState(0); + const [archivedPagination, setArchivedPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const isArchiveMode = selectedStatus === 'archived'; const loadData = useCallback(async () => { if (!user) return; @@ -94,10 +103,7 @@ const SummaryPage = () => { const loadProjects = async () => { setIsProjectsLoading(true); try { - const response = await ProjectsApi.listWithReports({ - limit: 1000, - ...(selectedBranch?.id ? { branch_id: selectedBranch.id } : {}), - }); + const response = await ProjectsApi.listWithReports({ limit: 1000 }); if (active) setTableData(response.result || []); } catch (requestError) { console.error('Error loading projects with reports:', requestError); @@ -110,7 +116,75 @@ const SummaryPage = () => { return () => { active = false; }; - }, [user, isLoading, selectedBranch?.id, projectsReloadKey]); + }, [user, isLoading, projectsReloadKey]); + + useEffect(() => { + let active = true; + if (!user || isLoading || !isArchiveMode) return undefined; + + const loadArchivedProjects = async () => { + setIsProjectsLoading(true); + try { + const offset = archivedPagination.pageIndex * archivedPagination.pageSize; + const params = { + status: 'archived', + offset, + limit: archivedPagination.pageSize, + ...(selectedBranch?.id ? { branch_id: selectedBranch.id } : {}), + ...(searchQuery.trim() ? { search: searchQuery.trim() } : {}), + }; + + // TODO: после реализации серверных фильтров и пагинации достаточно + // передать params в listWithReports и использовать response.result/count. + // Пока запрос возвращает полный набор, а код ниже имитирует ответ бэка. + const response = await ProjectsApi.listWithReports(params); + const query = searchQuery.toLowerCase().trim(); + const filtered = (response.result || []) + .filter((item) => item.status === 'archived') + .filter((item) => !selectedBranch || item.org_unit_id === selectedBranch.id) + .map((item) => { + if (!query) return item; + return { + ...item, + sub_rows: item.sub_rows?.filter((subItem) => + (subItem.project || '').toLowerCase().includes(query), + ) || [], + }; + }) + .filter((item) => !query + || (item.name || '').toLowerCase().includes(query) + || item.sub_rows.length > 0); + + if (active) { + setArchivedData(filtered.slice(offset, offset + archivedPagination.pageSize)); + setArchivedTotalCount(filtered.length); + } + } catch (requestError) { + console.error('Error loading archived projects:', requestError); + if (active) { + setArchivedData([]); + setArchivedTotalCount(0); + toast.error('Не удалось загрузить архивные проекты'); + } + } finally { + if (active) setIsProjectsLoading(false); + } + }; + + loadArchivedProjects(); + return () => { + active = false; + }; + }, [ + user, + isLoading, + isArchiveMode, + selectedBranch, + searchQuery, + archivedPagination.pageIndex, + archivedPagination.pageSize, + projectsReloadKey, + ]); useEffect(() => { let active = true; @@ -143,12 +217,19 @@ const SummaryPage = () => { }, [selectedRow?.original?.id]); const getFilteredData = () => { - if (!selectedBranch && !searchQuery) { + if (isArchiveMode) { + return archivedData; + } + + if (!selectedBranch && !searchQuery && !selectedStatus) { return tableData; } return tableData .filter((item) => { + if (selectedStatus && item.status !== selectedStatus) { + return false; + } // Фильтр по филиалу (только для корневых) if (selectedBranch) { return item.org_unit_id === selectedBranch.id; @@ -315,9 +396,10 @@ const SummaryPage = () => { const projectsTableOptions = { enableColumnActions: false, enableColumnFilters: false, - enablePagination: false, + enablePagination: isArchiveMode, + manualPagination: isArchiveMode, enableSorting: true, - enableBottomToolbar: false, + enableBottomToolbar: isArchiveMode, enableTopToolbar: false, enableExpanding: true, getSubRows: (row) => row.sub_rows, @@ -390,7 +472,7 @@ const SummaryPage = () => { border: '1px solid #e0e0e0', borderRadius: '1rem', overflow: 'hidden', - height: '45vh', + height: isArchiveMode ? 'auto' : '45vh', display: 'flex', flexDirection: 'column', }, @@ -398,7 +480,8 @@ const SummaryPage = () => { muiTableContainerProps: { sx: { flex: 1, - overflow: 'auto', + overflowX: 'auto', + overflowY: isArchiveMode ? 'visible' : 'auto', '& thead': { position: 'sticky', top: 0, @@ -484,7 +567,14 @@ const SummaryPage = () => { initialState: { columnPinning: { right: ['actions'], left: ['mrt-row-expand', 'project'] }, }, - state: { isLoading: isLoading || isProjectsLoading }, + ...(isArchiveMode ? { + rowCount: archivedTotalCount, + onPaginationChange: setArchivedPagination, + } : {}), + state: { + isLoading: isLoading || isProjectsLoading, + ...(isArchiveMode ? { pagination: archivedPagination } : {}), + }, }); const summaryProjectTable = useMaterialReactTable({ @@ -493,6 +583,8 @@ const SummaryPage = () => { ...summaryProjectTableOptions, localization: { noRecordsToDisplay: 'Нет данных для отображения', + rowsPerPage: 'Строк на странице', + of: 'из', }, initialState: { columnPinning: { left: ['data.header.name'] }, @@ -512,10 +604,19 @@ const SummaryPage = () => { setSelectedRow(null); setSummaryData([]); setSelectedBranch(value); + setArchivedPagination((current) => ({ ...current, pageIndex: 0 })); }; const handleSearchChange = (value) => { setSearchQuery(value); + setArchivedPagination((current) => ({ ...current, pageIndex: 0 })); + }; + + const handleStatusChange = (value) => { + setSelectedRow(null); + setSummaryData([]); + setSelectedStatus(value); + setArchivedPagination((current) => ({ ...current, pageIndex: 0 })); }; const handleCreateProject = async (formData) => { @@ -559,6 +660,8 @@ const SummaryPage = () => { onBranchChange={handleBranchChange} searchQuery={searchQuery} onSearchChange={handleSearchChange} + selectedStatus={selectedStatus} + onStatusChange={handleStatusChange} /> setCreateModalOpen(true)} startIcon={} sx={{ height: '2.5rem' }}> Создать проект @@ -570,7 +673,7 @@ const SummaryPage = () => { sx={{ mb: '2rem', overflow: 'hidden', - flex: '0 0 45vh', + flex: isArchiveMode ? '0 0 auto' : '0 0 45vh', borderRadius: '1rem', boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)', }}> diff --git a/web/src/pages/ProjectPages/components/EditModal/EditModal.jsx b/web/src/pages/ProjectPages/components/EditModal/EditModal.jsx index ba16c16..6fc4d82 100644 --- a/web/src/pages/ProjectPages/components/EditModal/EditModal.jsx +++ b/web/src/pages/ProjectPages/components/EditModal/EditModal.jsx @@ -3,10 +3,12 @@ import { Box, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Men import React from 'react'; import { useAuth } from '../../../../app/context/AuthProvider'; import { DangerOutlinedButton, PrimaryButton } from '../../../../components/common/Buttons/Buttons'; -import { PROJECT_TYPES } from '../../../../constants/constants'; +import { PROJECT_STATUSES, PROJECT_TYPES } from '../../../../constants/constants'; import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../constants'; import { canEditFirstTableField, hasReserveValuesInChildSmetas, isFirstTableFieldDependencySatisfied } from '../../constants/fieldAccess'; +const PROJECT_STATUS_OPTIONS = ['created', 'agreed', 'archived'].map((status) => [status, PROJECT_STATUSES[status]]); + const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNames = {} }) => { const { user } = useAuth(); const [formData, setFormData] = React.useState(rowData || {}); @@ -54,7 +56,7 @@ const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNa const projectFields = [ { key: 'name', label: 'Проект' }, - { key: 'status', label: 'Статус', options: [['created', 'Создан'], ['agreed', 'Согласован'], ['archived', 'В архиве']] }, + { key: 'status', label: 'Статус', options: PROJECT_STATUS_OPTIONS }, { key: 'technical_number', label: 'Технический номер проекта' }, { key: 'org_unit_id', label: 'ССП/РФ', valueType: 'number', options: Object.entries(orgUnitNames).map(([id, title]) => [Number(id), title]) }, { key: 'project_type', label: 'Тип проекта', options: PROJECT_TYPES }, diff --git a/web/src/pages/ProjectPages/components/StatusBadge/StatusBadge.jsx b/web/src/pages/ProjectPages/components/StatusBadge/StatusBadge.jsx index 80ecfc5..4d27dc2 100644 --- a/web/src/pages/ProjectPages/components/StatusBadge/StatusBadge.jsx +++ b/web/src/pages/ProjectPages/components/StatusBadge/StatusBadge.jsx @@ -1,26 +1,22 @@ +import { PROJECT_STATUSES } from '../../../../constants/constants'; import styles from './StatusBadge.module.css'; const StatusBadge = ({ status, className = '', ...props }) => { const statusMap = { deleted: { class: styles.deleted, - label: 'Удален', }, approved: { class: styles.approved, - label: 'Согласован', }, agreed: { class: styles.approved, - label: 'Согласован', }, archived: { class: styles.archived, - label: 'В архиве', }, created: { class: styles.created, - label: 'Создан', }, }; @@ -31,7 +27,7 @@ const StatusBadge = ({ status, className = '', ...props }) => { return ( - {currentStatus.label} + {PROJECT_STATUSES[status]} ); }; diff --git a/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx b/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx index 94ae4c1..75624db 100644 --- a/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx +++ b/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx @@ -1,12 +1,21 @@ import { Search } from '@mui/icons-material'; -import { Autocomplete, Box, Chip, CircularProgress, Paper, TextField } from '@mui/material'; +import { Autocomplete, Box, Chip, CircularProgress, MenuItem, Paper, TextField } from '@mui/material'; import { useEffect, useState, useCallback } from 'react'; import { SspApi } from '../../../../api/ssp'; import { useAuth } from '../../../../app/context/AuthProvider'; import { toast } from 'react-toastify'; -import { ROLES_NAME_ID } from '../../../../constants/constants'; +import { PROJECT_STATUSES, ROLES_NAME_ID } from '../../../../constants/constants'; -const TableFilters = ({ onBranchChange, onSearchChange, selectedBranch, searchQuery }) => { +const FILTER_PROJECT_STATUSES = ['created', 'agreed', 'archived']; + +const TableFilters = ({ + onBranchChange, + onSearchChange, + onStatusChange, + selectedBranch, + selectedStatus, + searchQuery, +}) => { const [branches, setBranches] = useState([]); const [branchInputValue, setBranchInputValue] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -138,7 +147,26 @@ const TableFilters = ({ onBranchChange, onSearchChange, selectedBranch, searchQu }} /> - {(selectedBranch || searchQuery) && ( + onStatusChange(event.target.value)} + sx={{ + minWidth: 180, + maxWidth: 220, + flex: '1 1 auto', + }}> + Все статусы + {FILTER_PROJECT_STATUSES.map((status) => ( + + {PROJECT_STATUSES[status]} + + ))} + + + {(selectedBranch || searchQuery || selectedStatus) && (