search-table-scroll #114

Merged
PotapovaA merged 5 commits from search-table-scroll into test 2026-08-21 15:26:23 +03:00
6 changed files with 163 additions and 32 deletions
Showing only changes of commit b51b8dc0a5 - Show all commits

View File

@ -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: 'Региональный филиал' },

View File

@ -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: 'Аренда',

View File

@ -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}
/>
<PrimaryButton onClick={() => setCreateModalOpen(true)} startIcon={<WhitePlus />} 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)',
}}>

View File

@ -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 },

View File

@ -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 (
<span className={`${styles.badge} ${currentStatus.class} ${className}`} {...props}>
{currentStatus.label}
{PROJECT_STATUSES[status]}
</span>
);
};

View File

@ -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) && (
<TextField
select
size='small'
label='Статус'
value={selectedStatus}
onChange={(event) => onStatusChange(event.target.value)}
sx={{
minWidth: 180,
maxWidth: 220,
flex: '1 1 auto',
}}>
<MenuItem value=''>Все статусы</MenuItem>
{FILTER_PROJECT_STATUSES.map((status) => (
<MenuItem key={status} value={status}>
{PROJECT_STATUSES[status]}
</MenuItem>
))}
</TextField>
{(selectedBranch || searchQuery || selectedStatus) && (
<Box sx={{ display: 'flex', gap: 1, ml: 'auto' }}>
<Chip
label='Сбросить фильтры'
@ -147,6 +175,7 @@ const TableFilters = ({ onBranchChange, onSearchChange, selectedBranch, searchQu
setBranchInputValue('');
onBranchChange('');
onSearchChange('');
onStatusChange('');
}}
sx={{
cursor: 'pointer',