feat(stages): фильтры и сортировка в окне «Этапы» (AURORA-1881)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
63c368902b
commit
59f9ebdafe
@ -1,5 +1,5 @@
|
|||||||
import { Box, CircularProgress, Stack } from '@mui/material';
|
import { Box, CircularProgress, Stack } from '@mui/material';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useAuth } from '../../app/context/AuthProvider';
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
import { useTaskStages } from '../../hooks/useTaskStages';
|
import { useTaskStages } from '../../hooks/useTaskStages';
|
||||||
import { useProjectStages } from '../../hooks/useProjectStages';
|
import { useProjectStages } from '../../hooks/useProjectStages';
|
||||||
@ -10,7 +10,9 @@ import { EditContainer } from '../common/Modal/ModalStyled';
|
|||||||
import { WhitePlus } from '../common/icons/icons';
|
import { WhitePlus } from '../common/icons/icons';
|
||||||
import { AddStagesModal } from './AddStagesModal';
|
import { AddStagesModal } from './AddStagesModal';
|
||||||
import { DeleteStageModal } from './DeleteStageModal';
|
import { DeleteStageModal } from './DeleteStageModal';
|
||||||
|
import { StagesFilters } from './StagesFilters';
|
||||||
import { StagesTable } from './StagesTable/StagesTable';
|
import { StagesTable } from './StagesTable/StagesTable';
|
||||||
|
import { EMPTY_STAGE_FILTERS, filterStages, sortStages } from './stageFilters';
|
||||||
|
|
||||||
export const SettingModal = ({
|
export const SettingModal = ({
|
||||||
isOpen = true,
|
isOpen = true,
|
||||||
@ -50,6 +52,24 @@ export const SettingModal = ({
|
|||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [stageToDelete, setStageToDelete] = useState(null);
|
const [stageToDelete, setStageToDelete] = useState(null);
|
||||||
const [sheetOptions, setSheetOptions] = useState([]);
|
const [sheetOptions, setSheetOptions] = useState([]);
|
||||||
|
const [filters, setFilters] = useState(EMPTY_STAGE_FILTERS);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) setFilters(EMPTY_STAGE_FILTERS);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const allStages = stagesInfo.stages || [];
|
||||||
|
const visibleStages = useMemo(
|
||||||
|
() =>
|
||||||
|
filterStages(
|
||||||
|
sortStages(
|
||||||
|
allStages,
|
||||||
|
(sheets || []).map((s) => s.sheet),
|
||||||
|
),
|
||||||
|
filters,
|
||||||
|
),
|
||||||
|
[allStages, sheets, filters],
|
||||||
|
);
|
||||||
|
|
||||||
const handleAddStage = () => {
|
const handleAddStage = () => {
|
||||||
setStageForEdit(null);
|
setStageForEdit(null);
|
||||||
@ -119,11 +139,25 @@ export const SettingModal = ({
|
|||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<StagesTable
|
<>
|
||||||
stages={stagesInfo.stages || []}
|
<StagesFilters
|
||||||
onEdit={handleEditStage}
|
filters={filters}
|
||||||
onDelete={handleDeleteStage}
|
onChange={setFilters}
|
||||||
/>
|
sheets={sheets || []}
|
||||||
|
showSheet={mode !== 'project'}
|
||||||
|
showRole={isAdmin}
|
||||||
|
/>
|
||||||
|
<StagesTable
|
||||||
|
stages={visibleStages}
|
||||||
|
emptyText={
|
||||||
|
allStages.length > 0 && visibleStages.length === 0
|
||||||
|
? 'По выбранным фильтрам этапы не найдены'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onEdit={handleEditStage}
|
||||||
|
onDelete={handleDeleteStage}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<AddStagesModal
|
<AddStagesModal
|
||||||
|
|||||||
56
web/src/components/Stages/StagesFilters.jsx
Normal file
56
web/src/components/Stages/StagesFilters.jsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Box, MenuItem, TextField } from '@mui/material';
|
||||||
|
import { DIRECTION_TRANSLATE, SHEET_NAME, STAGE_ROLE_RUSSIAN_NAME } from '../../constants/constants';
|
||||||
|
import { PrimaryOutlinedButton } from '../common/Buttons/Buttons';
|
||||||
|
import { EMPTY_STAGE_FILTERS, getSheetKey } from './stageFilters';
|
||||||
|
|
||||||
|
const STATUS_OPTIONS = ['Запланирован', 'Активен', 'Завершен', 'Не определен'];
|
||||||
|
|
||||||
|
const getSheetLabel = ({ sheet, direction, name }) => {
|
||||||
|
const title = SHEET_NAME[sheet] || name || sheet;
|
||||||
|
return direction ? `${title} — ${DIRECTION_TRANSLATE[direction] ?? direction}` : title;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StagesFilters = ({ filters, onChange, sheets = [], showSheet = true, showRole = false }) => {
|
||||||
|
const changeFilter = (key, value) => onChange({ ...filters, [key]: value });
|
||||||
|
const sheetOptions = [];
|
||||||
|
for (const item of sheets) {
|
||||||
|
const value = getSheetKey(item);
|
||||||
|
if (!sheetOptions.some((o) => o.value === value)) sheetOptions.push({ value, label: getSheetLabel(item) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const selects = [
|
||||||
|
showSheet && ['sheetKey', 'Лист', sheetOptions],
|
||||||
|
showRole && ['role', 'Роль', Object.entries(STAGE_ROLE_RUSSIAN_NAME).map(([value, label]) => ({ value, label }))],
|
||||||
|
['status', 'Статус', STATUS_OPTIONS.map((value) => ({ value, label: value }))],
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', width: '100%' }}>
|
||||||
|
<TextField
|
||||||
|
size='small'
|
||||||
|
label='Поиск по названию этапа'
|
||||||
|
value={filters.search}
|
||||||
|
onChange={(e) => changeFilter('search', e.target.value)}
|
||||||
|
sx={{ minWidth: 220, flex: 1 }}
|
||||||
|
/>
|
||||||
|
{selects.map(([key, label, options]) => (
|
||||||
|
<TextField
|
||||||
|
key={key}
|
||||||
|
select
|
||||||
|
size='small'
|
||||||
|
label={label}
|
||||||
|
value={filters[key]}
|
||||||
|
onChange={(e) => changeFilter(key, e.target.value)}
|
||||||
|
sx={{ minWidth: 170 }}>
|
||||||
|
<MenuItem value=''>Все</MenuItem>
|
||||||
|
{options.map(({ value, label: text }) => (
|
||||||
|
<MenuItem key={value} value={value}>
|
||||||
|
{text}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
))}
|
||||||
|
<PrimaryOutlinedButton onClick={() => onChange({ ...EMPTY_STAGE_FILTERS })}>Сбросить</PrimaryOutlinedButton>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -9,7 +9,7 @@ import { getStageKey } from '../utils';
|
|||||||
import { StyledTable, StyledTableContainer, StyledTableHead, StyledTableHeadRow, StyledTableRow } from './StagesTable.style';
|
import { StyledTable, StyledTableContainer, StyledTableHead, StyledTableHeadRow, StyledTableRow } from './StagesTable.style';
|
||||||
import { getStageStatus, getStatusColors } from './utils';
|
import { getStageStatus, getStatusColors } from './utils';
|
||||||
|
|
||||||
export const StagesTable = ({ stages, onEdit, onDelete }) => {
|
export const StagesTable = ({ stages, onEdit, onDelete, emptyText = 'Нет данных для отображения' }) => {
|
||||||
const [anchorEl, setAnchorEl] = useState(null);
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
const [selectedStage, setSelectedStage] = useState(null);
|
const [selectedStage, setSelectedStage] = useState(null);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@ -57,7 +57,7 @@ export const StagesTable = ({ stages, onEdit, onDelete }) => {
|
|||||||
{!stages || stages.length === 0 ? (
|
{!stages || stages.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} align='center'>
|
<TableCell colSpan={5} align='center'>
|
||||||
<Typography variant='body2'> Нет данных для отображения</Typography>
|
<Typography variant='body2'>{emptyText}</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -6,7 +6,11 @@ export const DIRECTION_ORDER = ['Support', 'Development'];
|
|||||||
|
|
||||||
export const getSheetKey = ({ sheet, direction }) => `${sheet}|${direction ?? ''}`;
|
export const getSheetKey = ({ sheet, direction }) => `${sheet}|${direction ?? ''}`;
|
||||||
|
|
||||||
const normalize = (value) => String(value ?? '').trim().toLowerCase().replaceAll('ё', 'е');
|
const normalize = (value) =>
|
||||||
|
String(value ?? '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll('ё', 'е');
|
||||||
|
|
||||||
export const filterStages = (stages, filters = EMPTY_STAGE_FILTERS, now = new Date()) => {
|
export const filterStages = (stages, filters = EMPTY_STAGE_FILTERS, now = new Date()) => {
|
||||||
const { search = '', sheetKey = '', role = '', status = '' } = filters ?? {};
|
const { search = '', sheetKey = '', role = '', status = '' } = filters ?? {};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user