diff --git a/web/src/components/RealtimeTable/constants/validation.js b/web/src/components/RealtimeTable/constants/validation.js index 5074b35..0abee24 100644 --- a/web/src/components/RealtimeTable/constants/validation.js +++ b/web/src/components/RealtimeTable/constants/validation.js @@ -10,6 +10,24 @@ const remainingBookingKeys = [ "data.q4.residual_after_booking", ]; +// Требование №1 работает для всех форм и листов, кроме CAP и OPER формы 4. +const remainingBookingRuleScope = { + excludedScopes: [ + { formType: "FORM_4", sheetName: "CAP" }, + { formType: "FORM_4", sheetName: "OPER" }, + ], +}; + +// Для листов CAP и OPER формы 4 требование №1 проверяет только остаток за 4 квартал. +const remainingBooking4FormKeys = ["data.q4.residual_after_booking"]; + +const remainingBooking4FormRuleScope = { + includedScopes: [ + { formType: "FORM_4", sheetName: "CAP" }, + { formType: "FORM_4", sheetName: "OPER" }, + ], +}; + // Требование №2. Контроль столбца «Текущие корректировки = 0». const zeroValueRequirementKeys = [ "data.q1.adj_current", @@ -18,7 +36,7 @@ const zeroValueRequirementKeys = [ "data.q4.adj_current", ]; -//Бронь <= Остаток после брони +// Бронь <= Остаток после брони для формы 2. const bookingLessBalance = [ ["data.q1.booking", "data.q1.residual_after_booking"], ["data.q2.booking", "data.q2.residual_after_booking"], @@ -33,15 +51,18 @@ const bookingLessBalanceByColumn = new Map( ]), ); -//Бронь <= Остаток после брони для листов кв.П и Опер 4 формы -const bookingLessBalance4From = [ - ["data.q4.booking", "data.q4.residual_after_booking"], +// Экономия <= Всего для форм 1 и 4. +const economyLessTotal = [ + ["data.q1.economy", "data.q1.total"], + ["data.q2.economy", "data.q2.total"], + ["data.q3.economy", "data.q3.total"], + ["data.q4.economy", "data.q4.total"], ]; -const bookingLessBalance4FormByColumn = new Map( - bookingLessBalance.flatMap(([bookingKey, balanceKey]) => [ - [bookingKey, { bookingKey, balanceKey }], - [balanceKey, { bookingKey, balanceKey }], +const economyLessTotalByColumn = new Map( + economyLessTotal.flatMap(([economyKey, totalKey]) => [ + [economyKey, { economyKey, totalKey }], + [totalKey, { economyKey, totalKey }], ]), ); @@ -59,17 +80,13 @@ const transferLessTotalByColumn = new Map( ]), ); -// Область действия всех правил. Пары можно добавлять, удалять и изменять. const bookingLessBalanceRuleScope = { - includedScopes: [], - excludedScopes: [ - { formType: "FORM_4", sheetName: "CAP" }, - { formType: "FORM_4", sheetName: "OPER" }, - ], + includedScopes: [{ formType: "FORM_2" }], }; export const validationRules = [ { + ...remainingBookingRuleScope, id: "remaining-booking", columnKeys: remainingBookingKeys, message: "Сумма бронирования превышает сумму, предусмотренную сметой", @@ -80,6 +97,18 @@ export const validationRules = [ Number.isFinite(Number(value)) && Number(value) < 0, }, + { + ...remainingBooking4FormRuleScope, + id: "remaining-booking-4-form", + columnKeys: remainingBooking4FormKeys, + message: "Сумма бронирования превышает сумму, предусмотренную сметой", + isInvalid: (value) => + value !== null && + value !== undefined && + value !== "" && + Number.isFinite(Number(value)) && + Number(value) < 0, + }, { id: "current-adjustments-zero", columnKeys: zeroValueRequirementKeys, @@ -123,31 +152,32 @@ export const validationRules = [ }, }, { - id: "booking-less-balance-4-form", - columnKeys: bookingLessBalance4From.flat(), - errorColumnKeys: bookingLessBalance4From.map(([bookingKey]) => bookingKey), - message: "Сумма бронирования превышает остаток после бронирования", + includedScopes: [{ formType: "FORM_1" }, { formType: "FORM_4" }], + id: "economy-less-total", + columnKeys: economyLessTotal.flat(), + errorColumnKeys: economyLessTotal.map(([economyKey]) => economyKey), + message: "Экономия должна быть меньше значения «Всего»", isInvalid: (_value, row, columnKey) => { - const pair = bookingLessBalance4FormByColumn.get(columnKey); + const pair = economyLessTotalByColumn.get(columnKey); if (!pair) return false; - const booking = pair.bookingKey + const economy = pair.economyKey .split(".") .reduce((value, key) => value?.[key], row); - const balance = pair.balanceKey + const total = pair.totalKey .split(".") .reduce((value, key) => value?.[key], row); return ( - booking !== null && - booking !== undefined && - booking !== "" && - balance !== null && - balance !== undefined && - balance !== "" && - Number.isFinite(Number(booking)) && - Number.isFinite(Number(balance)) && - Number(booking) > Number(balance) + economy !== null && + economy !== undefined && + economy !== "" && + total !== null && + total !== undefined && + total !== "" && + Number.isFinite(Number(economy)) && + Number.isFinite(Number(total)) && + Number(economy) > Number(total) ); }, }, diff --git a/web/src/components/common/icons/icons.jsx b/web/src/components/common/icons/icons.jsx index ccf2573..b19690f 100644 --- a/web/src/components/common/icons/icons.jsx +++ b/web/src/components/common/icons/icons.jsx @@ -555,14 +555,14 @@ export const TransitionSvg = ({ size = 16 }) => ( export const ProjectSvg = ({ size = 16 }) => ( ); export const SummarySvg = ({ size = 16 }) => ( ); diff --git a/web/src/pages/DictVspPage/DictVspPage.jsx b/web/src/pages/DictVspPage/DictVspPage.jsx index ba0264e..e918dca 100644 --- a/web/src/pages/DictVspPage/DictVspPage.jsx +++ b/web/src/pages/DictVspPage/DictVspPage.jsx @@ -39,7 +39,7 @@ function DictVspPage() { if (!user) return; setIsLoading(true); try { - const isAdmin = user.role_id == ROLES_NAME_ID.admin; + const isAdmin = user.role_id === ROLES_NAME_ID.admin; const [vspResponse, sspResponse] = await Promise.all([ DictVspApi.get(), isAdmin ? SspApi.getAll({ is_active: true }) : Promise.resolve(null), diff --git a/web/src/pages/ProjectPages/SummaryPage.jsx b/web/src/pages/ProjectPages/SummaryPage.jsx index e437b3b..50ba63b 100644 --- a/web/src/pages/ProjectPages/SummaryPage.jsx +++ b/web/src/pages/ProjectPages/SummaryPage.jsx @@ -1,6 +1,6 @@ import { Box, CircularProgress, Paper, Typography } from '@mui/material'; import { MaterialReactTable, useMaterialReactTable } from 'material-react-table'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'react-toastify'; import { ProjectsApi } from '../../api/projects'; @@ -15,6 +15,8 @@ import { createFirstColumns, secondColumns } from './columns'; import EditModal from './components/EditModal/EditModal'; import TableFilters from './components/TableFilters/TableFilters'; // Импортируем компонент фильтров import { handleNavigateClick } from './utils/tableHandlers'; +import { useAuth } from '../../app/context/AuthProvider'; +import { ROLES_NAME_ID } from '../../constants/constants'; const SummaryPage = () => { const navigate = useNavigate(); @@ -33,32 +35,49 @@ const SummaryPage = () => { const [createModalOpen, setCreateModalOpen] = useState(false); const [projectToDelete, setProjectToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + const { user } = useAuth(); // Состояния для фильтров const [selectedBranch, setSelectedBranch] = useState(''); const [searchQuery, setSearchQuery] = useState(''); - useEffect(() => { - let active = true; - const loadOrgUnits = async () => { - try { - const response = await SspApi.getAll({ is_active: true }); - if (active) { - setOrgUnits(response.result || []); - setOrgUnitNames( - Object.fromEntries((response.result || []).map((orgUnit) => [orgUnit.id, orgUnit.title])), - ); + const loadData = useCallback(async () => { + if (!user) return; + setIsLoading(true); + try { + const isAdmin = user.role_id === ROLES_NAME_ID.admin; + + if (isAdmin) { + const sspResponse = await ( + SspApi.getAll({ is_active: true }) + ); + if (!sspResponse.success) { + toast.error('Ошибка загрузки списка ССП'); + return; } - } catch (requestError) { - console.error('Error loading SSP/RF names:', requestError); - if (active) toast.error('Не удалось загрузить список ССП/РФ'); + setOrgUnits(sspResponse.result); + setOrgUnitNames( + Object.fromEntries((sspResponse.result || []).map((orgUnit) => [orgUnit.id, orgUnit.title])), + ); + } else { + setOrgUnits(user.org_units ?? []); + setOrgUnitNames( + Object.fromEntries((user.org_units || []).map((orgUnit) => [orgUnit.id, orgUnit.title])), + ); } - }; - loadOrgUnits(); - return () => { - active = false; - }; - }, []); + } catch (error) { + console.error('Error loading data:', error); + toast.error(error.response?.data?.message || 'Ошибка загрузки данных'); + } finally { + setIsLoading(false); + } + }, [user]); + + useEffect(() => { + loadData(); + }, [loadData]); const createProjectConfig = useMemo(() => ({ ...DEFAULT_PROJECT_CONFIG, diff --git a/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx b/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx index f650581..5b9fbee 100644 --- a/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx +++ b/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx @@ -1,27 +1,48 @@ import { Search } from '@mui/icons-material'; import { Autocomplete, Box, Chip, CircularProgress, Paper, TextField } from '@mui/material'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { SspApi } from '../../../../api/ssp'; +import { useAuth } from '../../../../app/context/AuthProvider'; +import { toast } from 'react-toastify'; +import { ROLES_NAME_ID } from '../../../../constants/constants'; const TableFilters = ({ onBranchChange, onSearchChange, selectedBranch, searchQuery }) => { const [branches, setBranches] = useState([]); - const [loading, setLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const { user } = useAuth(); + + const loadData = useCallback(async () => { + if (!user) return; + setIsLoading(true); + try { + const isAdmin = user.role_id === ROLES_NAME_ID.admin; + + if (isAdmin) { + const sspResponse = await ( + SspApi.getAll({ is_active: true }) + ); + if (!sspResponse.success) { + toast.error('Ошибка загрузки списка ССП'); + return; + } + setBranches(sspResponse.result); + + } else { + setBranches(user.org_units ?? []); - // Загружаем филиалы из 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(); - }, []); + } catch (error) { + console.error('Error loading data:', error); + toast.error(error.response?.data?.message || 'Ошибка загрузки данных'); + } finally { + setIsLoading(false); + } + }, [user]); + + + useEffect(() => { + loadData(); + }, [loadData]); return ( option.title || ''} getOptionKey={(option) => option.id} value={selectedBranch || null} @@ -65,7 +86,7 @@ const TableFilters = ({ onBranchChange, onSearchChange, selectedBranch, searchQu ...InputProps, endAdornment: ( <> - {loading && } + {isLoading && } {InputProps?.endAdornment} ),