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}
>
),