Compare commits

..

No commits in common. "af112285bcc7d3218354d48545fee960ef6ab593" and "db1df61f3774ba0c69e069b9389cb9acbd156b4a" have entirely different histories.

5 changed files with 147 additions and 281 deletions

View File

@ -1,6 +1,9 @@
import api from './client'; import api from './client';
export const TasksApi = { export const TasksApi = {
getById(taskId) {
return api.get(`/form/${taskId}`).then((r) => r.data);
},
list(params = {}) { list(params = {}) {
return api.get('/form/', { params }).then((r) => r.data); return api.get('/form/', { params }).then((r) => r.data);
}, },

View File

@ -1,36 +1,25 @@
import { Box, Typography } from '@mui/material'; import { Box, Typography } from '@mui/material';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState, useCallback } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { ProjectsApi } from '../../api/projects'; import { ProjectsApi } from '../../api/projects';
import { SspApi } from '../../api/ssp'; import { SspApi } from '../../api/ssp';
import { useAuth } from '../../app/context/AuthProvider';
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
import { DangerOutlinedButton, PrimaryButton } from '../../components/common/Buttons/Buttons'; import { DangerOutlinedButton, PrimaryButton } from '../../components/common/Buttons/Buttons';
import Modal from '../../components/common/Modal/Modal';
import { WhitePlus } from '../../components/common/icons/icons'; import { WhitePlus } from '../../components/common/icons/icons';
import { ROLES_NAME_ID } from '../../constants/constants'; import Modal from '../../components/common/Modal/Modal';
import { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig'; import { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig';
import { ModalCreateProject } from '../ProjectsPage/ModalCreateProject'; import { ModalCreateProject } from '../ProjectsPage/ModalCreateProject';
import EditModal from './components/EditModal/EditModal'; import EditModal from './components/EditModal/EditModal';
import ProjectSummaryTable from './components/ProjectSummaryTable/ProjectSummaryTable';
import ProjectsTable from './components/ProjectsTable/ProjectsTable'; import ProjectsTable from './components/ProjectsTable/ProjectsTable';
import ProjectSummaryTable from './components/ProjectSummaryTable/ProjectSummaryTable';
import TableFilters from './components/TableFilters/TableFilters'; // Импортируем компонент фильтров import TableFilters from './components/TableFilters/TableFilters'; // Импортируем компонент фильтров
import { handleNavigateClick } from './utils/tableHandlers'; import { handleNavigateClick } from './utils/tableHandlers';
import { useAuth } from '../../app/context/AuthProvider';
import { ROLES_NAME_ID } from '../../constants/constants';
const NavigationProjectPage = () => { const NavigationProjectPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const branchIdsParam = searchParams.get('branch_ids') || '';
const branchIds = useMemo(
() =>
branchIdsParam
.split(',')
.filter(Boolean)
.map(Number)
.filter((value) => Number.isInteger(value) && value > 0),
[branchIdsParam],
);
const [selectedRow, setSelectedRow] = useState(null); const [selectedRow, setSelectedRow] = useState(null);
const [editModalOpen, setEditModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false);
@ -51,7 +40,7 @@ const NavigationProjectPage = () => {
const { user } = useAuth(); const { user } = useAuth();
// Состояния для фильтров // Состояния для фильтров
const [selectedBranches, setSelectedBranches] = useState([]); const [selectedBranch, setSelectedBranch] = useState('');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [selectedStatus, setSelectedStatus] = useState(''); const [selectedStatus, setSelectedStatus] = useState('');
const [archivedData, setArchivedData] = useState([]); const [archivedData, setArchivedData] = useState([]);
@ -70,16 +59,22 @@ const NavigationProjectPage = () => {
const isAdmin = user.role_id === ROLES_NAME_ID.admin; const isAdmin = user.role_id === ROLES_NAME_ID.admin;
if (isAdmin) { if (isAdmin) {
const sspResponse = await SspApi.getAll({ is_active: true }); const sspResponse = await (
SspApi.getAll({ is_active: true })
);
if (!sspResponse.success) { if (!sspResponse.success) {
toast.error('Ошибка загрузки списка ССП'); toast.error('Ошибка загрузки списка ССП');
return; return;
} }
setOrgUnits(sspResponse.result); setOrgUnits(sspResponse.result);
setOrgUnitNames(Object.fromEntries((sspResponse.result || []).map((orgUnit) => [orgUnit.id, orgUnit.title]))); setOrgUnitNames(
Object.fromEntries((sspResponse.result || []).map((orgUnit) => [orgUnit.id, orgUnit.title])),
);
} else { } else {
setOrgUnits(user.org_units ?? []); setOrgUnits(user.org_units ?? []);
setOrgUnitNames(Object.fromEntries((user.org_units || []).map((orgUnit) => [orgUnit.id, orgUnit.title]))); setOrgUnitNames(
Object.fromEntries((user.org_units || []).map((orgUnit) => [orgUnit.id, orgUnit.title])),
);
} }
} catch (error) { } catch (error) {
console.error('Error loading data:', error); console.error('Error loading data:', error);
@ -93,20 +88,13 @@ const NavigationProjectPage = () => {
loadData(); loadData();
}, [loadData]); }, [loadData]);
useEffect(() => { const createProjectConfig = useMemo(() => ({
setSelectedBranches(orgUnits.filter(({ id }) => branchIds.includes(id))); ...DEFAULT_PROJECT_CONFIG,
}, [branchIds, orgUnits]); 'ССП/РФ': {
...DEFAULT_PROJECT_CONFIG['ССП/РФ'],
const createProjectConfig = useMemo( options: orgUnits,
() => ({ },
...DEFAULT_PROJECT_CONFIG, }), [orgUnits]);
'ССП/РФ': {
...DEFAULT_PROJECT_CONFIG['ССП/РФ'],
options: orgUnits,
},
}),
[orgUnits],
);
useEffect(() => { useEffect(() => {
let active = true; let active = true;
@ -115,7 +103,7 @@ const NavigationProjectPage = () => {
const loadProjects = async () => { const loadProjects = async () => {
setIsProjectsLoading(true); setIsProjectsLoading(true);
try { try {
const response = await ProjectsApi.listWithReports(); const response = await ProjectsApi.listWithReports({ limit: 1000 });
if (active) setTableData(response.result || []); if (active) setTableData(response.result || []);
} catch (requestError) { } catch (requestError) {
console.error('Error loading projects with reports:', requestError); console.error('Error loading projects with reports:', requestError);
@ -142,7 +130,7 @@ const NavigationProjectPage = () => {
status_in: 'archived', status_in: 'archived',
offset, offset,
limit: archivedPagination.pageSize, limit: archivedPagination.pageSize,
...(selectedBranches.length ? { branch_ids: selectedBranches.map(({ id }) => id).join(',') } : {}), ...(selectedBranch?.id ? { branch_id: selectedBranch.id } : {}),
...(searchQuery.trim() ? { search: searchQuery.trim() } : {}), ...(searchQuery.trim() ? { search: searchQuery.trim() } : {}),
}; };
@ -172,7 +160,7 @@ const NavigationProjectPage = () => {
user, user,
isLoading, isLoading,
isArchiveMode, isArchiveMode,
selectedBranches, selectedBranch,
searchQuery, searchQuery,
archivedPagination.pageIndex, archivedPagination.pageIndex,
archivedPagination.pageSize, archivedPagination.pageSize,
@ -192,7 +180,7 @@ const NavigationProjectPage = () => {
setIsSummaryLoading(true); setIsSummaryLoading(true);
try { try {
const response = await ProjectsApi.getSummary(selectedRow.original.id); const response = await ProjectsApi.getSummary(selectedRow.original.id);
if (active) setSummaryData(response.result.filter((v) => v.row_type !== 'PROJECT') || []); if (active) setSummaryData(response.result.filter(v => v.row_type !== 'PROJECT') || []);
} catch (requestError) { } catch (requestError) {
console.error('Error loading project summary:', requestError); console.error('Error loading project summary:', requestError);
if (active) { if (active) {
@ -214,7 +202,7 @@ const NavigationProjectPage = () => {
return archivedData; return archivedData;
} }
if (selectedBranches.length === 0 && !searchQuery && !selectedStatus) { if (!selectedBranch && !searchQuery && !selectedStatus) {
return tableData; return tableData;
} }
@ -226,8 +214,8 @@ const NavigationProjectPage = () => {
return false; return false;
} }
// Фильтр по филиалу (только для корневых) // Фильтр по филиалу (только для корневых)
if (selectedBranches.length > 0) { if (selectedBranch) {
return selectedBranches.some(({ id }) => item.org_unit_id === id); return item.org_unit_id === selectedBranch.id;
} }
return true; return true;
}) })
@ -255,7 +243,7 @@ const NavigationProjectPage = () => {
return parentMatches || hasMatchingChildren; return parentMatches || hasMatchingChildren;
}); });
}, [archivedData, isArchiveMode, searchQuery, selectedBranches, selectedStatus, tableData]); }, [archivedData, isArchiveMode, searchQuery, selectedBranch, selectedStatus, tableData]);
const onEdit = useCallback((row) => { const onEdit = useCallback((row) => {
const isSmeta = row.depth > 0; const isSmeta = row.depth > 0;
@ -270,12 +258,9 @@ const NavigationProjectPage = () => {
setEditModalOpen(true); setEditModalOpen(true);
}, []); }, []);
const onNavigate = useCallback( const onNavigate = useCallback((row) => {
(row) => { handleNavigateClick(row, navigate);
handleNavigateClick(row, navigate); }, [navigate]);
},
[navigate],
);
const handleDeleteProject = async () => { const handleDeleteProject = async () => {
if (!projectToDelete?.id) return; if (!projectToDelete?.id) return;
@ -301,8 +286,7 @@ const NavigationProjectPage = () => {
setIsSaving(true); setIsSaving(true);
try { try {
if (formData._rowType === 'smeta') { if (formData._rowType === 'smeta') {
const { is_in_plan, is_in_plan_q2, is_in_plan_q3, is_in_plan_q4, development_block, reserve_to_prrs_ahr, reserve_to_prrs_kv } = const { is_in_plan, is_in_plan_q2, is_in_plan_q3, is_in_plan_q4, development_block, reserve_to_prrs_ahr, reserve_to_prrs_kv } = formData;
formData;
await ProjectsApi.updateSmeta(formData._projectId, formData.year, { await ProjectsApi.updateSmeta(formData._projectId, formData.year, {
is_in_plan, is_in_plan,
is_in_plan_q2, is_in_plan_q2,
@ -313,23 +297,7 @@ const NavigationProjectPage = () => {
reserve_to_prrs_kv: reserve_to_prrs_kv === '' ? null : reserve_to_prrs_kv, reserve_to_prrs_kv: reserve_to_prrs_kv === '' ? null : reserve_to_prrs_kv,
}); });
} else { } else {
const { const { name, status, technical_number, project_type, vsp_format, placement_type, object_address, staff_count, total_area, org_unit_id, krf_decision_date, fk_decision_date, board_decision_date, open_relocate_close_date, funding_by_ko_decision } = formData;
name,
status,
technical_number,
project_type,
vsp_format,
placement_type,
object_address,
staff_count,
total_area,
org_unit_id,
krf_decision_date,
fk_decision_date,
board_decision_date,
open_relocate_close_date,
funding_by_ko_decision,
} = formData;
await ProjectsApi.update(formData._projectId, { await ProjectsApi.update(formData._projectId, {
name, name,
status: status || null, status: status || null,
@ -362,13 +330,7 @@ const NavigationProjectPage = () => {
const handleBranchChange = (value) => { const handleBranchChange = (value) => {
setSelectedRow(null); setSelectedRow(null);
setSummaryData([]); setSummaryData([]);
setSelectedBranches(value); setSelectedBranch(value);
setSearchParams((currentParams) => {
const nextParams = new URLSearchParams(currentParams);
if (value.length > 0) nextParams.set('branch_ids', value.map(({ id }) => id).join(','));
else nextParams.delete('branch_ids');
return nextParams;
});
setArchivedPagination((current) => ({ ...current, pageIndex: 0 })); setArchivedPagination((current) => ({ ...current, pageIndex: 0 }));
}; };
@ -417,10 +379,11 @@ const NavigationProjectPage = () => {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
}}> }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1, }}>
{/* Компонент фильтров */} {/* Компонент фильтров */}
<TableFilters <TableFilters
selectedBranches={selectedBranches} selectedBranch={selectedBranch}
onBranchChange={handleBranchChange} onBranchChange={handleBranchChange}
searchQuery={searchQuery} searchQuery={searchQuery}
onSearchChange={handleSearchChange} onSearchChange={handleSearchChange}
@ -447,17 +410,14 @@ const NavigationProjectPage = () => {
orgUnitNames={orgUnitNames} orgUnitNames={orgUnitNames}
/> />
<ProjectSummaryTable data={summaryData} isLoading={isSummaryLoading} selectedRow={selectedRow} /> <ProjectSummaryTable
data={summaryData}
isLoading={isSummaryLoading}
selectedRow={selectedRow}
/>
{/* Модалка редактирования */} {/* Модалка редактирования */}
<EditModal <EditModal open={editModalOpen} onClose={() => setEditModalOpen(false)} rowData={selectedRowData} onSave={onSaveEdit} isSaving={isSaving} orgUnitNames={orgUnitNames} />
open={editModalOpen}
onClose={() => setEditModalOpen(false)}
rowData={selectedRowData}
onSave={onSaveEdit}
isSaving={isSaving}
orgUnitNames={orgUnitNames}
/>
<ModalCreateProject <ModalCreateProject
open={createModalOpen} open={createModalOpen}
onClose={() => setCreateModalOpen(false)} onClose={() => setCreateModalOpen(false)}
@ -479,10 +439,13 @@ const NavigationProjectPage = () => {
</DangerOutlinedButton> </DangerOutlinedButton>
</> </>
}> }>
<Typography>Проект «{projectToDelete?.name}» будет удалён без возможности восстановления.</Typography> <Typography>
Проект «{projectToDelete?.name}» будет удалён без возможности восстановления.
</Typography>
</Modal> </Modal>
</Box> </Box>
</> </>
); );
}; };

View File

@ -1,14 +1,21 @@
import { Search } from '@mui/icons-material'; import { Search } from '@mui/icons-material';
import { Autocomplete, Box, Chip, CircularProgress, MenuItem, Paper, TextField } from '@mui/material'; import { Autocomplete, Box, Chip, CircularProgress, MenuItem, Paper, TextField } from '@mui/material';
import { useCallback, useEffect, useState } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { SspApi } from '../../../../api/ssp'; import { SspApi } from '../../../../api/ssp';
import { useAuth } from '../../../../app/context/AuthProvider'; import { useAuth } from '../../../../app/context/AuthProvider';
import { toast } from 'react-toastify';
import { PROJECT_STATUSES, ROLES_NAME_ID } from '../../../../constants/constants'; import { PROJECT_STATUSES, ROLES_NAME_ID } from '../../../../constants/constants';
const FILTER_PROJECT_STATUSES = ['created', 'agreed', 'archived']; const FILTER_PROJECT_STATUSES = ['created', 'agreed', 'archived'];
const TableFilters = ({ onBranchChange, onSearchChange, onStatusChange, selectedBranches, selectedStatus, searchQuery }) => { const TableFilters = ({
onBranchChange,
onSearchChange,
onStatusChange,
selectedBranch,
selectedStatus,
searchQuery,
}) => {
const [branches, setBranches] = useState([]); const [branches, setBranches] = useState([]);
const [branchInputValue, setBranchInputValue] = useState(''); const [branchInputValue, setBranchInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -21,14 +28,18 @@ const TableFilters = ({ onBranchChange, onSearchChange, onStatusChange, selected
const isAdmin = user.role_id === ROLES_NAME_ID.admin; const isAdmin = user.role_id === ROLES_NAME_ID.admin;
if (isAdmin) { if (isAdmin) {
const sspResponse = await SspApi.getAll({ is_active: true }); const sspResponse = await (
SspApi.getAll({ is_active: true })
);
if (!sspResponse.success) { if (!sspResponse.success) {
toast.error('Ошибка загрузки списка ССП'); toast.error('Ошибка загрузки списка ССП');
return; return;
} }
setBranches(sspResponse.result); setBranches(sspResponse.result);
} else { } else {
setBranches(user.org_units ?? []); setBranches(user.org_units ?? []);
} }
} catch (error) { } catch (error) {
console.error('Error loading data:', error); console.error('Error loading data:', error);
@ -38,10 +49,15 @@ const TableFilters = ({ onBranchChange, onSearchChange, onStatusChange, selected
} }
}, [user]); }, [user]);
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, [loadData]); }, [loadData]);
useEffect(() => {
setBranchInputValue(selectedBranch?.title || '');
}, [selectedBranch]);
return ( return (
<Paper <Paper
elevation={0} elevation={0}
@ -58,46 +74,29 @@ const TableFilters = ({ onBranchChange, onSearchChange, onStatusChange, selected
maxHeight: '4.5vh', maxHeight: '4.5vh',
}}> }}>
<Autocomplete <Autocomplete
multiple
disableCloseOnSelect
size='small' size='small'
sx={{ sx={{
minWidth: 420, minWidth: 250,
maxWidth: 520, maxWidth: 300,
flex: '1 1 auto', flex: '1 1 auto',
'& .MuiAutocomplete-inputRoot': {
flexWrap: 'nowrap',
},
'& .MuiAutocomplete-input': {
minWidth: '0 !important',
},
}} }}
options={branches} options={branches}
loading={isLoading} loading={isLoading}
getOptionLabel={(option) => option.title || ''} getOptionLabel={(option) => option.title || ''}
getOptionKey={(option) => option.id} getOptionKey={(option) => option.id}
value={selectedBranches} value={selectedBranch || null}
inputValue={branchInputValue} inputValue={branchInputValue}
onInputChange={(_event, newInputValue) => { onInputChange={(event, newInputValue) => {
setBranchInputValue(newInputValue); setBranchInputValue(newInputValue);
}} }}
onChange={(_event, newValue) => { onChange={(event, newValue) => {
onBranchChange(newValue); onBranchChange(newValue || '');
}} }}
renderValue={(value, getItemProps) =>
value
.slice(0, 2)
.map((option, index) => {
const { key, ...itemProps } = getItemProps({ index });
return <Chip {...itemProps} key={option.id || key} label={option.title} size='small' />;
})
.concat(value.length > 2 ? [<Chip key='more' label={`+${value.length - 2}`} size='small' />] : [])
}
renderInput={(params) => { renderInput={(params) => {
return ( return (
<TextField <TextField
{...params} {...params}
placeholder={selectedBranches.length ? '' : 'Региональные филиалы'} placeholder='Региональные филиалы'
variant='outlined' variant='outlined'
size='small' size='small'
slotProps={{ slotProps={{
@ -169,14 +168,14 @@ const TableFilters = ({ onBranchChange, onSearchChange, onStatusChange, selected
))} ))}
</TextField> </TextField>
{(selectedBranches.length > 0 || searchQuery || selectedStatus) && ( {(selectedBranch || searchQuery || selectedStatus) && (
<Box sx={{ display: 'flex', gap: 1, ml: 'auto' }}> <Box sx={{ display: 'flex', gap: 1, ml: 'auto' }}>
<Chip <Chip
label='Сбросить фильтры' label='Сбросить фильтры'
size='small' size='small'
onClick={() => { onClick={() => {
setBranchInputValue(''); setBranchInputValue('');
onBranchChange([]); onBranchChange('');
onSearchChange(''); onSearchChange('');
onStatusChange(''); onStatusChange('');
}} }}

View File

@ -2,7 +2,6 @@ import RefreshRoundedIcon from '@mui/icons-material/RefreshRounded';
import { Alert, Box, Button, Paper, Tab, Tabs, Typography } from '@mui/material'; import { Alert, Box, Button, Paper, Tab, Tabs, Typography } from '@mui/material';
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table'; import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
import { blueColumn } from '../../components/RealtimeTable/constants/columnColors'; import { blueColumn } from '../../components/RealtimeTable/constants/columnColors';
import { sectionCodeColor } from '../../components/RealtimeTable/constants/columnConfig'; import { sectionCodeColor } from '../../components/RealtimeTable/constants/columnConfig';
@ -174,7 +173,6 @@ const filterSummaryTree = (tree, query) =>
const countSummaryTree = (tree) => tree.reduce((count, row) => count + 1 + countSummaryTree(row.subRows), 0); const countSummaryTree = (tree) => tree.reduce((count, row) => count + 1 + countSummaryTree(row.subRows), 0);
export default function SvodPage() { export default function SvodPage() {
const navigate = useNavigate();
const { getSheetFilters, updateSheetFilters, getSummary, invalidateSummary } = useSvod(); const { getSheetFilters, updateSheetFilters, getSummary, invalidateSummary } = useSvod();
const [sheet, setSheet] = useState('MAIN'); const [sheet, setSheet] = useState('MAIN');
const [rows, setRows] = useState([]); const [rows, setRows] = useState([]);
@ -227,22 +225,6 @@ export default function SvodPage() {
loadSummary(); loadSummary();
}, [invalidateSummary, loadSummary, selectedOrganizations, sheet, year]); }, [invalidateSummary, loadSummary, selectedOrganizations, sheet, year]);
const openFilteredTasks = useCallback(() => {
const params = new URLSearchParams({ year: String(year) });
const organizationIds = selectedOrganizations.map(({ id }) => id);
if (organizationIds.length > 0) params.set('org_units', organizationIds.join(','));
if (sheet !== 'MAIN') params.set('form_type', sheet);
navigate(`/tasks?${params.toString()}`);
}, [navigate, selectedOrganizations, sheet, year]);
const openFilteredProjects = useCallback(() => {
const params = new URLSearchParams();
const organizationIds = selectedOrganizations.map(({ id }) => id);
if (organizationIds.length > 0) params.set('branch_ids', organizationIds.join(','));
const query = params.toString();
navigate(query ? `/projects?${query}` : '/projects');
}, [navigate, selectedOrganizations]);
const filteredRows = useMemo(() => { const filteredRows = useMemo(() => {
const normalizedSearch = search.trim().toLowerCase(); const normalizedSearch = search.trim().toLowerCase();
const tree = buildSummaryTree(rows); const tree = buildSummaryTree(rows);
@ -374,22 +356,13 @@ export default function SvodPage() {
{`Найдено ${filteredRowsCount.toLocaleString('ru-RU')} из ${rows.length.toLocaleString('ru-RU')} строк`} {`Найдено ${filteredRowsCount.toLocaleString('ru-RU')} из ${rows.length.toLocaleString('ru-RU')} строк`}
</Typography> </Typography>
)} )}
{sheet === 'FORM_3' ? (
<Button variant='contained' onClick={openFilteredProjects} sx={{ ml: 'auto', height: '2.5rem', flexShrink: 0 }}>
Перейти к проектам
</Button>
) : (
<Button variant='contained' onClick={openFilteredTasks} sx={{ ml: 'auto', height: '2.5rem', flexShrink: 0 }}>
Перейти к задачам
</Button>
)}
<Button <Button
variant='outlined' variant='outlined'
color='default' color='default'
startIcon={<RefreshRoundedIcon />} startIcon={<RefreshRoundedIcon />}
disabled={loading} disabled={loading}
onClick={refreshSummary} onClick={refreshSummary}
sx={{ height: '2.5rem', flexShrink: 0 }}> sx={{ ml: 'auto', height: '2.5rem', flexShrink: 0 }}>
Обновить Обновить
</Button> </Button>
</SvodFilters> </SvodFilters>

View File

@ -1,12 +1,12 @@
import { Autocomplete, Box, Button, FormControl, FormLabel, MenuItem, Select, TextField } from '@mui/material'; import { Autocomplete, Button, FormControl, FormLabel, MenuItem, Select, TextField } from '@mui/material';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useNavigate } from 'react-router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { SspApi } from '../../api/ssp'; import { SspApi } from '../../api/ssp';
import { TasksApi } from '../../api/tasks'; import { TasksApi } from '../../api/tasks';
import { useAuth } from '../../app/context/AuthProvider'; import { useAuth } from '../../app/context/AuthProvider';
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
import { PrimaryButton, PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons'; import { PrimaryButton } from '../../components/common/Buttons/Buttons';
import Modal from '../../components/common/Modal/Modal'; import Modal from '../../components/common/Modal/Modal';
import { OrgUnitsAutocompletePaper } from '../../components/common/OrgUnitsAutocompletePaper/OrgUnitsAutocompletePaper'; import { OrgUnitsAutocompletePaper } from '../../components/common/OrgUnitsAutocompletePaper/OrgUnitsAutocompletePaper';
import { renderOrgUnitValue } from '../../components/common/OrgUnitsAutocompletePaper/renderOrgUnitValue'; import { renderOrgUnitValue } from '../../components/common/OrgUnitsAutocompletePaper/renderOrgUnitValue';
@ -14,6 +14,7 @@ import { PaginatedList } from '../../components/common/PaginatedList';
import { TaskCardComponent } from '../../components/common/TaskCardComponent'; import { TaskCardComponent } from '../../components/common/TaskCardComponent';
import { WhitePlus } from '../../components/common/icons/icons'; import { WhitePlus } from '../../components/common/icons/icons';
import { FORM_TYPE_OPTIONS, FORM_TYPE_TRANSLATE, ORG_UNIT_TYPE_OPTIONS, ROLES_NAME_ID } from '../../constants/constants'; import { FORM_TYPE_OPTIONS, FORM_TYPE_TRANSLATE, ORG_UNIT_TYPE_OPTIONS, ROLES_NAME_ID } from '../../constants/constants';
import { exportBulkForms, exportSingleForm } from '../../utils/exportFile';
import { PageContainer } from '../StyledForPage'; import { PageContainer } from '../StyledForPage';
const modalSelectMenuProps = { const modalSelectMenuProps = {
@ -28,30 +29,19 @@ const modalSelectSx = {
}; };
export default function TasksPage() { export default function TasksPage() {
const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate();
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingUpdate, setLoadingUpdate] = useState(false); const [loadingUpdate, setLoadingUpdate] = useState(false);
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [query, setQuery] = useState('');
const yearFilter = searchParams.get('year') || '';
const formTypeFilter = searchParams.get('form_type') || '';
const orgUnitsFilter = searchParams.get('org_units') || '';
const orgUnitIdsFilter = useMemo(
() =>
orgUnitsFilter
.split(',')
.filter(Boolean)
.map(Number)
.filter((value) => Number.isInteger(value) && value > 0),
[orgUnitsFilter],
);
// Pagination state // Pagination state
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [limit, setLimit] = useState(20); const [limit, setLimit] = useState(20);
const [totalCount, setTotalCount] = useState(0); const [totalCount, setTotalCount] = useState(0);
const [isLoadAll, setisLoadAll] = useState(false);
const [sspList, setSspList] = useState([]); const [sspList, setSspList] = useState([]);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@ -61,13 +51,6 @@ export default function TasksPage() {
formTypeCode: '', formTypeCode: '',
}); });
const yearOptions = useMemo(() => {
const currentYear = new Date().getFullYear();
return Array.from({ length: 8 }, (_, index) => currentYear - 5 + index);
}, []);
const selectedOrgUnits = useMemo(() => sspList.filter((unit) => orgUnitIdsFilter.includes(unit.id)), [sspList, orgUnitIdsFilter]);
const filteredOrgUnits = useMemo(() => { const filteredOrgUnits = useMemo(() => {
if (!formData.orgUnitType) return []; if (!formData.orgUnitType) return [];
const isSsp = formData.orgUnitType === 'ssp'; const isSsp = formData.orgUnitType === 'ssp';
@ -84,18 +67,8 @@ export default function TasksPage() {
const { user } = useAuth(); const { user } = useAuth();
const updateFilter = (name, value) => { const [exportMode, setExportMode] = useState(false);
setSearchParams((currentParams) => { const [exportList, setExportList] = useState([]);
const nextParams = new URLSearchParams(currentParams);
if (value) {
nextParams.set(name, value);
} else {
nextParams.delete(name);
}
return nextParams;
});
};
const openModal = () => { const openModal = () => {
setModalOpen(true); setModalOpen(true);
}; };
@ -161,78 +134,83 @@ export default function TasksPage() {
handleFormChange('org_unit_id', checked ? filteredOrgUnits.map((unit) => unit.id) : []); handleFormChange('org_unit_id', checked ? filteredOrgUnits.map((unit) => unit.id) : []);
}; };
const loadTasks = useCallback( const loadTasks = async (currentPage = page, currentLimit = limit) => {
async (currentPage, currentLimit) => { if (isLoadAll) return;
setLoading(true); setLoading(true);
try { try {
const skip = (currentPage - 1) * currentLimit; const skip = (currentPage - 1) * currentLimit;
const params = { const params = {
offset: skip, offset: skip,
limit: currentLimit, limit: currentLimit,
...(yearFilter && { year: Number(yearFilter) }), };
...(formTypeFilter && { form_type: formTypeFilter }),
...(orgUnitIdsFilter.length > 0 && { org_units: orgUnitIdsFilter.join(',') }),
};
const res = await TasksApi.list(params); const res = await TasksApi.list(params);
const data = res.result; const data = res.result;
setItems(data); setItems(data);
setTotalCount(res.count); setTotalCount(res.count);
} catch (_error) { } catch (error) {
toast.error('Ошибка загрузки задач'); toast.error('Ошибка загрузки задач');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, };
[yearFilter, formTypeFilter, orgUnitIdsFilter],
);
const handlePageChange = (_event, value) => { useEffect(() => {
setPage(1);
}, [query]);
const handlePageChange = (event, value) => {
setPage(value); setPage(value);
loadTasks(value, limit);
}; };
const handleLimitChange = (event) => { const handleLimitChange = (event) => {
const newLimit = event.target.value; const newLimit = event.target.value;
setLimit(newLimit); setLimit(newLimit);
setPage(1); setPage(1);
loadTasks(1, newLimit);
}; };
useEffect(() => { useEffect(() => {
if (!user) return; if (modalOpen) {
if (user.role_id !== ROLES_NAME_ID.admin) { SspApi.getAll({ is_active: true }).then((data) => {
setSspList(user.org_units || []);
return;
}
SspApi.getAll({ is_active: true })
.then((data) => {
if (data.result) { if (data.result) {
setSspList(data.result); setSspList(data.result);
} }
}) });
.catch(() => toast.error('Ошибка загрузки подразделений')); }
}, [user]); }, [modalOpen]);
useEffect(() => { useEffect(() => {
loadTasks(page, limit); loadTasks(page, limit);
}, [page, limit, loadTasks]); }, []);
const handleFilterChange = (name, value) => { useEffect(() => {
setPage(1); setExportList([]);
updateFilter(name, value); }, [exportMode]);
const handleChangeToExportList = (taskId, checked) => {
if (checked) {
setExportList((prev) => [...prev, taskId]);
} else {
const newExportList = exportList.filter((t) => t != taskId);
setExportList(newExportList);
}
}; };
const handleClearFilters = () => { const handleBulkExport = async () => {
setPage(1); await exportBulkForms(exportList);
setSearchParams((currentParams) => { setExportMode(false);
const nextParams = new URLSearchParams(currentParams);
nextParams.delete('year');
nextParams.delete('form_type');
nextParams.delete('org_units');
return nextParams;
});
}; };
const handleSingleExport = (taskId, taskTitle) => {
exportSingleForm(taskId, taskTitle);
};
// Calculate total pages
const totalPages = Math.ceil(totalCount / limit);
if (!user) return; if (!user) return;
return ( return (
@ -264,56 +242,6 @@ export default function TasksPage() {
</PrimaryButton> </PrimaryButton>
) )
} }
filterActions={
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', alignItems: 'center', minHeight: '2.5rem' }}>
<Select
size='small'
displayEmpty
value={yearFilter}
onChange={(event) => handleFilterChange('year', event.target.value)}
sx={{ width: 130 }}>
<MenuItem value=''>Все годы</MenuItem>
{yearOptions.map((year) => (
<MenuItem key={year} value={year}>
{year}
</MenuItem>
))}
</Select>
<Select
size='small'
displayEmpty
value={formTypeFilter}
onChange={(event) => handleFilterChange('form_type', event.target.value)}
sx={{ width: 210 }}>
<MenuItem value=''>Все формы</MenuItem>
{FORM_TYPE_OPTIONS.map((formType) => (
<MenuItem key={formType} value={formType}>
{FORM_TYPE_TRANSLATE[formType]}
</MenuItem>
))}
</Select>
<Autocomplete
multiple
size='small'
options={sspList}
disableCloseOnSelect
noOptionsText='Не найдено'
getOptionLabel={(option) => option.title || ''}
isOptionEqualToValue={(option, value) => option.id === value.id}
value={selectedOrgUnits}
onChange={(_event, newValue) => handleFilterChange('org_units', newValue.map((unit) => unit.id).join(','))}
renderValue={renderOrgUnitValue}
renderInput={(params) => <TextField {...params} placeholder={selectedOrgUnits.length ? '' : 'Все подразделения'} />}
sx={{ width: 480, maxWidth: '100%' }}
/>
<PrimaryOutlinedButton onClick={handleClearFilters} sx={{ height: '2.5rem' }}>
Сбросить
</PrimaryOutlinedButton>
</Box>
}
/> />
</PageContainer> </PageContainer>
<Modal <Modal
@ -398,7 +326,7 @@ export default function TasksPage() {
isOptionEqualToValue={(option, value) => option.id === value.id} isOptionEqualToValue={(option, value) => option.id === value.id}
renderValue={renderOrgUnitValue} renderValue={renderOrgUnitValue}
value={filteredOrgUnits.filter((ssp) => formData.org_unit_id.includes(ssp.id))} value={filteredOrgUnits.filter((ssp) => formData.org_unit_id.includes(ssp.id))}
onChange={(_event, newValue) => { onChange={(event, newValue) => {
handleFormChange( handleFormChange(
'org_unit_id', 'org_unit_id',
newValue.map((ssp) => ssp.id), newValue.map((ssp) => ssp.id),