298 lines
9.1 KiB
JavaScript
298 lines
9.1 KiB
JavaScript
import { useEffect, useMemo, useState } from 'react';
|
||
import { ProjectsApi } from '../../api/projects';
|
||
import { CardsSkeleton } from '../../components/common/Skeleton';
|
||
import SearchComponent from '../../components/common/SearchComponent';
|
||
import { ProjectCardComponent } from './ProjectCard';
|
||
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
||
import { PageContainer, TasksContainer } from '../StyledForPage';
|
||
import {
|
||
Box,
|
||
FormControl,
|
||
Button,
|
||
Stack,
|
||
Pagination,
|
||
PaginationItem,
|
||
Select,
|
||
MenuItem,
|
||
Typography,
|
||
IconButton,
|
||
Tooltip,
|
||
TextField,
|
||
Autocomplete,
|
||
} from '@mui/material';
|
||
import { toast } from 'react-toastify';
|
||
import { ModalCreateProject } from './ModalCreateProject';
|
||
import {
|
||
ROLES_NAME_ID,
|
||
} from '../../constants/constants';
|
||
import { useAuth } from '../../app/context/AuthProvider';
|
||
import { PrimaryButton, PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
|
||
import {
|
||
ExportExitButton,
|
||
ExportWithTextButton,
|
||
} from '../../components/common/Buttons/ButtonsActions';
|
||
import { exportBulkForms, exportSingleForm } from '../../utils/exportFile';
|
||
import { WhitePlus } from '../../components/common/icons/icons';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { PaginatedList } from '../../components/common/PaginatedList';
|
||
import { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig';
|
||
import { SspApi } from '../../api/ssp';
|
||
import NavigationToggle from '../../components/common/NavigationToggle';
|
||
|
||
export { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig';
|
||
|
||
export default function ProjectsPage() {
|
||
const navigate = useNavigate();
|
||
const [items, setItems] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
|
||
// Новые фильтры
|
||
const [yearFilter, setYearFilter] = useState('');
|
||
const [branchFilter, setBranchFilter] = useState('');
|
||
|
||
// Pagination state
|
||
const [page, setPage] = useState(1);
|
||
const [limit, setLimit] = useState(20);
|
||
const [totalCount, setTotalCount] = useState(0);
|
||
|
||
const { user } = useAuth();
|
||
|
||
const [exportMode, setExportMode] = useState(false);
|
||
const [exportList, setExportList] = useState([]);
|
||
|
||
const [projectData, setProjectData] = useState(DEFAULT_PROJECT_CONFIG);
|
||
|
||
// Получаем список годов для фильтра (можно динамически или задать диапазон)
|
||
const yearOptions = useMemo(() => {
|
||
const currentYear = new Date().getFullYear();
|
||
const years = [];
|
||
for (let year = currentYear - 5; year <= currentYear + 2; year++) {
|
||
years.push(year);
|
||
}
|
||
return years;
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!user) return;
|
||
if (user.role_id == ROLES_NAME_ID.admin) {
|
||
SspApi.getAll().then(data => {
|
||
if (data.success) {
|
||
setProjectData(prev => ({
|
||
...prev, 'ССП/РФ': {
|
||
...prev['ССП/РФ'],
|
||
options: data.result
|
||
}
|
||
}))
|
||
return;
|
||
}
|
||
toast.error('Ошибка при получении ССП/РФ');
|
||
});
|
||
return;
|
||
}
|
||
setProjectData(prev => ({
|
||
...prev, 'ССП/РФ': {
|
||
...prev['ССП/РФ'],
|
||
options: user.org_units,
|
||
}
|
||
}))
|
||
}, [user])
|
||
|
||
const openModal = () => {
|
||
setModalOpen(true);
|
||
};
|
||
|
||
const mapFormDataToApi = (formData) => {
|
||
const apiData = {
|
||
name: formData['Название проекта'] || '',
|
||
project_type: formData['Тип проекта развития'] || null,
|
||
vsp_format: formData['Формат ВСП'] || null,
|
||
object_address: formData['Адрес объекта'] || '',
|
||
staff_count: formData['Целевая численность, чел.'] || 0,
|
||
placement_type: formData['Тип размещения'] || null,
|
||
total_area: formData['Площадь размещения, м2'] || 0,
|
||
year: formData['Год'] || null,
|
||
branch_id: formData['ССП/РФ'] || null,
|
||
|
||
};
|
||
return apiData;
|
||
};
|
||
|
||
const handleCreateProject = async (formData) => {
|
||
try {
|
||
const apiData = mapFormDataToApi(formData);
|
||
|
||
await ProjectsApi.create(apiData);
|
||
|
||
setPage(1);
|
||
loadProjects(1, limit);
|
||
|
||
setModalOpen(false);
|
||
toast.success('Проект успешно создан');
|
||
} catch (error) {
|
||
toast.error(
|
||
error.response?.data?.detail ||
|
||
error.response?.data?.message ||
|
||
'Ошибка при создании проекта',
|
||
);
|
||
}
|
||
};
|
||
|
||
const loadProjects = async (currentPage = page, currentLimit = limit) => {
|
||
setLoading(true);
|
||
try {
|
||
const skip = (currentPage - 1) * currentLimit;
|
||
const params = {
|
||
offset: skip,
|
||
limit: currentLimit,
|
||
// Добавляем фильтры в параметры запроса
|
||
...(yearFilter && { year: parseInt(yearFilter) }),
|
||
...(branchFilter && { branch_id: parseInt(branchFilter.id) }),
|
||
};
|
||
|
||
const res = await ProjectsApi.list(params);
|
||
const data = res.result;
|
||
|
||
setItems(data);
|
||
setTotalCount(res.count);
|
||
|
||
} catch (error) {
|
||
toast.error('Ошибка загрузки проектов');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// Сбрасываем страницу при изменении любых фильтров
|
||
useEffect(() => {
|
||
setPage(1);
|
||
loadProjects(1, limit);
|
||
}, [yearFilter, branchFilter]);
|
||
|
||
const handlePageChange = (event, value) => {
|
||
setPage(value);
|
||
loadProjects(value, limit);
|
||
};
|
||
|
||
const handleLimitChange = (event) => {
|
||
const newLimit = event.target.value;
|
||
setLimit(newLimit);
|
||
setPage(1);
|
||
loadProjects(1, newLimit);
|
||
};
|
||
|
||
// Сброс всех фильтров
|
||
const handleClearFilters = () => {
|
||
setYearFilter('');
|
||
setBranchFilter('');
|
||
setPage(1);
|
||
};
|
||
|
||
useEffect(() => {
|
||
loadProjects(page, limit);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
setExportList([]);
|
||
}, [exportMode]);
|
||
|
||
const handleChangeToExportList = (projectId, checked) => {
|
||
if (checked) {
|
||
setExportList((prev) => [...prev, projectId]);
|
||
} else {
|
||
const newExportList = exportList.filter((t) => t != projectId);
|
||
setExportList(newExportList);
|
||
}
|
||
};
|
||
|
||
const handleBulkExport = async () => {
|
||
await exportBulkForms(exportList);
|
||
setExportMode(false);
|
||
};
|
||
|
||
const handleSingleExport = (projectId, projectTitle) => {
|
||
exportSingleForm(projectId, projectTitle);
|
||
};
|
||
|
||
const handleRefresh = () => {
|
||
loadProjects(page, limit);
|
||
toast.info('Список проектов обновлен');
|
||
};
|
||
|
||
if (!user) return null;
|
||
|
||
return (
|
||
<>
|
||
<PageContainer>
|
||
<HeaderSwitch />
|
||
<NavigationToggle currentView={'projects'} onViewChange={(path) => navigate(path)} />
|
||
|
||
<PaginatedList
|
||
items={items}
|
||
loading={loading}
|
||
totalCount={totalCount}
|
||
page={page}
|
||
limit={limit}
|
||
onPageChange={handlePageChange}
|
||
onLimitChange={handleLimitChange}
|
||
entityName="проектов"
|
||
emptyMessage="Проекты не найдены"
|
||
renderItem={(project, exportProps) => (
|
||
<ProjectCardComponent
|
||
key={project.id}
|
||
project={project}
|
||
projectDataInit={projectData}
|
||
{...exportProps}
|
||
/>
|
||
)}
|
||
getItemId={(project) => project.id}
|
||
getItemTitle={(project) => project.name || 'Проект'}
|
||
extraActions={
|
||
user.role_id === ROLES_NAME_ID.admin && (
|
||
<PrimaryButton onClick={openModal} startIcon={<WhitePlus />} height='2.5rem' >
|
||
Создать проект
|
||
</PrimaryButton>
|
||
)
|
||
}
|
||
filterActions={
|
||
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', alignItems: 'center', height: '2.5rem' }}>
|
||
<Select
|
||
size="small"
|
||
displayEmpty
|
||
value={yearFilter}
|
||
onChange={(e) => setYearFilter(e.target.value)}
|
||
sx={{ width: 130 }}
|
||
>
|
||
<MenuItem value="">Все годы</MenuItem>
|
||
{yearOptions.map((year) => (
|
||
<MenuItem key={year} value={year}>{year}</MenuItem>
|
||
))}
|
||
</Select>
|
||
|
||
<Autocomplete
|
||
size="small"
|
||
options={projectData['ССП/РФ']?.options ?? []}
|
||
noOptionsText="Не найдено"
|
||
getOptionLabel={(option) => option.title || option.name || '-'}
|
||
value={branchFilter}
|
||
onChange={(event, newValue) => setBranchFilter(newValue)}
|
||
sx={{ width: 200 }}
|
||
renderInput={(params) => (
|
||
<TextField {...params} placeholder="Все" size="small" />
|
||
)}
|
||
/>
|
||
<PrimaryOutlinedButton onClick={handleClearFilters} height="100%"> Сбросить</PrimaryOutlinedButton>
|
||
</Box>
|
||
}
|
||
/>
|
||
</PageContainer>
|
||
|
||
<ModalCreateProject
|
||
open={modalOpen}
|
||
onClose={() => setModalOpen(false)}
|
||
onCreate={handleCreateProject}
|
||
config={projectData}
|
||
/>
|
||
</>
|
||
);
|
||
} |