diff --git a/web/src/api/tasks.js b/web/src/api/tasks.js index 7231d38..cd36193 100644 --- a/web/src/api/tasks.js +++ b/web/src/api/tasks.js @@ -1,9 +1,6 @@ import api from './client'; export const TasksApi = { - getById(taskId) { - return api.get(`/form/${taskId}`).then((r) => r.data); - }, list(params = {}) { return api.get('/form/', { params }).then((r) => r.data); }, diff --git a/web/src/pages/ProjectPages/NavigationProjectPage.jsx b/web/src/pages/ProjectPages/NavigationProjectPage.jsx index 924361b..293b3b6 100644 --- a/web/src/pages/ProjectPages/NavigationProjectPage.jsx +++ b/web/src/pages/ProjectPages/NavigationProjectPage.jsx @@ -1,25 +1,36 @@ import { Box, Typography } from '@mui/material'; -import { useEffect, useMemo, useState, useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { toast } from 'react-toastify'; import { ProjectsApi } from '../../api/projects'; import { SspApi } from '../../api/ssp'; +import { useAuth } from '../../app/context/AuthProvider'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; import { DangerOutlinedButton, PrimaryButton } from '../../components/common/Buttons/Buttons'; -import { WhitePlus } from '../../components/common/icons/icons'; import Modal from '../../components/common/Modal/Modal'; +import { WhitePlus } from '../../components/common/icons/icons'; +import { ROLES_NAME_ID } from '../../constants/constants'; import { DEFAULT_PROJECT_CONFIG } from '../../constants/projectConfig'; import { ModalCreateProject } from '../ProjectsPage/ModalCreateProject'; import EditModal from './components/EditModal/EditModal'; -import ProjectsTable from './components/ProjectsTable/ProjectsTable'; import ProjectSummaryTable from './components/ProjectSummaryTable/ProjectSummaryTable'; +import ProjectsTable from './components/ProjectsTable/ProjectsTable'; 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 NavigationProjectPage = () => { const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const branchIdsParam = searchParams.get('branch_ids') || ''; + const branchIds = useMemo( + () => + branchIdsParam + .split(',') + .filter(Boolean) + .map(Number) + .filter((value) => Number.isInteger(value) && value > 0), + [branchIdsParam], + ); const [selectedRow, setSelectedRow] = useState(null); const [editModalOpen, setEditModalOpen] = useState(false); @@ -40,7 +51,7 @@ const NavigationProjectPage = () => { const { user } = useAuth(); // Состояния для фильтров - const [selectedBranch, setSelectedBranch] = useState(''); + const [selectedBranches, setSelectedBranches] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [selectedStatus, setSelectedStatus] = useState(''); const [archivedData, setArchivedData] = useState([]); @@ -59,22 +70,16 @@ const NavigationProjectPage = () => { const isAdmin = user.role_id === ROLES_NAME_ID.admin; if (isAdmin) { - const sspResponse = await ( - SspApi.getAll({ is_active: true }) - ); + const sspResponse = await SspApi.getAll({ is_active: true }); if (!sspResponse.success) { toast.error('Ошибка загрузки списка ССП'); return; } setOrgUnits(sspResponse.result); - setOrgUnitNames( - Object.fromEntries((sspResponse.result || []).map((orgUnit) => [orgUnit.id, orgUnit.title])), - ); + 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])), - ); + setOrgUnitNames(Object.fromEntries((user.org_units || []).map((orgUnit) => [orgUnit.id, orgUnit.title]))); } } catch (error) { console.error('Error loading data:', error); @@ -88,13 +93,20 @@ const NavigationProjectPage = () => { loadData(); }, [loadData]); - const createProjectConfig = useMemo(() => ({ - ...DEFAULT_PROJECT_CONFIG, - 'ССП/РФ': { - ...DEFAULT_PROJECT_CONFIG['ССП/РФ'], - options: orgUnits, - }, - }), [orgUnits]); + useEffect(() => { + setSelectedBranches(orgUnits.filter(({ id }) => branchIds.includes(id))); + }, [branchIds, orgUnits]); + + const createProjectConfig = useMemo( + () => ({ + ...DEFAULT_PROJECT_CONFIG, + 'ССП/РФ': { + ...DEFAULT_PROJECT_CONFIG['ССП/РФ'], + options: orgUnits, + }, + }), + [orgUnits], + ); useEffect(() => { let active = true; @@ -103,7 +115,7 @@ const NavigationProjectPage = () => { const loadProjects = async () => { setIsProjectsLoading(true); try { - const response = await ProjectsApi.listWithReports({ limit: 1000 }); + const response = await ProjectsApi.listWithReports(); if (active) setTableData(response.result || []); } catch (requestError) { console.error('Error loading projects with reports:', requestError); @@ -130,7 +142,7 @@ const NavigationProjectPage = () => { status_in: 'archived', offset, limit: archivedPagination.pageSize, - ...(selectedBranch?.id ? { branch_id: selectedBranch.id } : {}), + ...(selectedBranches.length ? { branch_ids: selectedBranches.map(({ id }) => id).join(',') } : {}), ...(searchQuery.trim() ? { search: searchQuery.trim() } : {}), }; @@ -160,7 +172,7 @@ const NavigationProjectPage = () => { user, isLoading, isArchiveMode, - selectedBranch, + selectedBranches, searchQuery, archivedPagination.pageIndex, archivedPagination.pageSize, @@ -180,7 +192,7 @@ const NavigationProjectPage = () => { setIsSummaryLoading(true); try { const response = await ProjectsApi.getSummary(selectedRow.original.id); - if (active) setSummaryData(response.result.filter(v => v.row_type !== 'PROJECT') || []); + if (active) setSummaryData(response.result.filter((v) => v.row_type !== 'PROJECT') || []); } catch (requestError) { console.error('Error loading project summary:', requestError); if (active) { @@ -202,7 +214,7 @@ const NavigationProjectPage = () => { return archivedData; } - if (!selectedBranch && !searchQuery && !selectedStatus) { + if (selectedBranches.length === 0 && !searchQuery && !selectedStatus) { return tableData; } @@ -214,8 +226,8 @@ const NavigationProjectPage = () => { return false; } // Фильтр по филиалу (только для корневых) - if (selectedBranch) { - return item.org_unit_id === selectedBranch.id; + if (selectedBranches.length > 0) { + return selectedBranches.some(({ id }) => item.org_unit_id === id); } return true; }) @@ -243,7 +255,7 @@ const NavigationProjectPage = () => { return parentMatches || hasMatchingChildren; }); - }, [archivedData, isArchiveMode, searchQuery, selectedBranch, selectedStatus, tableData]); + }, [archivedData, isArchiveMode, searchQuery, selectedBranches, selectedStatus, tableData]); const onEdit = useCallback((row) => { const isSmeta = row.depth > 0; @@ -258,9 +270,12 @@ const NavigationProjectPage = () => { setEditModalOpen(true); }, []); - const onNavigate = useCallback((row) => { - handleNavigateClick(row, navigate); - }, [navigate]); + const onNavigate = useCallback( + (row) => { + handleNavigateClick(row, navigate); + }, + [navigate], + ); const handleDeleteProject = async () => { if (!projectToDelete?.id) return; @@ -286,7 +301,8 @@ const NavigationProjectPage = () => { setIsSaving(true); try { if (formData._rowType === 'smeta') { - const { is_in_plan, is_in_plan_q2, is_in_plan_q3, is_in_plan_q4, development_block, reserve_to_prrs_ahr, reserve_to_prrs_kv } = formData; + const { is_in_plan, is_in_plan_q2, is_in_plan_q3, is_in_plan_q4, development_block, reserve_to_prrs_ahr, reserve_to_prrs_kv } = + formData; await ProjectsApi.updateSmeta(formData._projectId, formData.year, { is_in_plan, is_in_plan_q2, @@ -297,7 +313,23 @@ const NavigationProjectPage = () => { reserve_to_prrs_kv: reserve_to_prrs_kv === '' ? null : reserve_to_prrs_kv, }); } else { - const { name, status, technical_number, project_type, vsp_format, placement_type, object_address, staff_count, total_area, org_unit_id, krf_decision_date, fk_decision_date, board_decision_date, open_relocate_close_date, funding_by_ko_decision } = formData; + const { + name, + status, + technical_number, + project_type, + vsp_format, + placement_type, + object_address, + staff_count, + total_area, + org_unit_id, + krf_decision_date, + fk_decision_date, + board_decision_date, + open_relocate_close_date, + funding_by_ko_decision, + } = formData; await ProjectsApi.update(formData._projectId, { name, status: status || null, @@ -330,7 +362,13 @@ const NavigationProjectPage = () => { const handleBranchChange = (value) => { setSelectedRow(null); setSummaryData([]); - setSelectedBranch(value); + setSelectedBranches(value); + setSearchParams((currentParams) => { + const nextParams = new URLSearchParams(currentParams); + if (value.length > 0) nextParams.set('branch_ids', value.map(({ id }) => id).join(',')); + else nextParams.delete('branch_ids'); + return nextParams; + }); setArchivedPagination((current) => ({ ...current, pageIndex: 0 })); }; @@ -379,11 +417,10 @@ const NavigationProjectPage = () => { display: 'flex', flexDirection: 'column', }}> - - + {/* Компонент фильтров */} { orgUnitNames={orgUnitNames} /> - + {/* Модалка редактирования */} - setEditModalOpen(false)} rowData={selectedRowData} onSave={onSaveEdit} isSaving={isSaving} orgUnitNames={orgUnitNames} /> + setEditModalOpen(false)} + rowData={selectedRowData} + onSave={onSaveEdit} + isSaving={isSaving} + orgUnitNames={orgUnitNames} + /> setCreateModalOpen(false)} @@ -439,13 +479,10 @@ const NavigationProjectPage = () => { }> - - Проект «{projectToDelete?.name}» будет удалён без возможности восстановления. - + Проект «{projectToDelete?.name}» будет удалён без возможности восстановления. - ); }; diff --git a/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx b/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx index ef09ee0..b1a1cd0 100644 --- a/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx +++ b/web/src/pages/ProjectPages/components/TableFilters/TableFilters.jsx @@ -1,21 +1,14 @@ import { Search } from '@mui/icons-material'; import { Autocomplete, Box, Chip, CircularProgress, MenuItem, Paper, TextField } from '@mui/material'; -import { useEffect, useState, useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { SspApi } from '../../../../api/ssp'; import { useAuth } from '../../../../app/context/AuthProvider'; -import { toast } from 'react-toastify'; import { PROJECT_STATUSES, ROLES_NAME_ID } from '../../../../constants/constants'; const FILTER_PROJECT_STATUSES = ['created', 'agreed', 'archived']; -const TableFilters = ({ - onBranchChange, - onSearchChange, - onStatusChange, - selectedBranch, - selectedStatus, - searchQuery, -}) => { +const TableFilters = ({ onBranchChange, onSearchChange, onStatusChange, selectedBranches, selectedStatus, searchQuery }) => { const [branches, setBranches] = useState([]); const [branchInputValue, setBranchInputValue] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -28,18 +21,14 @@ const TableFilters = ({ const isAdmin = user.role_id === ROLES_NAME_ID.admin; if (isAdmin) { - const sspResponse = await ( - SspApi.getAll({ is_active: true }) - ); + const sspResponse = await SspApi.getAll({ is_active: true }); if (!sspResponse.success) { toast.error('Ошибка загрузки списка ССП'); return; } setBranches(sspResponse.result); - } else { setBranches(user.org_units ?? []); - } } catch (error) { console.error('Error loading data:', error); @@ -49,15 +38,10 @@ const TableFilters = ({ } }, [user]); - useEffect(() => { loadData(); }, [loadData]); - useEffect(() => { - setBranchInputValue(selectedBranch?.title || ''); - }, [selectedBranch]); - return ( option.title || ''} getOptionKey={(option) => option.id} - value={selectedBranch || null} + value={selectedBranches} inputValue={branchInputValue} - onInputChange={(event, newInputValue) => { + onInputChange={(_event, newInputValue) => { setBranchInputValue(newInputValue); }} - onChange={(event, newValue) => { - onBranchChange(newValue || ''); + onChange={(_event, newValue) => { + onBranchChange(newValue); }} + renderValue={(value, getItemProps) => + value + .slice(0, 2) + .map((option, index) => { + const { key, ...itemProps } = getItemProps({ index }); + return ; + }) + .concat(value.length > 2 ? [] : []) + } renderInput={(params) => { return ( - {(selectedBranch || searchQuery || selectedStatus) && ( + {(selectedBranches.length > 0 || searchQuery || selectedStatus) && ( { setBranchInputValue(''); - onBranchChange(''); + onBranchChange([]); onSearchChange(''); onStatusChange(''); }} diff --git a/web/src/pages/SvodPage/SvodPage.jsx b/web/src/pages/SvodPage/SvodPage.jsx index 7184bd3..fbb9fa5 100644 --- a/web/src/pages/SvodPage/SvodPage.jsx +++ b/web/src/pages/SvodPage/SvodPage.jsx @@ -2,6 +2,7 @@ import RefreshRoundedIcon from '@mui/icons-material/RefreshRounded'; import { Alert, Box, Button, Paper, Tab, Tabs, Typography } from '@mui/material'; import { MaterialReactTable, useMaterialReactTable } from 'material-react-table'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; import { blueColumn } from '../../components/RealtimeTable/constants/columnColors'; import { sectionCodeColor } from '../../components/RealtimeTable/constants/columnConfig'; @@ -173,6 +174,7 @@ const filterSummaryTree = (tree, query) => const countSummaryTree = (tree) => tree.reduce((count, row) => count + 1 + countSummaryTree(row.subRows), 0); export default function SvodPage() { + const navigate = useNavigate(); const { getSheetFilters, updateSheetFilters, getSummary, invalidateSummary } = useSvod(); const [sheet, setSheet] = useState('MAIN'); const [rows, setRows] = useState([]); @@ -225,6 +227,22 @@ export default function SvodPage() { loadSummary(); }, [invalidateSummary, loadSummary, selectedOrganizations, sheet, year]); + const openFilteredTasks = useCallback(() => { + const params = new URLSearchParams({ year: String(year) }); + const organizationIds = selectedOrganizations.map(({ id }) => id); + if (organizationIds.length > 0) params.set('org_units', organizationIds.join(',')); + if (sheet !== 'MAIN') params.set('form_type', sheet); + navigate(`/tasks?${params.toString()}`); + }, [navigate, selectedOrganizations, sheet, year]); + + const openFilteredProjects = useCallback(() => { + const params = new URLSearchParams(); + const organizationIds = selectedOrganizations.map(({ id }) => id); + if (organizationIds.length > 0) params.set('branch_ids', organizationIds.join(',')); + const query = params.toString(); + navigate(query ? `/projects?${query}` : '/projects'); + }, [navigate, selectedOrganizations]); + const filteredRows = useMemo(() => { const normalizedSearch = search.trim().toLowerCase(); const tree = buildSummaryTree(rows); @@ -356,13 +374,22 @@ export default function SvodPage() { {`Найдено ${filteredRowsCount.toLocaleString('ru-RU')} из ${rows.length.toLocaleString('ru-RU')} строк`} )} + {sheet === 'FORM_3' ? ( + + ) : ( + + )} diff --git a/web/src/pages/TasksPage/TasksPage.jsx b/web/src/pages/TasksPage/TasksPage.jsx index b7760ac..67f73bb 100644 --- a/web/src/pages/TasksPage/TasksPage.jsx +++ b/web/src/pages/TasksPage/TasksPage.jsx @@ -1,12 +1,12 @@ -import { Autocomplete, Button, FormControl, FormLabel, MenuItem, Select, TextField } from '@mui/material'; -import { useEffect, useMemo, useState } from 'react'; -import { useNavigate } from 'react-router'; +import { Autocomplete, Box, Button, FormControl, FormLabel, MenuItem, Select, TextField } from '@mui/material'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { toast } from 'react-toastify'; import { SspApi } from '../../api/ssp'; import { TasksApi } from '../../api/tasks'; import { useAuth } from '../../app/context/AuthProvider'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; -import { PrimaryButton } from '../../components/common/Buttons/Buttons'; +import { PrimaryButton, PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons'; import Modal from '../../components/common/Modal/Modal'; import { OrgUnitsAutocompletePaper } from '../../components/common/OrgUnitsAutocompletePaper/OrgUnitsAutocompletePaper'; import { renderOrgUnitValue } from '../../components/common/OrgUnitsAutocompletePaper/renderOrgUnitValue'; @@ -14,7 +14,6 @@ import { PaginatedList } from '../../components/common/PaginatedList'; import { TaskCardComponent } from '../../components/common/TaskCardComponent'; import { WhitePlus } from '../../components/common/icons/icons'; import { FORM_TYPE_OPTIONS, FORM_TYPE_TRANSLATE, ORG_UNIT_TYPE_OPTIONS, ROLES_NAME_ID } from '../../constants/constants'; -import { exportBulkForms, exportSingleForm } from '../../utils/exportFile'; import { PageContainer } from '../StyledForPage'; const modalSelectMenuProps = { @@ -29,19 +28,30 @@ const modalSelectSx = { }; export default function TasksPage() { - const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [loadingUpdate, setLoadingUpdate] = useState(false); const [modalOpen, setModalOpen] = useState(false); - const [query, setQuery] = useState(''); + + const yearFilter = searchParams.get('year') || ''; + const formTypeFilter = searchParams.get('form_type') || ''; + const orgUnitsFilter = searchParams.get('org_units') || ''; + const orgUnitIdsFilter = useMemo( + () => + orgUnitsFilter + .split(',') + .filter(Boolean) + .map(Number) + .filter((value) => Number.isInteger(value) && value > 0), + [orgUnitsFilter], + ); // Pagination state const [page, setPage] = useState(1); const [limit, setLimit] = useState(20); const [totalCount, setTotalCount] = useState(0); - const [isLoadAll, setisLoadAll] = useState(false); const [sspList, setSspList] = useState([]); const [formData, setFormData] = useState({ @@ -51,6 +61,13 @@ export default function TasksPage() { formTypeCode: '', }); + const yearOptions = useMemo(() => { + const currentYear = new Date().getFullYear(); + return Array.from({ length: 8 }, (_, index) => currentYear - 5 + index); + }, []); + + const selectedOrgUnits = useMemo(() => sspList.filter((unit) => orgUnitIdsFilter.includes(unit.id)), [sspList, orgUnitIdsFilter]); + const filteredOrgUnits = useMemo(() => { if (!formData.orgUnitType) return []; const isSsp = formData.orgUnitType === 'ssp'; @@ -67,8 +84,18 @@ export default function TasksPage() { const { user } = useAuth(); - const [exportMode, setExportMode] = useState(false); - const [exportList, setExportList] = useState([]); + const updateFilter = (name, value) => { + setSearchParams((currentParams) => { + const nextParams = new URLSearchParams(currentParams); + if (value) { + nextParams.set(name, value); + } else { + nextParams.delete(name); + } + return nextParams; + }); + }; + const openModal = () => { setModalOpen(true); }; @@ -134,83 +161,78 @@ export default function TasksPage() { handleFormChange('org_unit_id', checked ? filteredOrgUnits.map((unit) => unit.id) : []); }; - const loadTasks = async (currentPage = page, currentLimit = limit) => { - if (isLoadAll) return; - setLoading(true); - try { - const skip = (currentPage - 1) * currentLimit; - const params = { - offset: skip, - limit: currentLimit, - }; + const loadTasks = useCallback( + async (currentPage, currentLimit) => { + setLoading(true); + try { + const skip = (currentPage - 1) * currentLimit; + const params = { + offset: skip, + limit: currentLimit, + ...(yearFilter && { year: Number(yearFilter) }), + ...(formTypeFilter && { form_type: formTypeFilter }), + ...(orgUnitIdsFilter.length > 0 && { org_units: orgUnitIdsFilter.join(',') }), + }; - const res = await TasksApi.list(params); - const data = res.result; + const res = await TasksApi.list(params); + const data = res.result; - setItems(data); - setTotalCount(res.count); - } catch (error) { - toast.error('Ошибка загрузки задач'); - } finally { - setLoading(false); - } - }; + setItems(data); + setTotalCount(res.count); + } catch (_error) { + toast.error('Ошибка загрузки задач'); + } finally { + setLoading(false); + } + }, + [yearFilter, formTypeFilter, orgUnitIdsFilter], + ); - useEffect(() => { - setPage(1); - }, [query]); - - const handlePageChange = (event, value) => { + const handlePageChange = (_event, value) => { setPage(value); - loadTasks(value, limit); }; const handleLimitChange = (event) => { const newLimit = event.target.value; setLimit(newLimit); setPage(1); - loadTasks(1, newLimit); }; useEffect(() => { - if (modalOpen) { - SspApi.getAll({ is_active: true }).then((data) => { + if (!user) return; + if (user.role_id !== ROLES_NAME_ID.admin) { + setSspList(user.org_units || []); + return; + } + SspApi.getAll({ is_active: true }) + .then((data) => { if (data.result) { setSspList(data.result); } - }); - } - }, [modalOpen]); + }) + .catch(() => toast.error('Ошибка загрузки подразделений')); + }, [user]); useEffect(() => { loadTasks(page, limit); - }, []); + }, [page, limit, loadTasks]); - useEffect(() => { - setExportList([]); - }, [exportMode]); - - const handleChangeToExportList = (taskId, checked) => { - if (checked) { - setExportList((prev) => [...prev, taskId]); - } else { - const newExportList = exportList.filter((t) => t != taskId); - setExportList(newExportList); - } + const handleFilterChange = (name, value) => { + setPage(1); + updateFilter(name, value); }; - const handleBulkExport = async () => { - await exportBulkForms(exportList); - setExportMode(false); + const handleClearFilters = () => { + setPage(1); + setSearchParams((currentParams) => { + const nextParams = new URLSearchParams(currentParams); + nextParams.delete('year'); + nextParams.delete('form_type'); + nextParams.delete('org_units'); + return nextParams; + }); }; - const handleSingleExport = (taskId, taskTitle) => { - exportSingleForm(taskId, taskTitle); - }; - - // Calculate total pages - const totalPages = Math.ceil(totalCount / limit); - if (!user) return; return ( @@ -242,6 +264,56 @@ export default function TasksPage() { ) } + filterActions={ + + + + + + option.title || ''} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={selectedOrgUnits} + onChange={(_event, newValue) => handleFilterChange('org_units', newValue.map((unit) => unit.id).join(','))} + renderValue={renderOrgUnitValue} + renderInput={(params) => } + sx={{ width: 480, maxWidth: '100%' }} + /> + + + Сбросить + + + } /> option.id === value.id} renderValue={renderOrgUnitValue} value={filteredOrgUnits.filter((ssp) => formData.org_unit_id.includes(ssp.id))} - onChange={(event, newValue) => { + onChange={(_event, newValue) => { handleFormChange( 'org_unit_id', newValue.map((ssp) => ssp.id),