Compare commits

..

No commits in common. "182baaff4e3d5f6f7aba76c3778f05d36d996b74" and "6b564fc34b60bc2661e9932b5c997cc89daa020d" have entirely different histories.

10 changed files with 45 additions and 364 deletions

View File

@ -5,16 +5,12 @@ from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Response from fastapi import APIRouter, Depends, HTTPException, Response
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from src.api.v1.deps import ( from src.api.v1.deps import get_current_active_user_with_set_db
get_current_active_user_with_set_db,
require_executor,
)
from src.db.models.app_user import AppUser from src.db.models.app_user import AppUser
from src.db.session import get_db from src.db.session import get_db
from src.domain.schemas import ( from src.domain.schemas import (
AddForm3LineBody, AddForm3LineBody,
AddProjectBody, AddProjectBody,
AddProjectYearBody,
BaseListResponse, BaseListResponse,
BaseSingleResponse, BaseSingleResponse,
CellPatch, CellPatch,
@ -330,21 +326,6 @@ async def delete_project(
return ResponseBase(success=True, message="Проект удалён") return ResponseBase(success=True, message="Проект удалён")
@router.post("/projects/{project_id}/year")
async def add_project_year(
project_id: int,
body: AddProjectYearBody,
db: AsyncSession = Depends(get_db),
current_user: AppUser = Depends(require_executor),
) -> dict:
project_service = ProjectService(db)
return await project_service.add_project_year(
project_id=project_id,
year=body.year,
user=current_user,
)
@router.post("/projects") @router.post("/projects")
async def add_project( async def add_project(
body: AddProjectBody, body: AddProjectBody,

View File

@ -399,10 +399,6 @@ class UpdProjectBody(BaseModel):
funding_by_ko_decision: Optional[Literal["prrs_budget", "bank_reserve"]] = None funding_by_ko_decision: Optional[Literal["prrs_budget", "bank_reserve"]] = None
class AddProjectYearBody(BaseModel):
year: int
class UpdSmetaBody(BaseModel): class UpdSmetaBody(BaseModel):
is_in_plan: Optional[bool] = None is_in_plan: Optional[bool] = None
is_in_plan_q2: Optional[bool] = None is_in_plan_q2: Optional[bool] = None

View File

@ -109,21 +109,6 @@ class ProjectRepository:
total = int((await self.db.execute(count_query)).scalar() or 0) total = int((await self.db.execute(count_query)).scalar() or 0)
return total, payload return total, payload
@staticmethod
def _serialize_sub_row(rpy: RfProjectYear, smeta: Smeta | None) -> dict:
return {
"id": rpy.id,
"year": rpy.year,
"project": f"Смета_{rpy.year}",
"is_in_plan": smeta.is_in_plan if smeta else None,
"is_in_plan_q2": smeta.is_in_plan_q2 if smeta else None,
"is_in_plan_q3": smeta.is_in_plan_q3 if smeta else None,
"is_in_plan_q4": smeta.is_in_plan_q4 if smeta else None,
"development_block": smeta.development_block if smeta else None,
"reserve_to_prrs_ahr": float(smeta.reserve_to_prrs_ahr) if smeta and smeta.reserve_to_prrs_ahr is not None else None,
"reserve_to_prrs_kv": float(smeta.reserve_to_prrs_kv) if smeta and smeta.reserve_to_prrs_kv is not None else None,
}
@staticmethod @staticmethod
def _serialize_project_with_reports(project: Project, years: list[tuple[RfProjectYear, Smeta | None]]) -> dict: def _serialize_project_with_reports(project: Project, years: list[tuple[RfProjectYear, Smeta | None]]) -> dict:
return { return {
@ -147,7 +132,18 @@ class ProjectRepository:
"funding_by_ko_decision": project.funding_by_ko_decision, "funding_by_ko_decision": project.funding_by_ko_decision,
"created_at": project.created_at.isoformat() if project.created_at else None, "created_at": project.created_at.isoformat() if project.created_at else None,
"sub_rows": [ "sub_rows": [
ProjectRepository._serialize_sub_row(rpy, smeta) {
"id": rpy.id,
"year": rpy.year,
"project": f"Смета_{rpy.year}",
"is_in_plan": smeta.is_in_plan if smeta else None,
"is_in_plan_q2": smeta.is_in_plan_q2 if smeta else None,
"is_in_plan_q3": smeta.is_in_plan_q3 if smeta else None,
"is_in_plan_q4": smeta.is_in_plan_q4 if smeta else None,
"development_block": smeta.development_block if smeta else None,
"reserve_to_prrs_ahr": float(smeta.reserve_to_prrs_ahr) if smeta and smeta.reserve_to_prrs_ahr is not None else None,
"reserve_to_prrs_kv": float(smeta.reserve_to_prrs_kv) if smeta and smeta.reserve_to_prrs_kv is not None else None,
}
for rpy, smeta in years for rpy, smeta in years
], ],
} }
@ -550,34 +546,6 @@ class ProjectRepository:
).scalar_one_or_none() ).scalar_one_or_none()
return self._serialize_smeta(smeta) return self._serialize_smeta(smeta)
async def add_project_year(self, project_id: int, year: int) -> dict | None:
query = text(
"""
SELECT limit_report_id, current_expenses_report_id
FROM v3.add_project_year(
CAST(:project_id AS INT),
CAST(:year AS INT)
)
"""
)
await self.db.execute(query, {"project_id": project_id, "year": year})
await self.db.flush()
row = (
await self.db.execute(
select(RfProjectYear, Smeta)
.outerjoin(Smeta, Smeta.rf_project_year_id == RfProjectYear.id)
.where(
RfProjectYear.project_id == project_id,
RfProjectYear.year == year,
)
)
).first()
if not row:
return None
rpy, smeta = row
return self._serialize_sub_row(rpy, smeta)
async def add_project( async def add_project(
self, self,
name: str, name: str,

View File

@ -62,13 +62,7 @@ class ProjectService:
project["reports"] = await self.project_repo.get_reports(project_id=project_id) project["reports"] = await self.project_repo.get_reports(project_id=project_id)
return project return project
async def get_instance( async def get_instance(self, project_id: int, user: AppUser) -> Project | None:
self,
project_id: int,
user: AppUser | None = None,
org_unit_ids: list[int] | None = None,
) -> Project | None:
if org_unit_ids is None and user is not None:
org_unit_ids = await self._allowed_org_unit_ids(user) org_unit_ids = await self._allowed_org_unit_ids(user)
return await self.project_repo.get_instance(project_id=project_id, org_unit_ids=org_unit_ids) return await self.project_repo.get_instance(project_id=project_id, org_unit_ids=org_unit_ids)
@ -221,28 +215,6 @@ class ProjectService:
await self.project_repo.upd_smeta(rf_project_year_id, data) await self.project_repo.upd_smeta(rf_project_year_id, data)
return await self.project_repo.get_smeta(rf_project_year_id) return await self.project_repo.get_smeta(rf_project_year_id)
async def add_project_year(
self,
project_id: int,
year: int,
user: AppUser,
) -> dict:
org_unit_ids = await self._allowed_org_unit_ids(user)
project = await self.get_instance(
project_id=project_id,
org_unit_ids=org_unit_ids,
)
if not project:
raise ValidationException("Проект не найден")
existing = await self.project_repo.resolve_rf_project_year_id(
project_id=project_id,
year=year,
org_unit_ids=org_unit_ids,
)
if existing is not None:
raise ValidationException(f"Год {year} у проекта уже заведён")
return await self.project_repo.add_project_year(project_id=project_id, year=year)
async def add_project( async def add_project(
self, self,
name: str, name: str,

View File

@ -337,75 +337,3 @@ def test_upd_smeta_not_found(client, admin_tokens, auth_headers):
assert response.status_code == 400 assert response.status_code == 400
payload = response.json() payload = response.json()
assert payload.get("message") == "Смета не найдена" assert payload.get("message") == "Смета не найдена"
def test_add_project_year_smoke(client, admin_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
response = client.post(
f"/api/v1/projects/{project_id}/year",
json={"year": 2027},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
payload = response.json()
assert payload["year"] == 2027
assert payload["project"] == "Смета_2027"
assert payload["id"] is not None
project = _get_project_with_reports(client, auth_headers, admin_tokens, project_id)
assert project is not None
sub_years = {sr["year"] for sr in project["sub_rows"]}
assert 2026 in sub_years
assert 2027 in sub_years
def test_add_project_year_duplicate(client, admin_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
response = client.post(
f"/api/v1/projects/{project_id}/year",
json={"year": 2026},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 400
payload = response.json()
assert payload.get("message") == "Год 2026 у проекта уже заведён"
def test_add_project_year_project_not_found(client, admin_tokens, auth_headers):
response = client.post(
"/api/v1/projects/99999/year",
json={"year": 2027},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 400
payload = response.json()
assert payload.get("message") == "Проект не найден"
def test_add_project_year_by_executor(client, admin_tokens, isp_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers, branch_id=2, year=2026)
response = client.post(
f"/api/v1/projects/{project_id}/year",
json={"year": 2027},
headers=auth_headers(isp_tokens),
)
assert response.status_code == 200
payload = response.json()
assert payload["year"] == 2027
def test_add_project_year_by_executor_from_other_org(client, admin_tokens, isp_tokens, auth_headers):
"""Исполнитель, привязанный к РФ id=2, не может добавить год проекту на ССП id=1 — ACL."""
project_id, _ = _create_project(client, admin_tokens, auth_headers, branch_id=1, year=2026)
response = client.post(
f"/api/v1/projects/{project_id}/year",
json={"year": 2027},
headers=auth_headers(isp_tokens),
)
assert response.status_code == 400
payload = response.json()
assert payload.get("message") == "Проект не найден"

View File

@ -24,10 +24,6 @@ export const ProjectsApi = {
return api.patch(`/projects/${projectId}/smeta/${year}`, data).then((r) => r.data); return api.patch(`/projects/${projectId}/smeta/${year}`, data).then((r) => r.data);
}, },
addYear: (projectId, year) => {
return api.post(`/projects/${projectId}/year`, { year }).then((r) => r.data);
},
delete: (id) => { delete: (id) => {
return api.delete(`/projects/${id}`).then((r) => r.data); return api.delete(`/projects/${id}`).then((r) => r.data);
}, },

View File

@ -1,4 +1,4 @@
import { Box, Typography } from '@mui/material'; import { Box } from '@mui/material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import TableCard from '../TableCard/TableCard'; import TableCard from '../TableCard/TableCard';
@ -16,19 +16,9 @@ const TablesList = ({ filteredTables, query, basePath = '/table', formInfo, isPr
} }
const path = `${basePath}/form/${formInfo.id}/form-type/${isProject ? 'PROJECT' : formInfo.form_type_code}/${table.sheet}/${table?.direction}/${table?.year}`; const path = `${basePath}/form/${formInfo.id}/form-type/${isProject ? 'PROJECT' : formInfo.form_type_code}/${table.sheet}/${table?.direction}/${table?.year}`;
return <TableCard key={`${table.sheet}_${table?.direction}_${table?.year}`} onNavigate={() => handleNavigate(path)} table={table} />; return <TableCard key={table.sheet + '_' + table?.direction} onNavigate={() => handleNavigate(path)} table={table} />;
}; };
const projectTablesByYear = isProject
? Object.entries(
filteredTables.reduce((groups, table) => {
const year = table.year ?? 'Без года';
groups[year] = [...(groups[year] || []), table];
return groups;
}, {}),
).sort(([firstYear], [secondYear]) => Number(secondYear) - Number(firstYear))
: [];
return ( return (
<Box <Box
sx={{ sx={{
@ -48,28 +38,7 @@ const TablesList = ({ filteredTables, query, basePath = '/table', formInfo, isPr
</Box> </Box>
)} )}
{isProject {filteredTables.map((table) => renderTableCard(table, formInfo))}
? projectTablesByYear.map(([year, tables]) => (
<Box key={year} sx={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
py: 0.5,
borderBottom: '1px solid',
borderColor: 'divider',
}}>
<Typography variant='subtitle3' sx={{ fontWeight: 400 }}>
{year === 'Без года' ? year : `${year} год`}
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{tables.map((table) => renderTableCard(table, formInfo))}
</Box>
</Box>
))
: filteredTables.map((table) => renderTableCard(table, formInfo))}
</Box> </Box>
); );
}; };

View File

@ -6,9 +6,8 @@ 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 { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
import { DangerOutlinedButton, PrimaryButton } from '../../components/common/Buttons/Buttons'; import { PrimaryButton } from '../../components/common/Buttons/Buttons';
import { WhitePlus } from '../../components/common/icons/icons'; import { WhitePlus } from '../../components/common/icons/icons';
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 { createFirstColumns, secondColumns } from './columns'; import { createFirstColumns, secondColumns } from './columns';
@ -31,8 +30,6 @@ const SummaryPage = () => {
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [projectsReloadKey, setProjectsReloadKey] = useState(0); const [projectsReloadKey, setProjectsReloadKey] = useState(0);
const [createModalOpen, setCreateModalOpen] = useState(false); const [createModalOpen, setCreateModalOpen] = useState(false);
const [projectToDelete, setProjectToDelete] = useState(null);
const [isDeleting, setIsDeleting] = useState(false);
// Состояния для фильтров // Состояния для фильтров
const [selectedBranch, setSelectedBranch] = useState(''); const [selectedBranch, setSelectedBranch] = useState('');
@ -194,26 +191,6 @@ const SummaryPage = () => {
handleNavigateClick(row, navigate); handleNavigateClick(row, navigate);
}; };
const handleDeleteProject = async () => {
if (!projectToDelete?.id) return;
setIsDeleting(true);
try {
await ProjectsApi.delete(projectToDelete.id);
if (selectedRow?.original?.id === projectToDelete.id) {
setSelectedRow(null);
setSummaryData([]);
}
setProjectToDelete(null);
setProjectsReloadKey((key) => key + 1);
toast.success('Проект успешно удалён');
} catch (requestError) {
console.error('Error deleting project:', requestError);
toast.error(requestError.response?.data?.detail || requestError.response?.data?.message || 'Не удалось удалить проект');
} finally {
setIsDeleting(false);
}
};
const onSaveEdit = async (formData) => { const onSaveEdit = async (formData) => {
setIsSaving(true); setIsSaving(true);
try { try {
@ -259,7 +236,7 @@ const SummaryPage = () => {
}; };
const firstColumns = useMemo( const firstColumns = useMemo(
() => createFirstColumns({ onDelete: setProjectToDelete, onEdit, onNavigate, orgUnitNames }), () => createFirstColumns({ onEdit, onNavigate, orgUnitNames }),
[orgUnitNames], [orgUnitNames],
); );
@ -289,7 +266,7 @@ const SummaryPage = () => {
}, },
}; };
const projectsTableOptions = { const summaryProjectsTableOptions = {
enableColumnActions: false, enableColumnActions: false,
enableColumnFilters: false, enableColumnFilters: false,
enablePagination: false, enablePagination: false,
@ -385,7 +362,7 @@ const SummaryPage = () => {
}, },
}; };
const summaryProjectTableOptions = { const projectTableOptions = {
enableColumnActions: false, enableColumnActions: false,
enableColumnFilters: false, enableColumnFilters: false,
enablePagination: false, enablePagination: false,
@ -446,23 +423,20 @@ const SummaryPage = () => {
}, },
}; };
const projectsTable = useMaterialReactTable({ const summaryProjectsTable = useMaterialReactTable({
columns: firstColumns, columns: firstColumns,
data: filteredData, data: filteredData,
...projectsTableOptions, ...summaryProjectsTableOptions,
initialState: { initialState: {
columnPinning: { right: ['actions'], left: ['mrt-row-expand', 'project'] }, columnPinning: { right: ['actions'], left: ['mrt-row-expand', 'project'] },
}, },
state: { isLoading: isProjectsLoading }, state: { isLoading: isProjectsLoading },
}); });
const summaryProjectTable = useMaterialReactTable({ const projectTable = useMaterialReactTable({
columns: secondColumns, columns: secondColumns,
data: summaryData, data: summaryData,
...summaryProjectTableOptions, ...projectTableOptions,
initialState: {
columnPinning: { left: ['data.header.name'] },
},
state: { isLoading: isSummaryLoading }, state: { isLoading: isSummaryLoading },
}); });
@ -540,7 +514,7 @@ const SummaryPage = () => {
borderRadius: '1rem', borderRadius: '1rem',
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)', boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
}}> }}>
<MaterialReactTable table={projectsTable} /> <MaterialReactTable table={summaryProjectsTable} />
</Paper> </Paper>
{/* Вторая таблица */} {/* Вторая таблица */}
@ -569,7 +543,7 @@ const SummaryPage = () => {
<Box sx={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <Box sx={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<CircularProgress size={32} /> <CircularProgress size={32} />
</Box> </Box>
) : <MaterialReactTable table={summaryProjectTable} />} ) : <MaterialReactTable table={projectTable} />}
</Box> </Box>
) : ( ) : (
<Box <Box
@ -595,25 +569,6 @@ const SummaryPage = () => {
onCreate={handleCreateProject} onCreate={handleCreateProject}
config={createProjectConfig} config={createProjectConfig}
/> />
<Modal
open={Boolean(projectToDelete)}
onClose={() => !isDeleting && setProjectToDelete(null)}
title='Удалить проект?'
size='small'
actions={
<>
<PrimaryButton onClick={() => setProjectToDelete(null)} disabled={isDeleting}>
Отмена
</PrimaryButton>
<DangerOutlinedButton onClick={handleDeleteProject} disabled={isDeleting}>
{isDeleting ? 'Удаление...' : 'Удалить'}
</DangerOutlinedButton>
</>
}>
<Typography>
Проект «{projectToDelete?.name}» будет удалён без возможности восстановления.
</Typography>
</Modal>
</Box> </Box>
</> </>

View File

@ -1,10 +1,10 @@
import { Box, IconButton, Tooltip } from '@mui/material'; import { Box, IconButton, Tooltip } from '@mui/material';
import { Back, EditPenSvg, TrashSvg } from '../../components/common/icons/icons'; import { Back, EditPenSvg } from '../../components/common/icons/icons';
import StatusBadge from './components/StatusBadge/StatusBadge'; import StatusBadge from './components/StatusBadge/StatusBadge';
import { DEVELOMENT_BLOCK } from './constants'; import { DEVELOMENT_BLOCK } from './constants';
export const createFirstColumns = (handlers) => { export const createFirstColumns = (handlers) => {
const { onDelete, onEdit, onNavigate, orgUnitNames = {} } = handlers; const { onEdit, onNavigate, orgUnitNames = {} } = handlers;
return [ return [
{ {
id: 'project', id: 'project',
@ -46,7 +46,7 @@ export const createFirstColumns = (handlers) => {
{ {
accessorKey: 'krf_decision_date', accessorKey: 'krf_decision_date',
header: 'Дата решения КРФ', header: 'Дата решения КРФ',
size: 160, size: 120,
}, },
{ {
accessorKey: 'fk_decision_date', accessorKey: 'fk_decision_date',
@ -140,7 +140,6 @@ export const createFirstColumns = (handlers) => {
</Tooltip> </Tooltip>
{row.depth == 0 && ( {row.depth == 0 && (
<>
<Tooltip title='Перейти'> <Tooltip title='Перейти'>
<IconButton <IconButton
variant='outlined' variant='outlined'
@ -151,18 +150,6 @@ export const createFirstColumns = (handlers) => {
<Back style={{ transform: 'scaleX(-1)' }} /> <Back style={{ transform: 'scaleX(-1)' }} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title='Удалить проект'>
<IconButton
variant='outlined'
color='error'
onClick={(e) => {
e.stopPropagation();
onDelete(row.original);
}}>
<TrashSvg fill='currentColor' />
</IconButton>
</Tooltip>
</>
)} )}
</Box> </Box>
), ),

View File

@ -1,4 +1,4 @@
import { Box, CircularProgress, FormControl, FormLabel, Stack, TextField } from '@mui/material'; import { Box, CircularProgress, Stack } from '@mui/material';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
@ -7,11 +7,10 @@ import { ProjectsApi } from '../../api/projects'; // Добавьте импор
import { TasksApi } from '../../api/tasks'; import { TasksApi } from '../../api/tasks';
import { SettingModal as SettingStageModal } from '../../components/Stages/SettingModal'; import { SettingModal as SettingStageModal } from '../../components/Stages/SettingModal';
import { BackButton } from '../../components/common/Buttons/BackButton'; import { BackButton } from '../../components/common/Buttons/BackButton';
import { DangerOutlinedButton, PrimaryButton, PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons'; import { PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions'; import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
import { IconWithContent } from '../../components/common/IconWithContent'; import { IconWithContent } from '../../components/common/IconWithContent';
import SearchComponent from '../../components/common/SearchComponent'; import SearchComponent from '../../components/common/SearchComponent';
import Modal from '../../components/common/Modal/Modal';
import { NameTask, TaskInfoContainer } from '../../components/common/SwitchFormTask/SwitchFormTask.style'; import { NameTask, TaskInfoContainer } from '../../components/common/SwitchFormTask/SwitchFormTask.style';
import TablesList from '../../components/common/TableList/TableList'; import TablesList from '../../components/common/TableList/TableList';
import { TableIcon } from '../../components/common/icons/icons'; import { TableIcon } from '../../components/common/icons/icons';
@ -51,10 +50,6 @@ export default function TaskPage() {
const [tempProjects, setTempProjects] = useState([]); const [tempProjects, setTempProjects] = useState([]);
const [projects, setProjects] = useState([]); const [projects, setProjects] = useState([]);
const [isProjectData, setIsProjectData] = useState(false); const [isProjectData, setIsProjectData] = useState(false);
const [addYearModalOpen, setAddYearModalOpen] = useState(false);
const [newProjectYear, setNewProjectYear] = useState('');
const [isAddingYear, setIsAddingYear] = useState(false);
const [dataReloadKey, setDataReloadKey] = useState(0);
useEffect(() => { useEffect(() => {
const getData = async () => { const getData = async () => {
@ -97,7 +92,7 @@ export default function TaskPage() {
if (Number.isFinite(id)) { if (Number.isFinite(id)) {
getData(); getData();
} }
}, [id, isProject, dataReloadKey]); }, [id, isProject]);
// Объединили два useEffect в один // Объединили два useEffect в один
@ -117,33 +112,6 @@ export default function TaskPage() {
exportSingleForm(id, fileName); exportSingleForm(id, fileName);
}; };
const handleCloseAddYearModal = () => {
if (isAddingYear) return;
setAddYearModalOpen(false);
setNewProjectYear('');
};
const handleAddProjectYear = async () => {
const year = Number(newProjectYear);
if (!Number.isInteger(year) || year < 1) {
toast.error('Введите корректный год');
return;
}
try {
setIsAddingYear(true);
await ProjectsApi.addYear(id, year);
toast.success(`Год ${year} успешно добавлен`);
setAddYearModalOpen(false);
setNewProjectYear('');
setDataReloadKey((key) => key + 1);
} catch (error) {
toast.error(error?.response?.data?.detail || error?.response?.data?.message || 'Не удалось добавить год');
} finally {
setIsAddingYear(false);
}
};
if (isLoading) { if (isLoading) {
return ( return (
<div className='flex justify-center pt-12'> <div className='flex justify-center pt-12'>
@ -179,11 +147,6 @@ export default function TaskPage() {
<PrimaryOutlinedButton variant='outlined' onClick={() => setModalStageOpen(true)}> <PrimaryOutlinedButton variant='outlined' onClick={() => setModalStageOpen(true)}>
<span>Этапы</span> <span>Этапы</span>
</PrimaryOutlinedButton> </PrimaryOutlinedButton>
{isProject && (
<PrimaryButton onClick={() => setAddYearModalOpen(true)}>
Добавить год
</PrimaryButton>
)}
<SettingStageModal <SettingStageModal
isOpen={modalStageOpen} isOpen={modalStageOpen}
@ -207,40 +170,6 @@ export default function TaskPage() {
/> />
</Box> </Box>
</Box> </Box>
{isProject && (
<Modal
open={addYearModalOpen}
onClose={handleCloseAddYearModal}
title='Добавить год'
size='small'
actions={
<>
<DangerOutlinedButton onClick={handleCloseAddYearModal} disabled={isAddingYear}>
Отмена
</DangerOutlinedButton>
<PrimaryButton onClick={handleAddProjectYear} disabled={isAddingYear}>
{isAddingYear ? 'Добавление...' : 'Добавить'}
</PrimaryButton>
</>
}>
<FormControl fullWidth>
<FormLabel required>Год</FormLabel>
<TextField
autoFocus
size='small'
type='number'
placeholder='Введите год'
value={newProjectYear}
disabled={isAddingYear}
onChange={(event) => setNewProjectYear(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') handleAddProjectYear();
}}
/>
</FormControl>
</Modal>
)}
</> </>
); );
} }