project form, path project-mock-summary #73

Merged
PotapovaA merged 1 commits from form-3-project-mock into test 2026-08-06 16:46:18 +03:00
9 changed files with 1817 additions and 0 deletions

View File

@ -18,6 +18,7 @@ import NewTablePage from '../pages/NewTablePage.jsx';
import TablesTest from '../pages/TablesTest.jsx'; import TablesTest from '../pages/TablesTest.jsx';
import NewFormTablePage from '../pages/NewFormTablePage.jsx' import NewFormTablePage from '../pages/NewFormTablePage.jsx'
import ProjectsPage from '../pages/ProjectsPage/ProjectsPage.jsx'; import ProjectsPage from '../pages/ProjectsPage/ProjectsPage.jsx';
import SummaryPage from '../pages/ProjectPages/SummaryPage.jsx';
export const PrivateRoute = () => { export const PrivateRoute = () => {
const { isAuthenticated, loading } = useAuth(); const { isAuthenticated, loading } = useAuth();
@ -53,6 +54,7 @@ export const AppRoutes = () => {
<Route path="/tables-new" element={<TablesTest />} /> <Route path="/tables-new" element={<TablesTest />} />
<Route path="/tables/:form/:sheetName" element={<NewFormTablePage />} /> <Route path="/tables/:form/:sheetName" element={<NewFormTablePage />} />
<Route path="/table/form/:formId/form-type/:formType/:sheetName/:direction/:year" element={<NewFormTablePage />} /> <Route path="/table/form/:formId/form-type/:formType/:sheetName/:direction/:year" element={<NewFormTablePage />} />
<Route path="/project-mock-summary" element={<SummaryPage />} />
</Route> </Route>
</Route> </Route>
</Routes> </Routes>

View File

@ -0,0 +1,409 @@
import React, { useMemo, useState } from "react";
import {
useMaterialReactTable,
MaterialReactTable,
} from 'material-react-table';
import {
Box,
Paper,
Typography,
} from "@mui/material";
import { useNavigate } from 'react-router-dom';
import { firstTableData, secondTableData } from "./mockData";
import { createFirstColumns, secondColumns } from "./columns";
import EditModal from "./components/EditModal/EditModal";
import TableFilters from "./components/TableFilters/TableFilters"; // Импортируем компонент фильтров
import { handleEditClick, handleNavigateClick, handleSaveEdit } from "./utils/tableHandlers";
const SummaryPage = () => {
const navigate = useNavigate();
const [selectedRow, setSelectedRow] = useState(null);
const [editModalOpen, setEditModalOpen] = useState(false);
const [selectedRowData, setSelectedRowData] = useState(null);
const [tableData, setTableData] = useState(firstTableData);
// Состояния для фильтров
const [selectedBranch, setSelectedBranch] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const getFilteredData = () => {
if (!selectedBranch && !searchQuery) {
return tableData;
}
return tableData
.filter(item => {
// Фильтр по филиалу (только для корневых)
if (selectedBranch) {
return item.org_unit_id === selectedBranch.id;
}
return true;
})
.map(item => {
if (!searchQuery) {
return item;
}
const query = searchQuery.toLowerCase().trim();
// Проверяем, совпадает ли родитель
const parentMatches = item.project.toLowerCase().includes(query);
// Фильтруем дочерние элементы - оставляем только те, что совпадают с поиском
const filteredSubRows = item.subRows?.filter(subItem =>
subItem.project.toLowerCase().includes(query)
) || [];
// Возвращаем элемент с отфильтрованными дочерними элементами
return {
...item,
subRows: filteredSubRows
};
})
.filter(item => {
if (!searchQuery) {
return true;
}
const query = searchQuery.toLowerCase().trim();
const parentMatches = item.project.toLowerCase().includes(query);
const hasMatchingChildren = item.subRows && item.subRows.length > 0;
return parentMatches || hasMatchingChildren;
});
};
const filteredData = getFilteredData();
const handleRowClick = (row) => {
if (row.depth === 0 && row) {
setSelectedRow(row);
}
};
const isChildOfSelectedRow = (row) => {
if (!selectedRow) return false;
const parentRowId = row.parentId;
return (parentRowId === selectedRow.id);
};
const onEdit = (row) => {
handleEditClick(row, setEditModalOpen, setSelectedRowData);
};
const onNavigate = (row) => {
handleNavigateClick(row, navigate);
};
const onSaveEdit = (formData) => {
handleSaveEdit(formData, updateTableData);
};
const updateTableData = (updatedData) => {
setTableData(prevData =>
prevData.map(item =>
item.id === updatedData.id ? { ...item, ...updatedData } : item
)
);
};
const firstColumns = useMemo(() =>
createFirstColumns({ onEdit, onNavigate }),
[onEdit, onNavigate]
);
const tableHeadCellStyles = {
fontWeight: 700,
fontSize: "12px",
lineHeight: 1.15,
textAlign: "left",
backgroundColor: "rgb(248, 249, 250)",
borderRight: "1px solid #e0e0e0",
"&:last-child": {
borderRight: "none",
},
whiteSpace: "nowrap",
position: "sticky",
top: 0,
boxShadow: 'none',
padding: "0.5rem 1rem",
"& .MuiTableSortLabel-icon": {
fontSize: "0.875rem",
},
"& .MuiTableSortLabel-iconDirectionDesc": {
fontSize: "0.875rem",
},
"& .MuiTableSortLabel-iconDirectionAsc": {
fontSize: "0.875rem",
},
};
const summaryProjectsTableOptions = {
enableColumnActions: false,
enableColumnFilters: false,
enablePagination: false,
enableSorting: true,
enableBottomToolbar: false,
enableTopToolbar: false,
enableExpanding: true,
getSubRows: (row) => row.subRows,
muiTableHeadCellProps: {
sx: tableHeadCellStyles,
},
muiExpandButtonProps: {
sx: {
width: "1.5rem",
height: "1.5rem",
minWidth: "1.5rem",
"& .MuiSvgIcon-root": {
fontSize: "0.875rem",
},
},
},
muiTableBodyRowProps: ({ row }) => {
const isSelected = selectedRow && row.id === selectedRow.id;
const isChild = isChildOfSelectedRow(row);
return {
sx: {
backgroundColor: "#ffffff",
"&:hover": {
backgroundColor: "rgb(248, 250, 252)",
},
"&:hover td::after": {
backgroundColor: "transparent !important",
},
...(row.depth > 0 && {
backgroundColor: "rgb(252, 252, 253)",
}),
...((isSelected || isChild) && {
backgroundColor: "rgb(234, 247, 236)",
"&:hover": {
backgroundColor: "rgb(225, 242, 229) !important",
},
}),
},
onClick: () => handleRowClick(row),
};
},
muiTableBodyCellProps: ({ row, column }) => ({
sx: {
borderRight: "1px solid #e0e0e0",
"&:last-child": {
borderRight: "none",
},
"&:hover td::after": {
backgroundColor: "transparent !important",
},
textAlign: "left",
fontSize: "0.875rem",
padding: "0.5rem 1rem",
...(column.id === 'mrt-row-expand' && {
width: "1.5rem",
maxWidth: "1.5rem",
minWidth: "1.5rem",
padding: "0.5rem 0.25rem",
}),
...(column.id === 'project' && row.depth > 0 && {
pl: `${2 + row.depth * 0.5}rem`,
}),
},
}),
muiTablePaperProps: {
elevation: 0,
sx: {
border: "1px solid #e0e0e0",
borderRadius: "1rem",
overflow: "hidden",
height: "45vh",
display: "flex",
flexDirection: "column",
},
},
muiTableContainerProps: {
sx: {
flex: 1,
overflow: "auto",
"& thead": {
position: "sticky",
top: 0,
zIndex: 10,
},
},
},
};
const projectTableOptions = {
enableColumnActions: false,
enableColumnFilters: false,
enablePagination: false,
enableSorting: true,
enableBottomToolbar: false,
enableTopToolbar: false,
enableExpanding: false,
muiTableBodyRowProps: ({ row }) => ({
sx: {
backgroundColor: "#ffffff",
"&:hover": {
backgroundColor: "rgb(248, 250, 252)",
},
"&:hover td::after": {
backgroundColor: "transparent !important",
},
boxShadow: 'none',
},
}),
muiTableHeadCellProps: {
sx: tableHeadCellStyles,
},
muiTableBodyCellProps: ({ column }) => ({
sx: {
borderRight: "1px solid #e0e0e0",
"&:last-child": {
borderRight: "none",
},
textAlign: "left",
fontSize: "0.875rem",
padding: "0.5rem 1rem",
},
}),
muiTablePaperProps: {
elevation: 0,
sx: {
border: "1px solid #e0e0e0",
borderRadius: "0 0 1rem 1rem",
overflow: "hidden",
height: "35vh",
display: "flex",
flexDirection: "column",
},
},
muiTableContainerProps: {
sx: {
flex: 1,
overflow: "auto",
"& thead": {
position: "sticky",
top: 0,
zIndex: 10,
},
},
},
initialState: {
columnPinning: { left: ['project'] }
},
};
const summaryProjectsTable = useMaterialReactTable({
columns: firstColumns,
data: filteredData,
...summaryProjectsTableOptions,
initialState: {
columnPinning: { right: ['actions'], left: ["mrt-row-expand", 'project'] }
},
});
const projectTable = useMaterialReactTable({
columns: secondColumns,
data: selectedRow ? secondTableData[selectedRow.original.id] : [],
...projectTableOptions,
});
const getSelectedProjectTitle = () => {
if (selectedRow && selectedRow.original.project) {
return selectedRow.original.project;
}
return 'Проект не выбран';
};
// Обработчики фильтров
const handleBranchChange = (value) => {
setSelectedBranch(value);
};
const handleSearchChange = (value) => {
setSearchQuery(value);
};
return (
<Box sx={{
p: "1.5rem",
maxWidth: "1600px",
mx: "auto",
height: "100vh",
display: "flex",
flexDirection: "column",
}}>
{/* Компонент фильтров */}
<TableFilters
data={tableData}
onFilterChange={() => { }} // Можно использовать для дополнительной логики
selectedBranch={selectedBranch}
onBranchChange={handleBranchChange}
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
/>
{/* Первая таблица */}
<Paper sx={{
mb: "2rem",
overflow: "hidden",
flex: "0 0 45vh",
borderRadius: "1rem",
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
}}>
<MaterialReactTable table={summaryProjectsTable} />
</Paper>
{/* Вторая таблица */}
<Paper sx={{
overflow: "hidden",
flex: "0 0 35vh",
borderRadius: "1rem",
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
}}>
<Typography
variant="subtitle1"
sx={{
p: "0.75rem 1.5rem",
backgroundColor: "white",
borderBottom: "1px solid #e0e0e0",
fontWeight: "bold",
flexShrink: 0,
fontSize: "1.25rem",
}}
>
{getSelectedProjectTitle()}
</Typography>
{selectedRow ?
<Box sx={{ height: "calc(35vh - 3rem)" }}>
<MaterialReactTable table={projectTable} />
</Box>
:
<Box sx={{
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'rgb(152, 162, 179)',
fontSize: '1rem',
fontWeight: 600,
}}>
Разверните или выберите проект в верхней таблице
</Box>
}
</Paper>
{/* Модалка редактирования */}
<EditModal
open={editModalOpen}
onClose={() => setEditModalOpen(false)}
rowData={selectedRowData}
onSave={onSaveEdit}
/>
</Box>
);
};
export default SummaryPage;

View File

@ -0,0 +1,550 @@
import React from "react";
import { Box, IconButton, Tooltip } from "@mui/material";
import StatusBadge from "./components/StatusBadge/StatusBadge";
import { Back, EditPenSvg } from "../../components/common/icons/icons";
export const createFirstColumns = (handlers) => {
const { onEdit, onNavigate } = handlers;
return [
{
accessorKey: "project",
header: "Проект",
size: 200,
filterFn: "contains",
filterVariant: "text",
},
{
accessorKey: "status",
header: "Статус",
size: 100,
Cell: ({ cell }) => {
const status = cell.getValue();
return <StatusBadge status={status} />;
},
},
{
accessorKey: "techNumber",
header: "Технический номер проекта",
size: 150,
},
{
accessorKey: "address",
header: "Адрес объекта",
size: 180,
},
{
accessorKey: "vspNumber",
header: "Порядковый номер ВСП",
size: 150,
Cell: ({ cell }) => cell.getValue() ?? "",
},
{
accessorKey: "krfDate",
header: "Дата решения КРФ",
size: 120,
},
{
accessorKey: "fkDate",
header: "Дата решения ФК",
size: 120,
},
{
accessorKey: "boardDate",
header: "Дата решения Правления",
size: 130,
},
{
accessorKey: "openMoveCloseDate",
header: "Дата открытия / переезда / закрытия",
size: 160,
},
{
accessorKey: "implementationMonths",
header: "Время реализации проекта в месяцах",
size: 140,
Cell: ({ cell }) => `${cell.getValue()} мес.`,
},
{
accessorKey: "koFinancing",
header: "Финансирование по решению КО",
size: 150,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "baseYearEstimate",
header: "В базовой смете года",
size: 130,
},
{
accessorKey: "correctedEstimateQ2",
header: "В скоррект смете 2 кв",
size: 140,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "correctedEstimateQ3",
header: "В скоррект смете 3 кв",
size: 140,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "correctedEstimateQ4",
header: "В скоррект смете 4 кв",
size: 140,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "developmentType",
header: "Тип развития",
size: 130,
},
{
accessorKey: "developmentBlock",
header: "Блок развития",
size: 120,
},
{
accessorKey: "reserveBankAXR",
header: "Из резерва Банка в смету ПРРС АХР",
size: 160,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "reserveBankKV",
header: "Из резерва Банка в смету ПРРС КВ",
size: 160,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actions",
header: "",
size: 120,
enableSorting: false,
Cell: ({ row }) => (
<Box sx={{ display: "flex", gap: 0.5 }}>
<Tooltip title="Редактировать">
<IconButton
variant="outlined"
onClick={(e) => {
e.stopPropagation();
onEdit(row);
}}
>
<EditPenSvg />
</IconButton>
</Tooltip>
{row.depth == 0 && (
<Tooltip title="Перейти">
<IconButton
variant="outlined"
onClick={(e) => {
e.stopPropagation();
onNavigate(row);
}}
>
<Back style={{ transform: "scaleX(-1)" }} />
</IconButton>
</Tooltip>
)}
</Box>
),
},
];
};
// Колонки для второй таблицы с многоуровневыми заголовками
export const secondColumns = [
{
accessorKey: "project",
header: "Проект",
size: 120,
sticky: "left",
},
// Объем финансирования по контрольному листу
{
header: "Объем финансирования по контрольному листу",
columns: [
{
accessorKey: "financingAXR",
header: "АХР",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "financingAXRLimit",
header: "АХР лимит",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "financingAXRCurrent",
header: "АХР текущие",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "financingKV",
header: "КВ",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
// Базовый период
{
header: "Базовый период 01.01 - 31.03",
columns: [
{
accessorKey: "baseAXR",
header: "АХР",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "baseAXRLimit",
header: "АХР лимит",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "baseAXRCurrent",
header: "АХР текущие",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "baseKV",
header: "КВ",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
// Скорректированный период Q2
{
header: "Скорректированный период 01.04 - 30.06",
columns: [
{
accessorKey: "q2AXR",
header: "АХР",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "q2AXRLimit",
header: "АХР лимит",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "q2AXRCurrent",
header: "АХР текущие",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "q2KV",
header: "КВ",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
// Скорректированный период Q3
{
header: "Скорректированный период 01.07 - 30.09",
columns: [
{
accessorKey: "q3AXR",
header: "АХР",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "q3AXRLimit",
header: "АХР лимит",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "q3AXRCurrent",
header: "АХР текущие",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "q3KV",
header: "КВ",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
// Скорректированный период Q4
{
header: "Скорректированный период 01.10 - 31.12",
columns: [
{
accessorKey: "q4AXR",
header: "АХР",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "q4AXRLimit",
header: "АХР лимит",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "q4AXRCurrent",
header: "АХР текущие",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "q4KV",
header: "КВ",
size: 100,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
// Фактические расходы (АХР)
{
header: "Фактические расходы (АХР)",
columns: [
{
header: "1 квартал",
columns: [
{
accessorKey: "actualAXRQ1",
header: "Фактические расходы",
size: 120,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualAXRLimitQ1",
header: "Лимит",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualAXRCurrentQ1",
header: "Текущие",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
{
header: "2 квартал",
columns: [
{
accessorKey: "actualAXRQ2",
header: "Фактические расходы",
size: 120,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualAXRLimitQ2",
header: "Лимит",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualAXRCurrentQ2",
header: "Текущие",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
{
header: "3 квартал",
columns: [
{
accessorKey: "actualAXRQ3",
header: "Фактические расходы",
size: 120,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualAXRLimitQ3",
header: "Лимит",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualAXRCurrentQ3",
header: "Текущие",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
{
header: "4 квартал",
columns: [
{
accessorKey: "actualAXRQ4",
header: "Фактические расходы",
size: 120,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualAXRLimitQ4",
header: "Лимит",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualAXRCurrentQ4",
header: "Текущие",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
],
},
// Фактические расходы (КВ)
{
header: "Фактические расходы (КВ)",
columns: [
{
header: "1 квартал",
columns: [
{
accessorKey: "actualKVQ1",
header: "Фактические расходы",
size: 120,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualKVLimitQ1",
header: "Лимит",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualKVCurrentQ1",
header: "Текущие",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
{
header: "2 квартал",
columns: [
{
accessorKey: "actualKVQ2",
header: "Фактические расходы",
size: 120,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualKVLimitQ2",
header: "Лимит",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualKVCurrentQ2",
header: "Текущие",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
{
header: "3 квартал",
columns: [
{
accessorKey: "actualKVQ3",
header: "Фактические расходы",
size: 120,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualKVLimitQ3",
header: "Лимит",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualKVCurrentQ3",
header: "Текущие",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
{
header: "4 квартал",
columns: [
{
accessorKey: "actualKVQ4",
header: "Фактические расходы",
size: 120,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualKVLimitQ4",
header: "Лимит",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "actualKVCurrentQ4",
header: "Текущие",
size: 80,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
],
},
// Смета 2026
{
header: "Смета 2026",
columns: [
{
accessorKey: "includeInEstimate2026",
header: "Вкл в смету 2026",
size: 100,
},
{
accessorKey: "developmentBlock2026",
header: "Блок развития 2026",
size: 120,
},
{
accessorKey: "impactRentAXR",
header: "Влияние на АХР подд АРЕНДА год",
size: 160,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "impactOtherAXR",
header: "Влияние на АХР подд ПРОЧЕЕ год",
size: 160,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "sumEstimateAXR",
header: "Сумма в смету 2026 года АХР",
size: 150,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
{
accessorKey: "sumEstimateKV",
header: "Сумма в смету 2026 года КВ",
size: 150,
Cell: ({ cell }) => cell.getValue().toLocaleString(),
},
],
},
];

View File

@ -0,0 +1,202 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Grid,
Box,
IconButton,
Typography,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { DangerOutlinedButton, PrimaryButton } from '../../../../components/common/Buttons/Buttons';
const EditModal = ({ open, onClose, rowData, onSave }) => {
const [formData, setFormData] = React.useState(rowData || {});
React.useEffect(() => {
if (rowData) {
setFormData(rowData);
}
}, [rowData]);
const handleChange = (field) => (event) => {
setFormData({
...formData,
[field]: event.target.value,
});
};
const handleSave = () => {
onSave(formData);
onClose();
};
if (!rowData) return null;
const leftColumnFields = [
{ key: 'project', label: 'Проект', placeholder: 'Смета_2026' },
{ key: 'techNumber', label: 'Технический номер проекта', placeholder: '2606_180801' },
{ key: 'vspNumber', label: 'Порядковый номер ВСП', placeholder: '1808' },
{ key: 'fkDate', label: 'Дата решения ФК', type: 'date', placeholder: '18.06.2026' },
{ key: 'openMoveCloseDate', label: 'Дата открытия / переезда / закрытия', type: 'date' },
{ key: 'koFinancing', label: 'Финансирование по решению КО', type: 'number' },
{ key: 'prrsEstimate', label: 'Смета ПРРС' },
{ key: 'correctedEstimateQ2', label: 'В скоррект смете 2 кв', type: 'number' },
{ key: 'baseYearEstimate', label: 'В базовой смете года' },
{ key: 'correctedEstimateQ3', label: 'В скоррект смете 3 кв', type: 'number' },
];
const rightColumnFields = [
{ key: 'correctedEstimateQ4', label: 'В скоррект смете 4 кв', type: 'number' },
{ key: 'developmentBlock', label: 'Блок развития' },
{ key: 'reserveBankKV', label: 'Из резерва Банка в смету ПРРС КВ', type: 'number' },
{ key: 'status', label: 'Статус' },
{ key: 'krfDate', label: 'Дата решения КРФ', type: 'date' },
{ key: 'boardDate', label: 'Дата решения Правления', type: 'date' },
{ key: 'implementationMonths', label: 'Время реализации проекта в месяцах', type: 'number' },
{ key: 'developmentType', label: 'Тип развития', placeholder: 'Реновация ВСП' },
{ key: 'reserveBankAXR', label: 'Из резерва Банка в смету ПРРС АХР', type: 'number' },
{ key: 'address', label: 'Адрес объекта', multiline: true, rows: 2, placeholder: '658213, Алтайский кр., г. Рубцовск, ул. Дзержинского, д. 14' },
];
// Компонент для поля ввода
const renderField = (field) => (
<Box key={field.key} sx={{ width: '390px', flexShrink: 0 }}>
<Typography variant="caption" sx={{
fontWeight: 600,
color: '#666',
display: 'block',
mb: 0.5,
fontSize: '0.75rem',
}}>
{field.label}
</Typography>
<TextField
fullWidth
value={formData[field.key] || ''}
onChange={handleChange(field.key)}
variant="outlined"
size="small"
type={field.type || 'text'}
placeholder={field.placeholder || ''}
multiline={field.multiline || false}
rows={field.rows || 1}
InputLabelProps={field.type === 'date' ? { shrink: true } : undefined}
sx={{
'& .MuiOutlinedInput-root': {
bgcolor: 'transparent',
'& fieldset': {
borderColor: '#d0d0d0',
},
'&:hover fieldset': {
borderColor: '#b0b0b0',
},
'&.Mui-focused fieldset': {
borderColor: '#1976d2',
},
...(field.multiline && {
minHeight: '56px',
}),
},
'& .MuiOutlinedInput-input': {
bgcolor: 'transparent',
},
'& .MuiInputBase-input': {
bgcolor: 'transparent',
},
width: '100%',
}}
/>
</Box>
);
return (
<Dialog
open={open}
onClose={onClose}
maxWidth={false}
PaperProps={{
sx: {
borderRadius: '12px',
maxHeight: '90vh',
height: 'auto',
maxWidth: '820px',
width: '100%',
},
}}
>
<DialogTitle sx={{
m: 0,
p: '16px 24px',
borderBottom: '1px solid #e0e0e0',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
bgcolor: '#f8f9fa',
}}>
<Typography variant="h6" fontWeight="600" fontSize="18px">
Редактирование строки
</Typography>
<IconButton onClick={onClose} size="small">
<CloseIcon fontSize="small" />
</IconButton>
</DialogTitle>
<DialogContent sx={{
pt: 3,
pb: 2,
px: 3,
overflow: 'auto',
}}>
<Box sx={{
display: 'flex',
gap: '20px',
justifyContent: 'space-between',
}}>
{/* Левая колонка */}
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
flex: '0 0 auto',
}}>
{leftColumnFields.map(renderField)}
</Box>
{/* Правая колонка */}
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
flex: '0 0 auto',
}}>
{rightColumnFields.map(renderField)}
</Box>
</Box>
</DialogContent>
<DialogActions sx={{
p: 2,
px: 3,
borderTop: '1px solid #e0e0e0',
bgcolor: '#f8f9fa',
justifyContent: 'flex-end',
gap: 1,
}}>
<DangerOutlinedButton onClick={onClose}>
Отмена
</DangerOutlinedButton>
<PrimaryButton onClick={handleSave}>
Сохранить
</PrimaryButton>
</DialogActions>
</Dialog>
);
};
export default EditModal;

View File

@ -0,0 +1,39 @@
import React from 'react';
import styles from './StatusBadge.module.css';
const StatusBadge = ({ status, className = '', ...props }) => {
const statusMap = {
deleted: {
class: styles.deleted,
label: 'Удален',
},
approved: {
class: styles.approved,
label: 'Согласован',
},
archived: {
class: styles.archived,
label: 'В архиве',
},
created: {
class: styles.created,
label: 'Создан',
},
};
// Если статус не найден, ничего не рендерим или показываем дефолтный
const currentStatus = statusMap[status];
if (!currentStatus) return null;
return (
<span
className={`${styles.badge} ${currentStatus.class} ${className}`}
{...props}
>
{currentStatus.label}
</span>
);
};
export default StatusBadge;

View File

@ -0,0 +1,37 @@
.badge {
display: inline-flex;
-moz-box-align: center;
align-items: center;
min-height: 1.5rem;
padding-left: 6px;
padding-right: 6px;
border: 1px solid rgb(254, 202, 202);
border-radius: 3914px;
font-size: 0.8rem;
font-weight: 700;
white-space: nowrap;
}
.deleted {
background-color: #fbe7e9;
border-color: #f5d3d6;
color: #7e2a2a;
}
.approved {
background-color: #e6f4ea;
border-color: #ceead6;
color: #1e7e34;
}
.archived {
background-color: #f1f3f4;
border-color: #dadce0;
color: #5f6368;
}
.created {
background-color: #e8f0fe;
border-color: #d2e3fc;
color: #1967d2;
}

View File

@ -0,0 +1,144 @@
import React, { useState, useEffect } from 'react';
import {
Box,
TextField,
Autocomplete,
Paper,
Typography,
Chip,
CircularProgress,
} from '@mui/material';
import { Search } from '@mui/icons-material';
import { SspApi } from '../../../../api/ssp';
const TableFilters = ({
onBranchChange,
onSearchChange,
selectedBranch,
searchQuery,
}) => {
const [branches, setBranches] = useState([]);
const [loading, setLoading] = useState(false);
// Загружаем филиалы из API
useEffect(() => {
const loadBranches = async () => {
setLoading(true);
try {
const data = await SspApi.getAll({ is_active: true });
setBranches(data.result);
} catch (error) {
console.error('Error loading branches:', error);
} finally {
setLoading(false);
}
};
loadBranches();
}, []);
return (
<Paper
elevation={0}
sx={{
mb: '1vh',
backgroundColor: 'transparent',
}}
>
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: '0.5vh',
flexWrap: 'wrap',
maxHeight: '4.5vh',
}}>
<Autocomplete
size="small"
sx={{
minWidth: 250,
maxWidth: 300,
flex: '1 1 auto',
}}
options={branches}
loading={loading}
getOptionLabel={(option) => option.title || ''}
getOptionKey={(option) => option.id}
value={selectedBranch || null}
onChange={(event, newValue) => {
onBranchChange(newValue || '');
}}
renderInput={(params) => {
const { InputProps, ...rest } = params;
return (
<TextField
{...rest}
placeholder="Региональные филиалы"
variant="outlined"
size="small"
InputProps={{
...InputProps,
endAdornment: (
<>
{loading && <CircularProgress color="inherit" size={20} />}
{InputProps?.endAdornment}
</>
),
}}
/>
);
}}
renderOption={(props, option) => (
<li {...props} key={option.id}>
{option.title}
</li>
)}
clearOnBlur={false}
isOptionEqualToValue={(option, value) => option === value}
/>
<TextField
size="small"
placeholder="Название проекта"
variant="outlined"
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
sx={{
minWidth: 250,
maxWidth: 300,
flex: '1 1 auto',
}}
InputProps={{
startAdornment: (
<Search sx={{
color: 'text.secondary',
mr: 1,
fontSize: 20,
}} />
),
}}
/>
{(selectedBranch || searchQuery) && (
<Box sx={{ display: 'flex', gap: 1, ml: 'auto' }}>
<Chip
label="Сбросить фильтры"
size="small"
onClick={() => {
onBranchChange('');
onSearchChange('');
}}
sx={{
cursor: 'pointer',
'&:hover': {
backgroundColor: 'action.hover',
},
}}
/>
</Box>
)}
</Box>
</Paper>
);
};
export default TableFilters;

View File

@ -0,0 +1,420 @@
export const firstTableData = [
{
id: "478",
org_unit_id: 6,
project: "Алтайский_01",
status: "created",
techNumber: "2606_180801",
address: "658213, Алтайский кр., г. Рубцовск, ул. Дз...",
vspNumber: 1808,
krfDate: "11.06.2026",
fkDate: "18.06.2026",
boardDate: "✅",
openMoveCloseDate: "15.07.2026",
implementationMonths: 8,
koFinancing: 35697,
baseYearEstimate: "2026",
correctedEstimateQ2: 12482,
correctedEstimateQ3: 13200,
correctedEstimateQ4: 14100,
developmentType: "Новое строительство",
developmentBlock: "Центральный",
reserveBankAXR: 1500,
reserveBankKV: 2300,
subRows: [
{
id: "sub_001_01",
project: "Смета_2026",
status: "Активна",
techNumber: "2606_180801_01",
address: "г. Рубцовск, ул. Дзержинского, 10",
vspNumber: 1809,
krfDate: "12.06.2026",
fkDate: "19.06.2026",
boardDate: "✅",
openMoveCloseDate: "15.07.2026",
implementationMonths: 8,
koFinancing: 11762,
baseYearEstimate: "2026",
correctedEstimateQ2: 12482,
correctedEstimateQ3: 13200,
correctedEstimateQ4: 14100,
developmentType: "Реконструкция",
developmentBlock: "Центральный",
reserveBankAXR: 500,
reserveBankKV: 800,
},
{
id: "sub_001_02",
project: "Смета_2027",
status: "Планируется",
techNumber: "2606_180801_02",
address: "г. Рубцовск, ул. Ленина, 25",
vspNumber: 1810,
krfDate: "13.06.2026",
fkDate: "20.06.2026",
boardDate: "⏳",
openMoveCloseDate: "01.08.2027",
implementationMonths: 12,
koFinancing: 11899,
baseYearEstimate: "2027",
correctedEstimateQ2: 12619,
correctedEstimateQ3: 13400,
correctedEstimateQ4: 14300,
developmentType: "Модернизация",
developmentBlock: "Северный",
reserveBankAXR: 600,
reserveBankKV: 900,
},
{
id: "sub_001_03",
project: "Смета_2028",
status: "Планируется",
techNumber: "2606_180801_03",
address: "г. Рубцовск, ул. Советская, 15",
vspNumber: 1811,
krfDate: "14.06.2026",
fkDate: "21.06.2026",
boardDate: "⏳",
openMoveCloseDate: "01.08.2028",
implementationMonths: 14,
koFinancing: 12036,
baseYearEstimate: "2028",
correctedEstimateQ2: 12756,
correctedEstimateQ3: 13600,
correctedEstimateQ4: 14500,
developmentType: "Расширение",
developmentBlock: "Южный",
reserveBankAXR: 700,
reserveBankKV: 1000,
},
],
},
{
id: "proj_002",
project: "Алтайский_02",
org_unit_id: 6,
status: "deleted",
techNumber: "2602_180801",
address: "658213, Алтайский кр., г. Рубцовск, ул. Дз...",
vspNumber: 1808,
krfDate: "11.01.2026",
fkDate: "16.01.2026",
boardDate: "✅",
openMoveCloseDate: "15.02.2026",
implementationMonths: 3,
koFinancing: 25000,
baseYearEstimate: "2026",
correctedEstimateQ2: 8500,
correctedEstimateQ3: 9200,
correctedEstimateQ4: 9800,
developmentType: "Временный",
developmentBlock: "Западный",
reserveBankAXR: 300,
reserveBankKV: 500,
subRows: [],
},
{
id: "proj_003",
project: "Алтайский_03",
org_unit_id: 7,
status: "approved",
techNumber: "2512_180601",
address: "Алтайский кр., объект развития ВСП",
vspNumber: null,
krfDate: "10.04.2025",
fkDate: "25.10.2025",
boardDate: "02.11.2025",
openMoveCloseDate: "01.12.2025",
implementationMonths: 6,
koFinancing: 32000,
baseYearEstimate: "2025",
correctedEstimateQ2: 10500,
correctedEstimateQ3: 11200,
correctedEstimateQ4: 11800,
developmentType: "Капитальный ремонт",
developmentBlock: "Восточный",
reserveBankAXR: 400,
reserveBankKV: 600,
subRows: [
{
id: "sub_003_01",
project: "Подпроект А",
status: "Завершен",
techNumber: "2512_180601_А",
address: "г. Барнаул, ул. Пушкина, 15",
vspNumber: 1811,
krfDate: "11.04.2025",
fkDate: "26.10.2025",
boardDate: "03.11.2025",
openMoveCloseDate: "15.11.2025",
implementationMonths: 5,
koFinancing: 15000,
baseYearEstimate: "2025",
correctedEstimateQ2: 5000,
correctedEstimateQ3: 5500,
correctedEstimateQ4: 6000,
developmentType: "Реконструкция",
developmentBlock: "Восточный",
reserveBankAXR: 200,
reserveBankKV: 300,
},
],
},
{
id: "proj_004",
project: ашкирский_01",
status: "archived",
org_unit_id: 8,
techNumber: "2405_620501",
address: "Республика Башкортостан, проект развит...",
vspNumber: null,
krfDate: "03.06.2024",
fkDate: "14.06.2024",
boardDate: "02.07.2024",
openMoveCloseDate: "15.08.2024",
implementationMonths: 10,
koFinancing: 45000,
baseYearEstimate: "2024",
correctedEstimateQ2: 15000,
correctedEstimateQ3: 16000,
correctedEstimateQ4: 17000,
developmentType: "Новое строительство",
developmentBlock: "Центральный",
reserveBankAXR: 800,
reserveBankKV: 1200,
subRows: [],
},
];
// Вторая таблица - объект, где ключ - id проекта, значение - массив данных для этого проекта
export const secondTableData = {
// Для проекта Алтайский_01 (3 дочерних строки)
478: [
{
id: "sub_001_01",
project: "Смета_2026",
financingAXR: 11942,
financingAXRLimit: 12122,
financingAXRCurrent: 10502,
financingKV: 12482,
baseAXR: 2800,
baseAXRLimit: 3000,
baseAXRCurrent: 2500,
baseKV: 2900,
q2AXR: 3100,
q2AXRLimit: 3300,
q2AXRCurrent: 2800,
q2KV: 3200,
q3AXR: 3400,
q3AXRLimit: 3600,
q3AXRCurrent: 3100,
q3KV: 3500,
q4AXR: 3700,
q4AXRLimit: 3900,
q4AXRCurrent: 3400,
q4KV: 3800,
actualAXRQ1: 2400,
actualAXRLimitQ1: 2600,
actualAXRCurrentQ1: 2200,
actualAXRQ2: 2800,
actualAXRLimitQ2: 3000,
actualAXRCurrentQ2: 2600,
actualAXRQ3: 3200,
actualAXRLimitQ3: 3400,
actualAXRCurrentQ3: 3000,
actualAXRQ4: 3600,
actualAXRLimitQ4: 3800,
actualAXRCurrentQ4: 3400,
actualKVQ1: 2500,
actualKVLimitQ1: 2700,
actualKVCurrentQ1: 2300,
actualKVQ2: 2900,
actualKVLimitQ2: 3100,
actualKVCurrentQ2: 2700,
actualKVQ3: 3300,
actualKVLimitQ3: 3500,
actualKVCurrentQ3: 3100,
actualKVQ4: 3700,
actualKVLimitQ4: 3900,
actualKVCurrentQ4: 3500,
includeInEstimate2026: "Да",
developmentBlock2026: "Центральный",
impactRentAXR: 200,
impactOtherAXR: 100,
sumEstimateAXR: 11942,
sumEstimateKV: 12482,
},
{
id: "sub_001_02",
project: "Смета_2027",
financingAXR: 12079,
financingAXRLimit: 12259,
financingAXRCurrent: 10639,
financingKV: 12619,
baseAXR: 2900,
baseAXRLimit: 3100,
baseAXRCurrent: 2600,
baseKV: 3000,
q2AXR: 3200,
q2AXRLimit: 3400,
q2AXRCurrent: 2900,
q2KV: 3300,
q3AXR: 3500,
q3AXRLimit: 3700,
q3AXRCurrent: 3200,
q3KV: 3600,
q4AXR: 3800,
q4AXRLimit: 4000,
q4AXRCurrent: 3500,
q4KV: 3900,
actualAXRQ1: 2500,
actualAXRLimitQ1: 2700,
actualAXRCurrentQ1: 2300,
actualAXRQ2: 2900,
actualAXRLimitQ2: 3100,
actualAXRCurrentQ2: 2700,
actualAXRQ3: 3300,
actualAXRLimitQ3: 3500,
actualAXRCurrentQ3: 3100,
actualAXRQ4: 3700,
actualAXRLimitQ4: 3900,
actualAXRCurrentQ4: 3500,
actualKVQ1: 2600,
actualKVLimitQ1: 2800,
actualKVCurrentQ1: 2400,
actualKVQ2: 3000,
actualKVLimitQ2: 3200,
actualKVCurrentQ2: 2800,
actualKVQ3: 3400,
actualKVLimitQ3: 3600,
actualKVCurrentQ3: 3200,
actualKVQ4: 3800,
actualKVLimitQ4: 4000,
actualKVCurrentQ4: 3600,
includeInEstimate2026: "Нет",
developmentBlock2026: "Северный",
impactRentAXR: 250,
impactOtherAXR: 150,
sumEstimateAXR: 12079,
sumEstimateKV: 12619,
},
{
id: "sub_001_03",
project: "Смета_2028",
financingAXR: 12216,
financingAXRLimit: 12396,
financingAXRCurrent: 10776,
financingKV: 12756,
baseAXR: 3000,
baseAXRLimit: 3200,
baseAXRCurrent: 2700,
baseKV: 3100,
q2AXR: 3300,
q2AXRLimit: 3500,
q2AXRCurrent: 3000,
q2KV: 3400,
q3AXR: 3600,
q3AXRLimit: 3800,
q3AXRCurrent: 3300,
q3KV: 3700,
q4AXR: 3900,
q4AXRLimit: 4100,
q4AXRCurrent: 3600,
q4KV: 4000,
actualAXRQ1: 2600,
actualAXRLimitQ1: 2800,
actualAXRCurrentQ1: 2400,
actualAXRQ2: 3000,
actualAXRLimitQ2: 3200,
actualAXRCurrentQ2: 2800,
actualAXRQ3: 3400,
actualAXRLimitQ3: 3600,
actualAXRCurrentQ3: 3200,
actualAXRQ4: 3800,
actualAXRLimitQ4: 4000,
actualAXRCurrentQ4: 3600,
actualKVQ1: 2700,
actualKVLimitQ1: 2900,
actualKVCurrentQ1: 2500,
actualKVQ2: 3100,
actualKVLimitQ2: 3300,
actualKVCurrentQ2: 2900,
actualKVQ3: 3500,
actualKVLimitQ3: 3700,
actualKVCurrentQ3: 3300,
actualKVQ4: 3900,
actualKVLimitQ4: 4100,
actualKVCurrentQ4: 3700,
includeInEstimate2026: "Нет",
developmentBlock2026: "Южный",
impactRentAXR: 300,
impactOtherAXR: 200,
sumEstimateAXR: 12216,
sumEstimateKV: 12756,
},
],
// Для проекта Алтайский_02 (0 дочерних строк - пустой массив)
proj_002: [],
// Для проекта Алтайский_03 (1 дочерняя строка)
proj_003: [
{
id: "sub_003_01",
project: "Подпроект А",
financingAXR: 15200,
financingAXRLimit: 15400,
financingAXRCurrent: 13800,
financingKV: 15800,
baseAXR: 3500,
baseAXRLimit: 3700,
baseAXRCurrent: 3200,
baseKV: 3600,
q2AXR: 3800,
q2AXRLimit: 4000,
q2AXRCurrent: 3500,
q2KV: 3900,
q3AXR: 4100,
q3AXRLimit: 4300,
q3AXRCurrent: 3800,
q3KV: 4200,
q4AXR: 4400,
q4AXRLimit: 4600,
q4AXRCurrent: 4100,
q4KV: 4500,
actualAXRQ1: 3200,
actualAXRLimitQ1: 3400,
actualAXRCurrentQ1: 3000,
actualAXRQ2: 3600,
actualAXRLimitQ2: 3800,
actualAXRCurrentQ2: 3400,
actualAXRQ3: 4000,
actualAXRLimitQ3: 4200,
actualAXRCurrentQ3: 3800,
actualAXRQ4: 4400,
actualAXRLimitQ4: 4600,
actualAXRCurrentQ4: 4200,
actualKVQ1: 3300,
actualKVLimitQ1: 3500,
actualKVCurrentQ1: 3100,
actualKVQ2: 3700,
actualKVLimitQ2: 3900,
actualKVCurrentQ2: 3500,
actualKVQ3: 4100,
actualKVLimitQ3: 4300,
actualKVCurrentQ3: 3900,
actualKVQ4: 4500,
actualKVLimitQ4: 4700,
actualKVCurrentQ4: 4300,
includeInEstimate2026: "Да",
developmentBlock2026: "Восточный",
impactRentAXR: 400,
impactOtherAXR: 200,
sumEstimateAXR: 15200,
sumEstimateKV: 15800,
},
],
proj_004: [],
};

View File

@ -0,0 +1,14 @@
export const handleEditClick = (row, setEditModalOpen, setSelectedRowData) => {
setSelectedRowData(row.original);
setEditModalOpen(true);
};
export const handleNavigateClick = (row, navigate) => {
const projectId = row.original.id || row.original.project;
navigate(`/project/${projectId}`);
};
export const handleSaveEdit = (formData, updateTableData) => {
updateTableData(formData);
console.log('Сохраненные данные:', formData);
};