From ac14407f3de7e4820f4f533250d07317e05edbb9 Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Thu, 6 Aug 2026 16:45:42 +0300 Subject: [PATCH] project form, path project-mock-summary --- web/src/app/Routes.jsx | 2 + web/src/pages/ProjectPages/SummaryPage.jsx | 409 +++++++++++++ web/src/pages/ProjectPages/columns.js | 550 ++++++++++++++++++ .../components/EditModal/EditModal.jsx | 202 +++++++ .../components/StatusBadge/StatusBadge.jsx | 39 ++ .../StatusBadge/StatusBadge.module.css | 37 ++ .../components/TableFilters/TableFilters.jsx | 144 +++++ web/src/pages/ProjectPages/mockData.js | 420 +++++++++++++ .../pages/ProjectPages/utils/tableHandlers.js | 14 + 9 files changed, 1817 insertions(+) create mode 100644 web/src/pages/ProjectPages/SummaryPage.jsx create mode 100644 web/src/pages/ProjectPages/columns.js create mode 100644 web/src/pages/ProjectPages/components/EditModal/EditModal.jsx create mode 100644 web/src/pages/ProjectPages/components/StatusBadge/StatusBadge.jsx create mode 100644 web/src/pages/ProjectPages/components/StatusBadge/StatusBadge.module.css create mode 100644 web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx create mode 100644 web/src/pages/ProjectPages/mockData.js create mode 100644 web/src/pages/ProjectPages/utils/tableHandlers.js diff --git a/web/src/app/Routes.jsx b/web/src/app/Routes.jsx index d0945a3..6918c00 100644 --- a/web/src/app/Routes.jsx +++ b/web/src/app/Routes.jsx @@ -18,6 +18,7 @@ import NewTablePage from '../pages/NewTablePage.jsx'; import TablesTest from '../pages/TablesTest.jsx'; import NewFormTablePage from '../pages/NewFormTablePage.jsx' import ProjectsPage from '../pages/ProjectsPage/ProjectsPage.jsx'; +import SummaryPage from '../pages/ProjectPages/SummaryPage.jsx'; export const PrivateRoute = () => { const { isAuthenticated, loading } = useAuth(); @@ -53,6 +54,7 @@ export const AppRoutes = () => { } /> } /> } /> + } /> diff --git a/web/src/pages/ProjectPages/SummaryPage.jsx b/web/src/pages/ProjectPages/SummaryPage.jsx new file mode 100644 index 0000000..bf76839 --- /dev/null +++ b/web/src/pages/ProjectPages/SummaryPage.jsx @@ -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 ( + + {/* Компонент фильтров */} + { }} // Можно использовать для дополнительной логики + selectedBranch={selectedBranch} + onBranchChange={handleBranchChange} + searchQuery={searchQuery} + onSearchChange={handleSearchChange} + /> + + {/* Первая таблица */} + + + + + {/* Вторая таблица */} + + + {getSelectedProjectTitle()} + + {selectedRow ? + + + + : + + Разверните или выберите проект в верхней таблице + + } + + + {/* Модалка редактирования */} + setEditModalOpen(false)} + rowData={selectedRowData} + onSave={onSaveEdit} + /> + + ); +}; + +export default SummaryPage; \ No newline at end of file diff --git a/web/src/pages/ProjectPages/columns.js b/web/src/pages/ProjectPages/columns.js new file mode 100644 index 0000000..84b8f5d --- /dev/null +++ b/web/src/pages/ProjectPages/columns.js @@ -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 ; + }, + }, + { + 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 }) => ( + + + { + e.stopPropagation(); + onEdit(row); + }} + > + + + + + {row.depth == 0 && ( + + { + e.stopPropagation(); + onNavigate(row); + }} + > + + + + )} + + ), + }, + ]; +}; + +// Колонки для второй таблицы с многоуровневыми заголовками +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(), + }, + ], + }, +]; diff --git a/web/src/pages/ProjectPages/components/EditModal/EditModal.jsx b/web/src/pages/ProjectPages/components/EditModal/EditModal.jsx new file mode 100644 index 0000000..1bed176 --- /dev/null +++ b/web/src/pages/ProjectPages/components/EditModal/EditModal.jsx @@ -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) => ( + + + {field.label} + + + + ); + + return ( + + + + Редактирование строки + + + + + + + + + {/* Левая колонка */} + + {leftColumnFields.map(renderField)} + + + {/* Правая колонка */} + + {rightColumnFields.map(renderField)} + + + + + + + Отмена + + + + Сохранить + + + + ); +}; + +export default EditModal; \ No newline at end of file diff --git a/web/src/pages/ProjectPages/components/StatusBadge/StatusBadge.jsx b/web/src/pages/ProjectPages/components/StatusBadge/StatusBadge.jsx new file mode 100644 index 0000000..f6c7a0e --- /dev/null +++ b/web/src/pages/ProjectPages/components/StatusBadge/StatusBadge.jsx @@ -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 ( + + {currentStatus.label} + + ); +}; + +export default StatusBadge; \ No newline at end of file diff --git a/web/src/pages/ProjectPages/components/StatusBadge/StatusBadge.module.css b/web/src/pages/ProjectPages/components/StatusBadge/StatusBadge.module.css new file mode 100644 index 0000000..640d482 --- /dev/null +++ b/web/src/pages/ProjectPages/components/StatusBadge/StatusBadge.module.css @@ -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; +} diff --git a/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx b/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx new file mode 100644 index 0000000..ce91797 --- /dev/null +++ b/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx @@ -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 ( + + + + option.title || ''} + getOptionKey={(option) => option.id} + value={selectedBranch || null} + onChange={(event, newValue) => { + onBranchChange(newValue || ''); + }} + renderInput={(params) => { + const { InputProps, ...rest } = params; + return ( + + {loading && } + {InputProps?.endAdornment} + + ), + }} + /> + ); + }} + renderOption={(props, option) => ( +
  • + {option.title} +
  • + )} + clearOnBlur={false} + isOptionEqualToValue={(option, value) => option === value} + /> + + onSearchChange(e.target.value)} + sx={{ + minWidth: 250, + maxWidth: 300, + flex: '1 1 auto', + }} + InputProps={{ + startAdornment: ( + + ), + }} + /> + + {(selectedBranch || searchQuery) && ( + + { + onBranchChange(''); + onSearchChange(''); + }} + sx={{ + cursor: 'pointer', + '&:hover': { + backgroundColor: 'action.hover', + }, + }} + /> + + )} +
    +
    + ); +}; + +export default TableFilters; \ No newline at end of file diff --git a/web/src/pages/ProjectPages/mockData.js b/web/src/pages/ProjectPages/mockData.js new file mode 100644 index 0000000..eb3fc10 --- /dev/null +++ b/web/src/pages/ProjectPages/mockData.js @@ -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: [], +}; diff --git a/web/src/pages/ProjectPages/utils/tableHandlers.js b/web/src/pages/ProjectPages/utils/tableHandlers.js new file mode 100644 index 0000000..2736918 --- /dev/null +++ b/web/src/pages/ProjectPages/utils/tableHandlers.js @@ -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); +}; \ No newline at end of file