diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/Admin.routes.jsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/Admin.routes.jsx index f6f36cd74fc..77680409149 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/Admin.routes.jsx +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/Admin.routes.jsx @@ -1,5 +1,7 @@ import React, {lazy} from 'react'; import {Route, Routes } from 'react-router-dom'; +import OutcomeOutputManagementPage from "./indicator_manager/pages/OutcomeOutputManagementPage"; +import DisaggregationManagerPage from "./indicator_manager/pages/DisaggregationManagerPage"; const AdminNDDApp = lazy(() => import('./ndd')); const IndicatorManagerApp = lazy(() => import('./indicator_manager')); @@ -9,6 +11,8 @@ const AdminRoutes = () => { } /> } /> + } /> + } /> ); } diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/StartUp.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/StartUp.tsx index 96d2a12b2b8..a98ba8c4fb0 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/StartUp.tsx +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/StartUp.tsx @@ -10,6 +10,8 @@ import { getPrograms } from '../reducers/fetchProgramsReducer'; import { getSettings } from '../reducers/fetchSettingsReducer'; import { resetSizePerPage } from '../reducers/fetchIndicatorsReducer'; import {getAmpCategories} from "../reducers/fetchAmpCategoryReducer"; +import {getOutputs} from "../reducers/fetchOutputsReducer"; +import {getOutcomes} from "../reducers/fetchOutcomesReducer"; export const AdminIndicatorManagerContext = React.createContext({}); @@ -43,6 +45,8 @@ const Startup: React.FC = (props: any) => { dispatch(getSectors()); dispatch(getPrograms()); dispatch(getAmpCategories()); + dispatch(getOutputs()); + dispatch(getOutcomes()); dispatch(resetSizePerPage()); // eslint-disable-next-line }, []); diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewIndicatorModal.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewIndicatorModal.tsx index e6a3745bde4..8d0f30dfc96 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewIndicatorModal.tsx +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewIndicatorModal.tsx @@ -1,7 +1,5 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { - Form, Modal, Button, Col, Row -} from 'react-bootstrap'; +import { Card, Form, Modal, Button, Col, Row } from 'react-bootstrap'; import { Formik, FormikProps } from 'formik'; import Select from 'react-select'; import styles from './css/IndicatorModal.module.css'; @@ -11,12 +9,17 @@ import { useDispatch, useSelector } from 'react-redux'; import { BaseAndTargetValueType, DefaultComponentProps, ProgramSchemeType, SettingsType } from '../../types'; import { createIndicator } from '../../reducers/createIndicatorReducer'; import { getIndicators } from '../../reducers/fetchIndicatorsReducer'; +import { getOutcomes } from '../../reducers/fetchOutcomesReducer'; import Swal from 'sweetalert2' import withReactContent from 'sweetalert2-react-content'; import { checkObjectIsNull, extractChildrenFromProgramScheme } from '../../utils/helpers'; import useDidMountEffect from '../../utils/hooks'; import DateInput from '../DateInput'; import lodash from 'lodash'; +import { getResponsibleOrgs } from '../../reducers/fetchResponsibleOrgsReducer'; +import axios from 'axios'; +import Accordion from 'react-bootstrap/Accordion'; + const MySwal = withReactContent(Swal); @@ -31,17 +34,46 @@ interface IndicatorFormValues { name: string; description?: string; code: string; + relevanceForClimateChange?: string; + indicatorType?: number; sectors: number[]; + logframeLinks: string[]; + data?: string; + dataSource?: string; + disaggregation: number[]; + unitOfMeasure?: number; + calculationMethod?: string; + responsibleOrganizations: number[]; + frequency?: number; ascending: boolean; creationDate?: any; - programId: string; + programId?: number; base: BaseAndTargetValueType; target: BaseAndTargetValueType; - indicatorsCategory?: string; + outputId?: number; + outcomeId?: number; + indicatorsCategory?: number; + // Add editable disaggregation values + disaggregationValues?: Array<{ + parentCategoryId: number; + childCategoryId: number | null; + base: { + originalValue: string | number; + originalValueDate: string; + revisedValue: string | number; + revisedValueDate: string; + }; + target: { + originalValue: string | number; + originalValueDate: string; + revisedValue: string | number; + revisedValueDate: string; + }; + }>; } const AddNewIndicatorModal: React.FC = (props) => { - const { show, setShow, translations, filterBySector, filterByProgram } = props; + const { show, setShow, translations } = props; const ascendingOptions = [ { value: true, label: translations["amp.indicatormanager:true"] }, @@ -69,20 +101,27 @@ const AddNewIndicatorModal: React.FC = (props) => { const sectorsReducer = useSelector((state: any) => state.fetchSectorsReducer); const programsReducer = useSelector((state: any) => state.fetchProgramsReducer); const categoriesReducer = useSelector((state: any) => state.fetchAmpCategoryReducer); + const outcomesReducer = useSelector((state: any) => state.fetchOutcomesReducer); + const allOutcomes = outcomesReducer.outcomes || []; const [programFieldVisible, setProgramFieldVisible] = useState(false); const [selectedProgramSchemeId, setSelectedProgramSchemeId] = useState(null); const [sectors, setSectors] = useState<{ value: string, name: string }[]>([]); - const [categories, setCategories] = useState<{ value: string, name: string }[]>([]); const [programSchemes, setProgramSchemes] = useState<{ value: string, name: string }[]>([]); const [programs, setPrograms] = useState<{ value: string, label: string }[]>([]); + const [categories, setCategories] = useState<{ value: number, label: string }[]>([]); const [baseOriginalValueDateDisabled, setBaseOriginalValueDateDisabled] = useState(false); const [targetOriginalValueDateDisabled, setTargetOriginalValueDateDisabled] = useState(false); const formikRef = useRef>(null); + // --- Outcome/Output dropdown logic --- + const [filteredOutputs, setFilteredOutputs] = useState<{ id: number, name: string }[]>([]); + + const responsibleOrgOptions = useSelector((state: any) => state.fetchResponsibleOrgsReducer.options || []); + const getCategories = () => { const categoryData = categoriesReducer.categories.map((category: any) => ({ value: category.id, @@ -178,8 +217,28 @@ const AddNewIndicatorModal: React.FC = (props) => { getCategories(); getProgramSchemes(); getPrograms(); + getOutcomes(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sectorsReducer.sectors, programsReducer.programs, programsReducer.programSchemes, outcomesReducer.outcomes]) + + + const [selectedOutcomeId, setSelectedOutcomeId] = useState(null); + + useEffect(() => { + if (selectedOutcomeId) { + const found = allOutcomes.find(o => o.id === selectedOutcomeId); + setFilteredOutputs(found ? found.outputs : []); + } else { + setFilteredOutputs([]); + } + }, [selectedOutcomeId, allOutcomes]); + + useEffect(() => { + if (!responsibleOrgOptions.length) { + dispatch(getResponsibleOrgs()); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sectorsReducer.sectors, programsReducer.programs, programsReducer.programSchemes]) + }, [dispatch, responsibleOrgOptions.length]); console.log("indicator===>", createIndicatorState); @@ -221,8 +280,10 @@ const AddNewIndicatorModal: React.FC = (props) => { name: '', description: '', code: '', + relevanceForClimateChange: '', + indicatorType: undefined, sectors: [], - programId: '', + programId: undefined, creationDate: DateUtil.getCurrentDate().toString(), ascending: false, base: { @@ -237,9 +298,33 @@ const AddNewIndicatorModal: React.FC = (props) => { revisedValue: 0, revisedValueDate: '' }, - indicatorsCategory: '' + outputId: undefined, + outcomeId: undefined, + logframeLinks: [], + data: '', + dataSource: '', + disaggregation: [], + unitOfMeasure: undefined, + calculationMethod: '', + responsibleOrganizations: [], + frequency: undefined, + disaggregationValues: [], + }; + + // --- Dynamic category options from fetchAmpCategoryReducer --- + const getCategoryOptions = (keyName: string) => { + return categoriesReducer.categories + .filter((cat: any) => cat.ampCategoryClass && cat.ampCategoryClass.keyName === keyName) + .map((cat: any) => ({ value: cat.id, label: cat.value })); }; + const indicatorTypeOptions = getCategoryOptions('indicator_type'); + const disaggregationOptions = getCategoryOptions('indicator_disaggregation'); + const unitOfMeasureOptions = getCategoryOptions('indicator_unit_of_measure'); + const frequencyOptions = getCategoryOptions('indicator_frequency'); + + const [disaggregationChildren, setDisaggregationChildren] = useState<{[key: number]: any[]}>({}); + return ( // this modal wrapper should be a separate component that can be reused since the props are the same = (props) => { return; } + + // Format disaggregationValues date fields + const formattedDisaggregationValues = (values.disaggregationValues || []).map(dv => ({ + ...dv, + base: { + ...dv.base, + originalValueDate: dv.base?.originalValueDate ? formatDate(dv.base.originalValueDate) : null, + revisedValueDate: dv.base?.revisedValueDate ? formatDate(dv.base.revisedValueDate) : null, + }, + target: { + ...dv.target, + originalValueDate: dv.target?.originalValueDate ? formatDate(dv.target.originalValueDate) : null, + revisedValueDate: dv.target?.revisedValueDate ? formatDate(dv.target.revisedValueDate) : null, + } + })); + const indicatorData = { name, description, code, sectors, - programId: programId ? parseInt(programId) : null, + programId: programId ? programId : null, ascending, creationDate: creationDate ? formatDate(new Date(creationDate)) : null, base: checkObjectIsNull(base) ? null : { originalValue: base.originalValue ? lodash.toNumber(base.originalValue): null, - originalValueDate: base.originalValueDate ? DateUtil.formatJavascriptDate(base.originalValueDate) : null, + originalValueDate: base.originalValueDate ? formatDate(base.originalValueDate) : null, revisedValue: base.revisedValue ? lodash.toNumber(base.revisedValue) : null, - revisedValueDate: base.revisedValueDate ? DateUtil.formatJavascriptDate(base.revisedValueDate) : null, + revisedValueDate: base.revisedValueDate ? formatDate(base.revisedValueDate) : null, }, target: checkObjectIsNull(target) ? null : { originalValue: target.originalValue ? lodash.toNumber(target.originalValue) : null, - originalValueDate: target.originalValueDate ? DateUtil.formatJavascriptDate(target.originalValueDate) : null, + originalValueDate: target.originalValueDate ? formatDate(target.originalValueDate) : null, revisedValue: target.revisedValue ? lodash.toNumber(target.revisedValue) : null, - revisedValueDate: target.revisedValueDate ? DateUtil.formatJavascriptDate(target.revisedValueDate) : null, + revisedValueDate: target.revisedValueDate ? formatDate(target.revisedValueDate) : null, }, - indicatorsCategory + indicatorsCategory, + outputId: values.outputId, + outcomeId: values.outcomeId, + relevanceForClimateChange: values.relevanceForClimateChange, + indicatorType: values.indicatorType, + logframeLinks: values.logframeLinks, + data: values.data, + dataSource: values.dataSource, + disaggregation: values.disaggregation, + unitOfMeasure: values.unitOfMeasure, + calculationMethod: values.calculationMethod, + responsibleOrganizations: values.responsibleOrganizations, + frequency: values.frequency, + disaggregationValues: formattedDisaggregationValues, }; dispatch(createIndicator(indicatorData)); }} > - {(props) => ( -
- -
- - - {translations["amp.indicatormanager:indicator-name"]} - - - {props.errors.name} - - - - - {translations["amp.indicatormanager:indicator-code"]} - - - {props.errors.code} - - - - - - {translations["amp.indicatormanager:indicator-description"]} - - - {props.errors.description} - - - - - - - {translations["amp.indicatormanager:ascending"]} - { + props.setFieldValue('indicatorType', selectedValue?.value); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.indicatorType && props.touched.indicatorType) && styles.text_is_invalid}`} + classNamePrefix="select" + value={indicatorTypeOptions.find(opt => opt.value === props.values.indicatorType) || null} + /> + + +
+ {/* Categorization and Linkage */} +
{translations["amp.indicatormanager:categorization-linkage-info"] || "Categorization and Linkage"}
+
+ + + {translations["amp.indicatormanager:outcome"]} + ({ value: output.id, label: output.name }))} + onChange={(selectedValue) => { + props.setFieldValue('outputId', selectedValue?.value); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.outputId && props.touched.outputId) && styles.text_is_invalid}`} + classNamePrefix="select" + value={filteredOutputs.find(output => output.id === props.values.outputId) ? { value: props.values.outputId, label: filteredOutputs.find(output => output.id === props.values.outputId)?.name } : null} + isDisabled={!selectedOutcomeId} + /> + + + + + Link to Logframe (Program Scheme) + { + props.setFieldValue("programId", selectedValue?.value); + }} + isClearable + getOptionValue={(option) => option.value} + onBlur={props.handleBlur} + className={`basic-multi-select ${styles.input_field} ${(props.errors.programId && props.touched.programId) && styles.text_is_invalid}`} + classNamePrefix="select" + /> + + )} + - {translations["amp.indicatormanager:sectors"]} - { - (sectors.length > 0) ? ( - - ) - } + Sector + { - // set the formik value with the selected values and remove the label - props.setFieldValue('indicatorsCategory', parseInt(value?.value)); - }} - isClearable - getOptionValue={(option: any) => option.value} - onBlur={props.handleBlur} - className={`basic-multi-select ${(props.errors.indicatorsCategory && props.touched.indicatorsCategory) && styles.text_is_invalid}`} - classNamePrefix="select" + name="unitOfMeasure" + options={unitOfMeasureOptions} + onChange={(selectedValue) => { + props.setFieldValue('unitOfMeasure', selectedValue?.value); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.unitOfMeasure && props.touched.unitOfMeasure) && styles.text_is_invalid}`} + classNamePrefix="select" + value={unitOfMeasureOptions.find(opt => opt.value === props.values.unitOfMeasure) || null} /> - ) : ( + + + Calculation Method + + + + + + Disaggregation { - // set the formik value with the selected values and remove the label - if (selectedValue) { - handleProgramSchemeChange(selectedValue.value, props); - }else { - handleProgramSchemeChange(null, props); - setProgramFieldVisible(false); - } - }} - isClearable - getOptionValue={(option) => option.value} - onBlur={props.handleBlur} - className={`basic-multi-select ${styles.input_field}`} - classNamePrefix="select" - /> - ) : ( - { + props.setFieldValue('responsibleOrganizations', selectedValues.map((v: any) => v.value)); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.responsibleOrganizations && props.touched.responsibleOrganizations) && styles.text_is_invalid}`} + classNamePrefix="select" + value={responsibleOrgOptions.filter(opt => props.values.responsibleOrganizations?.includes(opt.value))} + /> + + + Frequency + { - // set the formik value with the selected values and remove the label - props.setFieldValue('programId', selectedValue?.value); - }} - isClearable - getOptionValue={(option) => option.value} - onBlur={props.handleBlur} - className={`basic-multi-select ${styles.input_field} ${(props.errors.programId && props.touched.programId) && styles.text_is_invalid}`} - classNamePrefix="select" - /> - ) : - { + if (value) props.setFieldValue('ascending', value.value) + }} + defaultValue={{ + value: false, + label: translations["amp.indicatormanager:true"] + }} + /> + + {props.errors.ascending && {props.errors.ascending}} + + + + + {translations["amp.indicatormanager:table-header-creation-date"]} + + + +
+ + + +
+ + + + +
+ )}}
); }; export default AddNewIndicatorModal; + diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/EditIndicatorModal.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/EditIndicatorModal.tsx index 78d48d4b9e2..8b5944f7654 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/EditIndicatorModal.tsx +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/EditIndicatorModal.tsx @@ -1,7 +1,7 @@ /* eslint-disable import/no-unresolved */ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { - Form, Modal, Button, Col, Row + Form, Modal, Button, Col, Row, Accordion, Card } from 'react-bootstrap'; import Select from 'react-select'; import { Formik, FormikProps } from 'formik'; @@ -19,6 +19,7 @@ import { checkObjectIsNull, extractChildrenFromProgramScheme, getProgamSchemeFor import useDidMountEffect from '../../utils/hooks'; import DateInput from '../DateInput'; import lodash from 'lodash'; +import axios from 'axios'; const MySwal = withReactContent(Swal); @@ -36,13 +37,41 @@ interface IndicatorFormValues { name: string; description?: string; code: string; + relevanceForClimateChange?: string; + indicatorType?: number; sectors: any[]; + logframeLinks: string[]; + data?: string; + dataSource?: string; + disaggregation: number[]; + unitOfMeasure?: number; + calculationMethod?: string; + responsibleOrganizations: number[]; + frequency?: number; ascending: boolean; creationDate?: string; - programId: string | any; + programId: number | any; base: BaseAndTargetValueType; target: BaseAndTargetValueType; - indicatorsCategory?: string; + outcomeId?: number; + outputId?: number; + // Add editable disaggregation values + disaggregationValues?: Array<{ + parentCategoryId: number; + childCategoryId: number | null; + base: { + originalValue: string | number; + originalValueDate: string; + revisedValue: string | number; + revisedValueDate: string; + }; + target: { + originalValue: string | number; + originalValueDate: string; + revisedValue: string | number; + revisedValueDate: string; + }; + }>; } const EditIndicatorModal: React.FC = (props) => { @@ -76,6 +105,11 @@ const EditIndicatorModal: React.FC = (props) => { const programsReducer = useSelector((state: any) => state.fetchProgramsReducer); const updateIndicatorReducer = useSelector((state: any) => state.updateIndicatorReducer); + // Add selectors for responsible orgs and outcomes + const responsibleOrgOptions = useSelector((state: any) => state.fetchResponsibleOrgsReducer.options || []); + const outcomesState = useSelector((state: any) => state.fetchOutcomesReducer); + const allOutcomes = outcomesState.outcomes || []; + const [programFieldVisible, setProgramFieldVisible] = useState(false); const [selectedProgramSchemeId, setSelectedProgramSchemeId] = useState(null); @@ -92,10 +126,19 @@ const EditIndicatorModal: React.FC = (props) => { const [baseOriginalValueDateDisabled, setBaseOriginalValueDateDisabled] = useState(false); const [targetOriginalValueDateDisabled, setTargetOriginalValueDateDisabled] = useState(false); + // --- Outcome/Output dropdown logic --- + const [allOutcomesData, setAllOutcomes] = useState<{ id: number, name: string, outputs: { id: number, name: string }[] }[]>([]); + const [selectedOutcomeId, setSelectedOutcomeId] = useState(indicator?.outcomeId ?? null); + const [filteredOutputs, setFilteredOutputs] = useState<{ id: number, name: string }[]>([]); + + // --- Add disaggregationChildren state --- + const [disaggregationChildren, setDisaggregationChildren] = useState<{[key: number]: any[]}>({}); + + // Utility to convert any date string to ISO format const convertDateToISO = (date?: string) => { - if (!date) { - return ''; - } + if (!date) return ''; + // Try to parse as ISO first, fallback to parsing with default format + if (/^\d{4}-\d{2}-\d{2}/.test(date)) return date; return DateUtil.toISO8601(date, globalSettings['default-date-format']); }; @@ -261,6 +304,8 @@ const EditIndicatorModal: React.FC = (props) => { }, []); + + useEffect(() => { getDefaultSectors(); getDefaultCategory(); @@ -297,13 +342,38 @@ const EditIndicatorModal: React.FC = (props) => { text: updateIndicatorReducer.loading ? translations["amp.indicatormanager:save-failed"] : updateIndicatorReducer.error, }); }, [updateIndicatorReducer]); + const convertDisaggregationDatesToISO = (disaggregationValues: any[] = []) => { + return disaggregationValues.map(dv => ({ + ...dv, + base: { + ...dv.base, + originalValueDate: dv.base?.originalValueDate ? convertDateToISO(dv.base.originalValueDate) : '', + revisedValueDate: dv.base?.revisedValueDate ? convertDateToISO(dv.base.revisedValueDate) : '', + }, + target: { + ...dv.target, + originalValueDate: dv.target?.originalValueDate ? convertDateToISO(dv.target.originalValueDate) : '', + revisedValueDate: dv.target?.revisedValueDate ? convertDateToISO(dv.target.revisedValueDate) : '', + } + })); + }; const initialValues: IndicatorFormValues = { name: indicator?.name || '', description: indicator?.description || '', code: indicator?.code || '', + relevanceForClimateChange: indicator?.relevanceForClimateChange || '', + indicatorType: indicator?.indicatorType || undefined, sectors: indicator?.sectors || [], - programId: '', + logframeLinks: indicator?.logframeLinks || [], + data: indicator?.data || '', + dataSource: indicator?.dataSource || '', + disaggregation: indicator?.disaggregation || [], + unitOfMeasure: indicator?.unitOfMeasure || undefined, + calculationMethod: indicator?.calculationMethod || '', + responsibleOrganizations: indicator?.responsibleOrganizations || [], + frequency: indicator?.frequency || undefined, + programId:indicator?.programId || undefined, ascending: indicator?.ascending || false, creationDate: indicator?.creationDate ? convertDateToISO(indicator?.creationDate) : '', base: { @@ -318,9 +388,52 @@ const EditIndicatorModal: React.FC = (props) => { revisedValue: indicator?.target?.revisedValue, revisedValueDate: indicator?.target?.revisedValueDate ? convertDateToISO(indicator?.target?.revisedValueDate) : '', }, - indicatorsCategory: indicator?.indicatorsCategory?.toString() || '' + outcomeId: indicator?.outcomeId || undefined, + outputId: indicator?.outputId || undefined, + // Add editable disaggregation values + disaggregationValues: convertDisaggregationDatesToISO(indicator?.disaggregationValues || []), + }; + + + const getCategoryOptions = (keyName: string, isMulti = false) => { + // Filter only category values with the correct keyName + return categoriesReducer.categories + .filter((cat: any) => cat.ampCategoryClass && cat.ampCategoryClass.keyName === keyName) + .map((cat: any) => ({ value: cat.id, label: cat.value })); }; + const indicatorTypeOptions = getCategoryOptions('indicator_type'); + const disaggregationOptions = getCategoryOptions('indicator_disaggregation', true); + const unitOfMeasureOptions = getCategoryOptions('indicator_unit_of_measure'); + const frequencyOptions = getCategoryOptions('indicator_frequency'); + + useEffect(() => { + fetch('/rest/amp-outcome-output/outcomes') + .then(res => res.json()) + .then (data => setAllOutcomes(data)); + }, []); + + useEffect(() => { + if (selectedOutcomeId) { + const found = allOutcomes.find(o => o.id === selectedOutcomeId); + setFilteredOutputs(found ? found.outputs : []); + // Set initial outputId if editing and outputId matches a filtered output + if (formikRef.current && indicator?.outputId) { + const match = found?.outputs.find(out => out.id === indicator.outputId); + if (match) { + formikRef.current.setFieldValue('outputId', indicator.outputId); + } else { + formikRef.current.setFieldValue('outputId', undefined); + } + } + } else { + setFilteredOutputs([]); + if (formikRef.current) { + formikRef.current.setFieldValue('outputId', undefined); + } + } + }, [selectedOutcomeId, allOutcomes]); + return ( // this modal wrapper should be a separate component that can be reused since the props are the same = (props) => { validationSchema={translatedIndicatorValidationSchema(translations)} innerRef={formikRef} onSubmit={(values) => { - const { - name, - description, - code, - sectors, - ascending, - programId, - creationDate, - base, - target, - indicatorsCategory - } = values; - - if (selectedProgramSchemeId && !programId) { + // Format disaggregationValues date fields + const formattedDisaggregationValues = (values.disaggregationValues || []).map(dv => ({ + ...dv, + base: { + ...dv.base, + originalValueDate: dv.base?.originalValueDate ? formatDate(dv.base.originalValueDate) : null, + revisedValueDate: dv.base?.revisedValueDate ? formatDate(dv.base.revisedValueDate) : null, + }, + target: { + ...dv.target, + originalValueDate: dv.target?.originalValueDate ? formatDate(dv.target.originalValueDate) : null, + revisedValueDate: dv.target?.revisedValueDate ? formatDate(dv.target.revisedValueDate) : null, + } + })); + const updatedIndicatorData = { + id: indicator.id, + name: values.name, + description: values.description, + code: values.code, + relevanceForClimateChange: values.relevanceForClimateChange, + indicatorType: values.indicatorType, + sectors: formatObjArrayToNumberArray(values.sectors), + logframeLinks: values.logframeLinks, + data: values.data, + dataSource: values.dataSource, + disaggregation: values.disaggregation, + unitOfMeasure: values.unitOfMeasure, + calculationMethod: values.calculationMethod, + responsibleOrganizations: values.responsibleOrganizations, + frequency: values.frequency, + programId: values.programId ? values.programId: null, + ascending: values.ascending, + creationDate: values.creationDate && formatDate(values.creationDate), + base: checkObjectIsNull(values.base) ? null : { + originalValue: values.base.originalValue ? lodash.toNumber(values.base.originalValue) : null, + originalValueDate: values.base.originalValueDate ? formatDate(values.base.originalValueDate) : null, + revisedValue: values.base.revisedValue ? lodash.toNumber(values.base.revisedValue) : null, + revisedValueDate: values.base.revisedValueDate ? formatDate(values.base.revisedValueDate) : null, + }, + target: checkObjectIsNull(values.target) ? null : { + originalValue: values.target.originalValue ? lodash.toNumber(values.target.originalValue) : null, + originalValueDate: values.target.originalValueDate ? formatDate(values.target.originalValueDate) : null, + revisedValue: values.target.revisedValue ? lodash.toNumber(values.target.revisedValue) : null, + revisedValueDate: values.target.revisedValueDate ? formatDate(values.target.revisedValueDate) : null, + }, + outcomeId: values.outcomeId, + outputId: values.outputId, + disaggregationValues: formattedDisaggregationValues, + indicatorsCategory: indicator.indicatorsCategory || undefined, + }; + + if (selectedProgramSchemeId && !values.programId) { MySwal.fire({ title: translations['amp.indicatormanager:error'], text: translations['amp.indicatormanager:errors-program-is-required'], icon: 'error', confirmButtonText: translations['amp.indicatormanager:ok'], }) - return; } - const updatedIndicatorData = { - id: indicator.id, - name, - description, - code, - sectors: formatObjArrayToNumberArray(sectors), - programId: programId ? parseInt(programId) : null, - ascending, - creationDate: creationDate && formatDate(creationDate), - base: checkObjectIsNull(base) ? null : { - originalValue: base.originalValue ? lodash.toNumber(base.originalValue) : null, - originalValueDate: base.originalValueDate ? formatDate(base.originalValueDate) : null, - revisedValue: base.revisedValue ? lodash.toNumber(base.revisedValue) : null, - revisedValueDate: base.revisedValueDate ? formatDate(base.revisedValueDate) : null, - }, - target: checkObjectIsNull(target) ? null : { - originalValue: target.originalValue ? lodash.toNumber(target.originalValue) : null, - originalValueDate: target.originalValueDate ? formatDate(target.originalValueDate) : null, - revisedValue: target.revisedValue ? lodash.toNumber(target.revisedValue) : null, - revisedValueDate: target.revisedValueDate ? formatDate(target.revisedValueDate) : null, - }, - indicatorsCategory : indicatorsCategory ? parseInt(indicatorsCategory) : null - }; - dispatch(updateIndicator(updatedIndicatorData as IndicatorObjectType)); }} > - {(props) => ( - <> + {(props) => { + // Fetch disaggregation children when disaggregation changes + useEffect(() => { + const selected = props.values.disaggregation; + if (selected && (selected.length === 1 || selected.length === 2)) { + Promise.all(selected.map(id => axios.get(`/rest/indicator_disaggregation/options/${id}`))) + .then((responses) => { + const childrenMap: {[key: number]: any[]} = {}; + selected.forEach((id, idx) => { + childrenMap[id] = responses[idx].data; + }); + setDisaggregationChildren(childrenMap); + + // --- Rebuild disaggregationValues to match current selection --- + let newDisaggValues: any[] = []; + if (selected.length === 1) { + // For single disaggregation, childCategoryId is null + const children = childrenMap[selected[0]] || []; + newDisaggValues = children.map((child: any) => { + // Try to find existing entry for this child + const existing = (props.values.disaggregationValues || []).find((v: any) => v.parentCategoryId === child.id && v.childCategoryId === null); + return existing || { + parentCategoryId: child.id, + childCategoryId: null, + base: { originalValue: '', originalValueDate: '', revisedValue: '', revisedValueDate: '' }, + target: { originalValue: '', originalValueDate: '', revisedValue: '', revisedValueDate: '' } + }; + }); + } else if (selected.length === 2) { + // For double disaggregation, cross product of children + const parents = childrenMap[selected[0]] || []; + const children = childrenMap[selected[1]] || []; + parents.forEach((parent: any) => { + children.forEach((child: any) => { + const existing = (props.values.disaggregationValues || []).find((v: any) => v.parentCategoryId === parent.id && v.childCategoryId === child.id); + newDisaggValues.push(existing || { + parentCategoryId: parent.id, + childCategoryId: child.id, + base: { originalValue: '', originalValueDate: '', revisedValue: '', revisedValueDate: '' }, + target: { originalValue: '', originalValueDate: '', revisedValue: '', revisedValueDate: '' } + }); + }); + }); + } + props.setFieldValue('disaggregationValues', newDisaggValues); + }); + } else { + setDisaggregationChildren({}); + props.setFieldValue('disaggregationValues', []); + } + }, [props.values.disaggregation]); + + // Helper to update a field in disaggregationValues + const updateDisaggregationField = (entryIdx: number, fieldPath: string[], value: any) => { + let updated = Array.isArray(props.values.disaggregationValues) ? [...props.values.disaggregationValues] : []; + if (!updated[entryIdx]) return; // Only update if entry exists + let obj = updated[entryIdx]; + for (let i = 0; i < fieldPath.length - 1; i++) { + obj = obj[fieldPath[i]]; + } + obj[fieldPath[fieldPath.length - 1]] = value; + props.setFieldValue('disaggregationValues', updated); + }; + + return (
- - - {translations["amp.indicatormanager:indicator-name"]} - - - {props.errors.name} - - - - - {translations["amp.indicatormanager:indicator-code"]} - - - {props.errors.code} - - - - - - {translations["amp.indicatormanager:indicator-description"]} - - - {props.errors.description} - - - - - - - {translations["amp.indicatormanager:ascending"]} - { - // set the formik value with the selected values and remove the label - const selectedValues = values.map((value: any) => parseInt(value.value)) - setDefaultSectors(values as any); - props.setFieldValue('sectors', selectedValues); - }} - onBlur={props.handleBlur} - className={`basic-multi-select ${(props.errors.sectors && props.touched.sectors) && styles.text_is_invalid}`} - classNamePrefix="select" - value={defaultSectors} - /> - - ) : ( - { - // set the formik value with the selected values and remove the label - setDefaultCategory(selectedValue) - props.setFieldValue('indicatorsCategory', selectedValue?.value); - }} - isClearable - getOptionValue={(option) => option.value} - onBlur={props.handleBlur} - className={`basic-multi-select ${(props.errors.indicatorsCategory && props.touched.indicatorsCategory) && styles.text_is_invalid}`} - classNamePrefix="select" - value={defaultCategory} - /> - ) : ( - { - // set the formik value with the selected values and remove the label - if (selectedValue) { - setDefaultProgramScheme(selectedValue); - handleProgramSchemeChange(selectedValue.value, props); - } - }} - isClearable - getOptionValue={(option) => option.value} - onBlur={props.handleBlur} - className={`basic-multi-select ${styles.input_field}`} - classNamePrefix="select" - value={defaultProgramScheme} - /> - ) : ( - { - // set the formik value with the selected values and remove the label - props.setFieldValue("programId", selectedValue?.value); - }} - isClearable - getOptionValue={(option) => option.value} - onBlur={props.handleBlur} - className={`basic-multi-select ${styles.input_field} ${(props.errors.programId && props.touched.programId) && styles.text_is_invalid}`} - classNamePrefix="select" - defaultValue={defaultProgram} - /> - ) : - { + props.setFieldValue('indicatorType', selectedValue?.value); }} onBlur={props.handleBlur} - name="base.revisedValueDate" - className={`${styles.input_field} ${(props.errors.base?.revisedValueDate && props.touched.base?.revisedValueDate) && styles.text_is_invalid}`} - id="baseRevisedValueDate" - inputRef={baseRevisedValueDateRef} + className={`basic-multi-select ${(props.errors.indicatorType && props.touched.indicatorType) && styles.text_is_invalid}`} + classNamePrefix="select" + value={indicatorTypeOptions.find(opt => opt.value === props.values.indicatorType) || null} /> - - - {props.errors.base?.revisedValueDate} - - - - -

{translations["amp.indicatormanager:target-values"]}

+
+ {/* Categorization and Linkage */} +
{translations["amp.indicatormanager:categorization-linkage-info"] || "Categorization and Linkage"}
+
- - {translations["amp.indicatormanager:target-value"]} - + {translations["amp.indicatormanager:outcome"]} + ({ value: output.id, label: output.name }))} + onChange={(selectedValue) => { + props.setFieldValue('outputId', selectedValue?.value); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.outputId && props.touched.outputId) && styles.text_is_invalid}`} + classNamePrefix="select" + value={filteredOutputs.find(output => output.id === props.values.outputId) ? { value: props.values.outputId, label: filteredOutputs.find(output => output.id === props.values.outputId)?.name } : null} + isDisabled={!selectedOutcomeId} + /> + + + + + Link to Logframe (Program Scheme) + { + props.setFieldValue("programId", selectedValue?.value); + }} + isClearable + getOptionValue={(option) => option.value} + onBlur={props.handleBlur} + className={`basic-multi-select ${styles.input_field} ${(props.errors.programId && props.touched.programId) && styles.text_is_invalid}`} + classNamePrefix="select" + defaultValue={defaultProgram} + /> + + )} + + + + Sector + { + props.setFieldValue('unitOfMeasure', selectedValue?.value); }} onBlur={props.handleBlur} - name="target.revisedValueDate" - className={`${styles.input_field} ${(props.errors.target?.revisedValueDate && props.touched.target?.revisedValueDate) && styles.text_is_invalid}`} - id="targetRevisedValueDate" - inputRef={targetRevisedValueDateRef} + className={`basic-multi-select ${(props.errors.unitOfMeasure && props.touched.unitOfMeasure) && styles.text_is_invalid}`} + classNamePrefix="select" + value={unitOfMeasureOptions.find(opt => opt.value === props.values.unitOfMeasure) || null} /> + + + Calculation Method + + + + + + + Disaggregation + { + props.setFieldValue('responsibleOrganizations', selectedValues.map((v: any) => v.value)); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.responsibleOrganizations && props.touched.responsibleOrganizations) && styles.text_is_invalid}`} + classNamePrefix="select" + value={responsibleOrgOptions.filter(opt => props.values.responsibleOrganizations?.includes(opt.value))} + /> + + + Frequency + { + if (value) props.setFieldValue('ascending', value.value) + }} + defaultValue={{ + value: false, + label: translations["amp.indicatormanager:true"] + }} + /> - {props.errors.target?.revisedValueDate} + {props.errors.ascending} + + + {translations["amp.indicatormanager:table-header-creation-date"]} + + - +
@@ -811,12 +1437,11 @@ const EditIndicatorModal: React.FC = (props) => { - - )} - + )}}
); }; export default EditIndicatorModal; + diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeModal.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeModal.tsx new file mode 100644 index 00000000000..81eeefb3068 --- /dev/null +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeModal.tsx @@ -0,0 +1,112 @@ +import React, { useRef } from 'react'; +import { Modal, Button, Form, Row, Col } from 'react-bootstrap'; +import { Formik, FormikProps, Form as FormikForm, Field } from 'formik'; +import * as Yup from 'yup'; +import styles from './css/IndicatorModal.module.css'; + +interface AddNewOutcomeModalProps { + show: boolean; + setShow: (show: boolean) => void; + onSubmit?: (outcome: { name: string; description?: string }) => void; + initialName?: string; + initialDescription?: string; + translations?: Record; +} + +const OutcomeModal: React.FC = ({ show, setShow, onSubmit, initialName = '', initialDescription = '', translations = {} }) => { + const nodeRef = useRef(null); + const validationSchema = Yup.object().shape({ + name: Yup.string().required(translations['amp.outcomeoutput:errors-name-required'] || 'Name is required'), + description: Yup.string() + }); + + const initialValues = { + name: initialName, + description: initialDescription + }; + + const handleClose = () => setShow(false); + + return ( + + + {translations['amp.outcomeoutput:modal-title-outcome'] || 'Add New Outcome'} + + { + if (onSubmit) onSubmit(values); + setShow(false); + resetForm(); + }} + > + {(props: FormikProps<{ name: string; description?: string }>) => ( +
+ +
+ + + {translations['amp.outcomeoutput:outcome-name'] || 'Outcome Name'} + + + {props.errors.name} + + + + + + {translations['amp.outcomeoutput:outcome-description'] || 'Outcome Description'} + + + {props.errors.description} + + + +
+
+ + + + +
+ )} +
+
+ ); +}; + +export default OutcomeModal; diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutputModal.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutputModal.tsx new file mode 100644 index 00000000000..187b249ca74 --- /dev/null +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutputModal.tsx @@ -0,0 +1,146 @@ +import React, { useRef } from 'react'; +import { Modal, Button, Form, Row, Col } from 'react-bootstrap'; +import { Formik, FormikProps } from 'formik'; +import * as Yup from 'yup'; +import styles from './css/IndicatorModal.module.css'; + +interface Outcome { + id: number; + name: string; +} + +interface AddNewOutputModalProps { + show: boolean; + setShow: (show: boolean) => void; + onSubmit?: (output: { name: string; description?: string; outcomeId: number }) => void; + initialName?: string; + initialDescription?: string; + selectedOutcome?: Outcome; + translations?: Record; + loading?: boolean; +} + +const OutputModal: React.FC = ({ + show, + setShow, + onSubmit, + initialName = '', + initialDescription = '', + selectedOutcome = undefined, + translations = {}, + loading = false +}) => { + const nodeRef = useRef(null); + + // Only render if selectedOutcome is provided + if (!selectedOutcome) return null; + + const validationSchema = Yup.object().shape({ + name: Yup.string().required(translations['amp.outcomeoutput:errors-name-required'] || 'Name is required'), + description: Yup.string(), + outcomeId: Yup.number().required('Outcome is required') + }); + + const initialValues = { + name: initialName, + description: initialDescription, + outcomeId: selectedOutcome.id + }; + + const handleClose = () => setShow(false); + + return ( + + + {translations['amp.outcomeoutput:modal-title-output'] || 'Add New Output'} + + { + if (onSubmit) onSubmit(values); + setShow(false); + resetForm(); + }} + > + {(props: FormikProps<{ name: string; description?: string; outcomeId: number }>) => ( +
+ +
+ + + {translations['amp.outcomeoutput:output-name'] || 'Output Name'} + + + {props.errors.name} + + + + + + {translations['amp.outcomeoutput:output-description'] || 'Output Description'} + + + {props.errors.description} + + + + + + {translations['amp.outcomeoutput:linked-outcome'] || 'Linked Outcome'} + + + +
+
+ + + + +
+ )} +
+
+ ); +}; + +export default OutputModal; diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/ViewIndicatorModal.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/ViewIndicatorModal.tsx index 5d1723c9ec8..e732221555a 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/ViewIndicatorModal.tsx +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/ViewIndicatorModal.tsx @@ -1,194 +1,525 @@ /* eslint-disable import/no-unresolved */ import React, { useLayoutEffect } from 'react'; -import { Modal, Row, } from 'react-bootstrap'; +import { Modal, Row, Col, Badge } from 'react-bootstrap'; import backdropStyles from './css/IndicatorModal.module.css'; import styles from './css/ViewIndicatorModal.module.css'; import { DefaultComponentProps, IndicatorObjectType, ProgramObjectType, SectorObjectType } from '../../types'; import { useSelector } from 'react-redux'; -import { extractChildrenFromProgramScheme } from '../../utils/helpers'; interface ViewIndicatorModalProps extends DefaultComponentProps { - show: boolean; - setShow: React.Dispatch>; - indicator: IndicatorObjectType; + show: boolean; + setShow: React.Dispatch>; + indicator: IndicatorObjectType; } -const colorOptions = [ - { value: 'ocean', label: 'Ocean', color: '#00B8D9' }, - { value: 'blue', label: 'Blue', color: '#0052CC' }, - { value: 'purple', label: 'Purple', color: '#5243AA' }, - { value: 'red', label: 'Red', color: '#FF5630' }, - { value: 'orange', label: 'Orange', color: '#FF8B00' }, - { value: 'yellow', label: 'Yellow', color: '#FFC400' }, - { value: 'green', label: 'Green', color: '#36B37E' }, - { value: 'forest', label: 'Forest', color: '#00875A' }, - { value: 'slate', label: 'Slate', color: '#fffff2' }, - { value: 'silver', label: 'Silver', color: '#666666' }, -]; - const ViewIndicatorModal: React.FC = (props) => { - const { show, setShow, indicator, translations } = props; - const sectorsReducer = useSelector((state: any) => state.fetchSectorsReducer); - const programsReducer = useSelector((state: any) => state.fetchProgramsReducer); - - const handleClose = () => setShow(false); - - const [sectorData, setSectorData] = React.useState([]); - const [programData, setProgramData] = React.useState([]); - - const getSectorData = () => { - if (!indicator) return; - const sectorIds = indicator.sectors; - const sectorData = sectorsReducer.sectors.filter((sector: any) => sectorIds.includes(sector.id)); - setSectorData(sectorData); - }; - - const getProgramData = () => { - if (!indicator) return; - const programId = indicator.programId; - const children = extractChildrenFromProgramScheme(programsReducer.programs); - const programData = children.filter((program: any) => programId === program.id); - setProgramData(programData); - }; - - useLayoutEffect(() => { - getSectorData(); - getProgramData(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indicator]); - - return ( - // this modal wrapper should be a separate component that can be reused since the props are the same - - - {translations["amp.indicatormanager:view-indicator"]} - - {indicator ? - -
- -
-

{translations["amp.indicatormanager:view-indicator-id"]}

-

{indicator.id}

-
-
-

{translations["amp.indicatormanager:indicator-name"]}

-

{indicator.name}

-
-
- -
-

{translations["amp.indicatormanager:indicator-code"]}

-

{indicator.code}

-
-
-

{translations["amp.indicatormanager:indicator-description"]}

-

{indicator.description === "" || '' ? translations["amp.indicatormanager:no-description-available"]: indicator.description}

-
-
+ const { show, setShow, indicator, translations } = props; + const sectorsReducer = useSelector((state: any) => state.fetchSectorsReducer); + const programsReducer = useSelector((state: any) => state.fetchProgramsReducer); + const categoriesReducer = useSelector((state: any) => state.fetchAmpCategoryReducer); + const outcomesReducer = useSelector((state: any) => state.fetchOutcomesReducer); + const outputsReducer = useSelector((state: any) => state.fetchOutputsReducer); + const responsibleOrgsReducer = useSelector((state: any) => state.fetchResponsibleOrgsReducer); - -
-

{translations["amp.indicatormanager:ascending"]}

-

{indicator.ascending ? 'True' : 'False'}

-
+ const handleClose = () => setShow(false); -
-

{translations["amp.indicatormanager:table-header-creation-date"]}

-

{indicator.creationDate}

-
-
+ const [sectorData, setSectorData] = React.useState([]); - -

{translations["amp.indicatormanager:sectors"]}

-
- {sectorData.length > 0 ? sectorData.map((sector) => ( -

{sector.name}

- )) : -

{translations["amp.indicatormanager:no-data"]}

- } -
+ const getSectorData = () => { + if (!indicator) return; + const sectorIds = indicator.sectors; + const sectorData = sectorsReducer.sectors.filter((sector: any) => sectorIds.includes(sector.id)); + setSectorData(sectorData); + }; -
+ useLayoutEffect(() => { + getSectorData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [indicator]); - -

{translations["amp.indicatormanager:programs"]}

- {programData.length > 0 ? programData.map((program) => ( -

{program.name}

- )) : -

{translations["amp.indicatormanager:no-data"]}

- } -
+ // Helper functions for lookups + const getCategoryLabel = (id: number | undefined) => { + if (!id) return translations["amp.indicatormanager:no-data"]; + const found = categoriesReducer.categories.find((cat: any) => cat.id === id); + return found ? found.value : id; + }; + const getOutcomeLabel = (id: number | undefined) => { + if (!id) return translations["amp.indicatormanager:no-data"]; + const found = outcomesReducer.outcomes.find((o: any) => o.id === id); + return found ? found.name : id; + }; + const getOutputLabel = (id: number | undefined) => { + if (!id) return translations["amp.indicatormanager:no-data"]; + const found = outputsReducer.outputs.find((o: any) => o.id === id); + return found ? found.name : id; + }; + const getResponsibleOrgLabels = (ids: number[] = []) => { + if (!ids.length) return [translations["amp.indicatormanager:no-data"]]; + return ids.map(id => { + const found = responsibleOrgsReducer.options.find((org: any) => org.value === id); + return found ? found.label : id; + }); + }; + const getProgramLabel = (id: number | null) => { + if (!id) return translations["amp.indicatormanager:no-data"]; + const found = programsReducer.programs.find((p: any) => p.id === id); + return found ? found.name : id; + }; -
-

{translations["amp.indicatormanager:base-values"]}

- -
-
{translations["amp.indicatormanager:original-base-value"]}
-

{indicator.base?.originalValue ?? translations["amp.indicatormanager:no-data"]}

-
-
-
{translations["amp.indicatormanager:original-value-date"]}
-

{indicator.base?.originalValueDate ?? translations["amp.indicatormanager:no-data"]}

-
-
+ return ( + + + + + {translations["amp.indicatormanager:view-indicator"]} + + + {indicator ? + +
+ {/* Core Indicator Information Section */} +
+

+ + {translations["amp.indicatormanager:core-info"] || "Core Indicator Information"} +

+ + +
{translations["amp.indicatormanager:indicator-name"]}
+
{indicator.name}
+ + +
{translations["amp.indicatormanager:indicator-code"]}
+
{indicator.code}
+ +
+ + +
{translations["amp.indicatormanager:indicator-description"]}
+
+ {indicator.description === "" || !indicator.description ? + {translations["amp.indicatormanager:no-description-available"]} : + indicator.description + } +
+ +
+ + +
{translations["amp.indicatormanager:relevance-for-climate-change"]}
+
{indicator.relevanceForClimateChange || {translations["amp.indicatormanager:no-data"]}}
+ + +
{translations["amp.indicatormanager:type"] || "Type"}
+
{getCategoryLabel(indicator.indicatorType)}
+ +
+
- -
-
{translations["amp.indicatormanager:revised-value"]}
-

{indicator.base?.revisedValue ?? translations["amp.indicatormanager:no-data"]}

-
-
-
{translations["amp.indicatormanager:revised-value-date"]}
-

{indicator.base?.revisedValueDate ?? translations["amp.indicatormanager:no-data"]}

-
-
-
+ {/* Categorization and Linkage Section */} +
+

+ + {translations["amp.indicatormanager:categorization-linkage-info"] || "Categorization and Linkage"} +

+ + +
{translations["amp.indicatormanager:outcome"]}
+
{getOutcomeLabel(indicator.outcomeId)}
+ + +
{translations["amp.indicatormanager:output"]}
+
{getOutputLabel(indicator.outputId)}
+ +
+ + +
{translations["amp.indicatormanager:logframe-links"] || "Link to Logframe (Program)"}
+
{getProgramLabel(indicator.programId)}
+ +
+ + +
{translations["amp.indicatormanager:sectors"]}
+
+ {sectorData.length > 0 ? ( +
    + {sectorData.map((sector) => ( +
  • {sector.name}
  • + ))} +
+ ) : ( + {translations["amp.indicatormanager:no-data"]} + )} +
+ +
+
-
-

{translations["amp.indicatormanager:target-values"]}

- -
-
{translations["amp.indicatormanager:target-value"]}
-

{indicator.target?.originalValue ?? translations["amp.indicatormanager:no-data"]}

-
-
-
{translations["amp.indicatormanager:target-value-date"]}
-

{indicator.target?.originalValueDate ?? translations["amp.indicatormanager:no-data"]}

-
-
+ {/* Data Definition and Sourcing Section */} +
+

+ + {translations["amp.indicatormanager:data"] || "Data Definition and Sourcing"} +

+ + +
{translations["amp.indicatormanager:data"]}
+
{indicator.data || {translations["amp.indicatormanager:no-data"]}}
+ + +
{translations["amp.indicatormanager:data-source"]}
+
{indicator.dataSource || {translations["amp.indicatormanager:no-data"]}}
+ +
+ + +
{translations["amp.indicatormanager:disaggregation"]}
+
+ {indicator.disaggregation && indicator.disaggregation.length > 0 ? ( +
    + {indicator.disaggregation.map((item: any, idx: number) => ( +
  • {getCategoryLabel(item)}
  • + ))} +
+ ) : ( + {translations["amp.indicatormanager:no-data"]} + )} +
+ + +
{translations["amp.indicatormanager:unit-of-measure"]}
+
{getCategoryLabel(indicator.unitOfMeasure)}
+ +
+ + +
{translations["amp.indicatormanager:calculation-method"]}
+
{indicator.calculationMethod || {translations["amp.indicatormanager:no-data"]}}
+ +
+
- -
-
{translations["amp.indicatormanager:revised-value"]}
-

{indicator.target?.revisedValue ?? translations["amp.indicatormanager:no-data"]}

-
-
-
{translations["amp.indicatormanager:revised-value-date"]}
-

{indicator.target?.revisedValueDate ?? translations["amp.indicatormanager:no-data"]}

-
-
-
+ {/* Disaggregation Values Section */} +
+

+ + {translations["amp.indicatormanager:disaggregation-values"] || "Disaggregation Values"} +

+ + +
{translations["amp.indicatormanager:disaggregation-values"] || "Disaggregation Values"}
+
+ {indicator.disaggregationValues && indicator.disaggregationValues.length > 0 ? ( +
+ {/* Transposed Table */} + {(() => { + // Group disaggregationValues by parentCategoryId + const parentGroups: Record = {}; + (indicator.disaggregationValues || []).forEach((dv: any) => { + if (!parentGroups[dv.parentCategoryId]) parentGroups[dv.parentCategoryId] = []; + parentGroups[dv.parentCategoryId].push(dv); + }); + // Build ordered parent list + const parentIds = Object.keys(parentGroups).map(Number); + // Helper to get child list for a parent (exclude childCategoryId=null) + const getChildren = (parentId: number) => parentGroups[parentId].filter(dv => dv.childCategoryId !== null); + // Helper to get dv for parent/child + const getDV = (parentId: number, childId: number|null) => parentGroups[parentId].find(dv => dv.childCategoryId === childId); + // Table header + return ( + + + {/* Row 1: Parent categories */} + + + {parentIds.map(parentId => { + const children = getChildren(parentId); + if (children.length > 0) { + return ( + + ); + } else { + // No children, colSpan=2 + return ( + + ); + } + })} + + {/* Row 2: Child categories */} + {parentIds.length > 0 && getChildren(parentIds[0]).length > 0?( + + + {parentIds.map(parentId => { + const children = getChildren(parentId); + if (children.length > 0) { + return children.map(child => ( + + )); + } else { + // No children, just Value/Date + return [ + + ]; + } + })} + ): + ()} + {/* Row 3: Value/Date subcolumns */} + + + {parentIds.map(parentId => { + const children = getChildren(parentId); + if (children.length > 0) { + return children.map(child => [ + , + + ]); + } else { + // No children, just Value/Date + return [ + , + + ]; + } + })} + + + + {/* Original Base Value */} + + + {parentIds.map(parentId => { + const children = getChildren(parentId); + if (children.length > 0) { + return children.map(child => [ + , + + ]); + } else { + // No children, use parent only + const dv = getDV(parentId, null); + return [ + , + + ]; + } + })} + + {/* Revised Base Value */} + + + {parentIds.map(parentId => { + const children = getChildren(parentId); + if (children.length > 0) { + return children.map(child => [ + , + + ]); + } else { + const dv = getDV(parentId, null); + return [ + , + + ]; + } + })} + + {/* Original Target Value */} + + + {parentIds.map(parentId => { + const children = getChildren(parentId); + if (children.length > 0) { + return children.map(child => [ + , + + ]); + } else { + const dv = getDV(parentId, null); + return [ + , + + ]; + } + })} + + {/* Revised Target Value */} + + + {parentIds.map(parentId => { + const children = getChildren(parentId); + if (children.length > 0) { + return children.map(child => [ + , + + ]); + } else { + const dv = getDV(parentId, null); + return [ + , + + ]; + } + })} + + +
+ {/* Parent disaggregation label for first column */} + {parentIds.length > 0 ? getCategoryLabel(parentGroups[parentIds[0]][0].parentDisaggregationId) : translations["amp.indicatormanager:no-parent-category"] || "No Parent"} + + {getCategoryLabel(parentId) || translations["amp.indicatormanager:no-parent-category"] || "No Parent"} + + {getCategoryLabel(parentId) || translations["amp.indicatormanager:no-parent-category"] || "No Parent"} +
+ {/* Child category label for first column */} + {parentIds.length > 0 && getChildren(parentIds[0]).length > 0 + ? getCategoryLabel(parentGroups[parentIds[0]][0].childDisaggregationId) + : translations["amp.indicatormanager:no-child-category"] || "No Child"} + + {getCategoryLabel(child.childCategoryId)} + + {getCategoryLabel(parentId)} +
+
{translations["amp.indicatormanager:value"] || "Value"}
+
+
{translations["amp.indicatormanager:date"] || "Date"}
+
{translations["amp.indicatormanager:value"] || "Value"}{translations["amp.indicatormanager:date"] || "Date"}
{translations["amp.indicatormanager:original-base-value"] || "Original Base Value"}{child.base.originalValue}{child.base.originalValueDate}{dv?.base.originalValue}{dv?.base.originalValueDate}
{translations["amp.indicatormanager:revised-base-value"] || "Revised Base Value"}{child.base.revisedValue}{child.base.revisedValueDate}{dv?.base.revisedValue}{dv?.base.revisedValueDate}
{translations["amp.indicatormanager:original-target-value"] || "Original Target Value"}{child.target.originalValue}{child.target.originalValueDate }{dv?.target.originalValue}{dv?.target.originalValueDate}
{translations["amp.indicatormanager:revised-target-value"] || "Revised Target Value"}{child.target.revisedValue}{child.target.revisedValueDate}{dv?.target.revisedValue}{dv?.target.revisedValueDate}
+ ); + })()} +
+ ) : ( +
{translations["amp.indicatormanager:no-disaggregation-values"] || "No Disaggregation Values"}
+ )} +
+ +
+
+ {/* Responsibility and Frequency Section */} +
+

+ + {translations["amp.indicatormanager:responsibility-frequency-info"] || "Responsibility and Frequency"} +

+ + +
{translations["amp.indicatormanager:responsible-organizations"]}
+
+ {indicator.responsibleOrganizations && indicator.responsibleOrganizations.length > 0 ? ( +
    + {getResponsibleOrgLabels(indicator.responsibleOrganizations).map((org, idx) => ( +
  • {org}
  • + ))} +
+ ) : ( + {translations["amp.indicatormanager:no-data"]} + )} +
+ + +
{translations["amp.indicatormanager:frequency"]}
+
{getCategoryLabel(indicator.frequency)}
+ +
+
-
- : - -

{translations["amp.indicatormanager:view-error"]}

-
- } + {/* Value Tracking Section */} +
+

+ + {translations["amp.indicatormanager:value-tracking"] || "Value Tracking"} +

+ + +
+ + {translations["amp.indicatormanager:base-values"]} +
+
+
{translations["amp.indicatormanager:original-base-value"]}
+
{indicator.base?.originalValue ?? {translations["amp.indicatormanager:no-data"]}}
+
+
+
{translations["amp.indicatormanager:original-value-date"]}
+
{indicator.base?.originalValueDate ?? {translations["amp.indicatormanager:no-data"]}}
+
+
+
{translations["amp.indicatormanager:revised-value"]}
+
{indicator.base?.revisedValue ?? {translations["amp.indicatormanager:no-data"]}}
+
+
+
{translations["amp.indicatormanager:revised-value-date"]}
+
{indicator.base?.revisedValueDate ?? {translations["amp.indicatormanager:no-data"]}}
+
+ + +
+ + {translations["amp.indicatormanager:target-values"]} +
+
+
{translations["amp.indicatormanager:target-value"]}
+
{indicator.target?.originalValue ?? {translations["amp.indicatormanager:no-data"]}}
+
+
+
{translations["amp.indicatormanager:target-value-date"]}
+
{indicator.target?.originalValueDate ?? {translations["amp.indicatormanager:no-data"]}}
+
+
+
{translations["amp.indicatormanager:revised-value"]}
+
{indicator.target?.revisedValue ?? {translations["amp.indicatormanager:no-data"]}}
+
+
+
{translations["amp.indicatormanager:revised-value-date"]}
+
{indicator.target?.revisedValueDate ?? {translations["amp.indicatormanager:no-data"]}}
+
+ +
+
- - ); + {/* Other Considerations Section */} +
+

+ + {translations["amp.indicatormanager:other-considerations"] || "Other Considerations"} +

+ + +
{translations["amp.indicatormanager:table-header-creation-date"]}
+
{indicator.creationDate}
+ + +
{translations["amp.indicatormanager:ascending"]}
+
+ + {indicator.ascending ? translations["amp.indicatormanager:yes"] : translations["amp.indicatormanager:no"]} + +
+ +
+
+
+
: + +
+ +

{translations["amp.indicatormanager:view-error"]}

+
+
+ } +
+ ); }; export default ViewIndicatorModal; diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/css/IndicatorModal.module.css b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/css/IndicatorModal.module.css index 80b94cb88a4..361ef43aed0 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/css/IndicatorModal.module.css +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/css/IndicatorModal.module.css @@ -87,4 +87,58 @@ input::-webkit-inner-spin-button { /* Firefox */ input[type=number] { -moz-appearance: textfield; -} \ No newline at end of file +} + +.sectionTitle { + color: #1769aa; + background: #eaf4fb; + border-left: 5px solid #1769aa; + padding: 8px 16px; + margin-bottom: 0.5rem; + font-weight: bold; + font-size: 1.1rem; + border-radius: 4px 4px 0 0; +} + +.sectionContainer { + border: 1.5px solid #e0e0e0; + border-radius: 4px; + background: #fff; + padding: 18px 16px; + margin-bottom: 2rem; + box-shadow: 0 2px 8px rgba(23, 105, 170, 0.04); +} + +.accordionHeader { + background: #e9ecef; + color: #2c3e50; + font-size: 1.1rem; + font-weight: 600; + border-bottom: 1px solid #d1d1d1; + padding: 12px 16px; + cursor: pointer; + transition: background 0.2s; +} + +.accordionHeader:hover { + background: #d6e0f0; + color: #1a252f; +} + +.accordionHeaderTitle { + font-size: 1.05rem; + font-weight: 500; + letter-spacing: 0.02em; + color: #34495e; +} + +.accordionChildTitle { + font-size: 1rem; + font-weight: 600; + color: #1769aa; + margin-bottom: 8px; + letter-spacing: 0.01em; + background: #f3f8fc; + border-radius: 3px; + padding: 4px 8px; +} diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/css/ViewIndicatorModal.module.css b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/css/ViewIndicatorModal.module.css index 89292c0d172..8265fd6698e 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/css/ViewIndicatorModal.module.css +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/css/ViewIndicatorModal.module.css @@ -3,6 +3,8 @@ height: 100%; display: flex; flex-direction: column; + background: #fff; /* Ensure light background */ + padding: 0; } .view_row { @@ -27,7 +29,7 @@ } .view_item { - width: 50%; + width: 100%; /* Remove 50% restriction for full width */ text-align: left; } @@ -36,3 +38,80 @@ margin-right: -15px; margin-left: -15px; } + +.section { + margin-bottom: 24px; + padding-bottom: 16px; +} +.section_title { + font-size: 1.2rem; + font-weight: bold; + margin-bottom: 16px; + display: flex; + align-items: center; +} +.important { + font-weight: bold; + color: #36B37E; +} +.label { + font-weight: bold; + margin-bottom: 4px; + color: #333; +} +.value { + font-size: 1rem; + color: #222; +} +.no_data { + color: #999; + font-style: italic; +} +.tags_container { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.tag { + display: inline-block; + padding: 4px 12px; + border-radius: 12px; + font-size: 0.95rem; + font-weight: bold; + margin-right: 4px; + margin-bottom: 4px; +} +.modal_title { + font-size: 1.5rem; + font-weight: bold; + color: #0052CC; + display: flex; + align-items: center; +} +.modal_header { + background: #f7f7f7; + border-bottom: 2px solid #e0e0e0; +} +.modal_body { + background: #fff !important; /* Force modal body to be light */ +} + +.hierarchical_table { + width: 100%; + background: #fff; +} + +.error_state { + text-align: center; + color: #FF5630; + padding: 32px 0; +} +.modal_backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + z-index: 100; +} diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/diss.png b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/diss.png new file mode 100644 index 00000000000..0115d9a330c Binary files /dev/null and b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/diss.png differ diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/table/IndicatorTable.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/table/IndicatorTable.tsx index c133518a300..4f9c9c2a28f 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/table/IndicatorTable.tsx +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/table/IndicatorTable.tsx @@ -13,6 +13,7 @@ import {DefaultComponentProps, IndicatorObjectType, ProgramObjectType, SettingsT import {getIndicators} from '../../reducers/fetchIndicatorsReducer'; + // Modals import ViewIndicatorModal from '../modals/ViewIndicatorModal'; import EditIndicatorModal from '../modals/EditIndicatorModal'; @@ -24,10 +25,14 @@ interface IndicatorTableProps extends DefaultComponentProps { const IndicatorTable: React.FC = ({ translations }) => { const dispatch = useDispatch(); - const { indicators: fetchedIndicators, loading } = useSelector((state: any) => state.fetchIndicatorsReducer); const globalSettings: SettingsType = useSelector((state: any) => state.fetchSettingsReducer.settings); const sectorsReducer = useSelector((state: any) => state.fetchSectorsReducer); const programsReducer = useSelector((state: any) => state.fetchProgramsReducer); + const outcomesReducer = useSelector((state: any) => state.fetchOutcomesReducer); + const ampCategoryReducer = useSelector((state: any) => state.fetchAmpCategoryReducer); + const outputsReducer = useSelector((state: any) => state.fetchOutputsReducer); + const { indicators: fetchedIndicators, loading } = useSelector((state: any) => state.fetchIndicatorsReducer); + useLayoutEffect(() => { dispatch(getIndicators()); @@ -40,6 +45,11 @@ const IndicatorTable: React.FC = ({ translations }) => { const [showDeleteIndicatorModal, setShowDeleteIndicatorModal] = useState(false); const [selectedSector, setSelectedSector] = useState(0); const [selectedProgram, setSelectedProgram] = useState(0); + const [selectedOutcome, setSelectedOutcome] = useState(0); + const [selectedOutput, setSelectedOutput] = useState(0); + const [selectedIndicatorType, setSelectedIndicatorType] = useState(0); + const [indicatorCode, setIndicatorCode] = useState(''); + const [indicatorName, setIndicatorName] = useState(''); const [indicators, setIndicators] = useState(fetchedIndicators); useEffect(() => { @@ -92,7 +102,6 @@ const IndicatorTable: React.FC = ({ translations }) => { return _cell.map((sectorId: any) => { if (sectorId) { const foundSector = !sectorsReducer.loading && sectorsReducer.sectors.find((sector: any) => sector.id === sectorId); - if (foundSector) { return foundSector.name } else { @@ -108,21 +117,12 @@ const IndicatorTable: React.FC = ({ translations }) => {
{ _cell.map((sectorId: any) => { - const foundSector = !sectorsReducer.loading && sectorsReducer.sectors.find((sector: any) => sector.id === sectorId); - if (foundSector) { - return {foundSector.name} -
- -
+ return {foundSector.name}
} - return ( - - {sectorId} -
-
+ {sectorId}
) }) } @@ -131,45 +131,85 @@ const IndicatorTable: React.FC = ({ translations }) => { }, } ]: []), - - ...(globalSettings["indicator-filter-by-program"] ? [ - { - dataField: 'programs', - text: translations['amp.indicatormanager:programs'], - sort: true, - headerStyle: {width: '40%'}, - csvFormatter: (_cell: any, row: any) => { - const programId = row.programId; - if (programId) { - const foundProgram = !programsReducer.loading && programsReducer.programs.find((program: any) => program.id === programId); - if (foundProgram) { - return foundProgram.name; - } else { - return programId; - } - - }else { - return ''; - } - }, - formatter: (_cell: any, row: any) => { - const programId = row.programId; - const foundProgram = !programsReducer.loading && programsReducer.programs.find((program: any) => program.id === programId); - - if (foundProgram) { - return {foundProgram.name} -
-
- } - return ( - - {programId} -
-
- ) + // Program column + ...(globalSettings["indicator-filter-by-program"] ? [ + { + dataField: 'programId', + text: translations['amp.indicatormanager:programs'], + sort: true, + headerStyle: {width: '40%'}, + csvFormatter: (_cell: any, row: any) => { + const programId = row.programId; + if (programId) { + const foundProgram = !programsReducer.loading && programsReducer.programs.find((program: any) => program.id === programId); + if (foundProgram) { + return foundProgram.name; + } else { + return programId; } + } else { + return ''; + } + }, + formatter: (_cell: any, row: any) => { + const programId = row.programId; + const foundProgram = !programsReducer.loading && programsReducer.programs.find((program: any) => program.id === programId); + if (foundProgram) { + return {foundProgram.name}
} - ]: []), + return ( + {programId}
+ ) + } + } + ]: []), + // Outcome column + { + dataField: 'outcome', + text: translations['amp.indicatormanager:outcome'], + sort: true, + headerStyle: { width: '20%' }, + formatter: (_cell: any, row: any) => { + if (outcomesReducer.loading) return ''; + const foundOutcome = !outcomesReducer.loading && outcomesReducer.outcomes.find((outcome: any) => outcome.id === row.outcomeId); + return foundOutcome ? foundOutcome.name : ''; + }, + csvFormatter: (_cell: any, row: any) => { + if (outcomesReducer.loading) return ''; + const foundOutcome = !outcomesReducer.loading && outcomesReducer.programs.find((outcome: any) => outcome.id === row.outcomeId); + return foundOutcome ? foundOutcome.name : ''; + } + }, + // Output column + { + dataField: 'output', + text: translations['amp.indicatormanager:output'], + sort: true, + headerStyle: { width: '20%' }, + formatter: (_cell: any, row: any) => { + const foundOutput = !outputsReducer.loading && outputsReducer.outputs.find((output: any) => output.id === row.outputId); + return foundOutput ? foundOutput.name : row.output || ''; + }, + csvFormatter: (_cell: any, row: any) => { + const foundOutput = !outputsReducer.loading && outputsReducer.outputs.find((output: any) => output.id === row.outputId); + return foundOutput ? foundOutput.name : row.output || ''; + } + }, + // Indicator Type column + { + dataField: 'indicatorType', + text: translations['amp.indicatormanager:indicator-type'], + sort: true, + headerStyle: { width: '15%' }, + formatter: (_cell: any, row: any) => { + const foundType = !ampCategoryReducer.loading && ampCategoryReducer.categories.find((cat: any) => cat.id === row.indicatorType); + return foundType ? foundType.value : row.indicatorType || ''; + }, + csvFormatter: (_cell: any, row: any) => { + const foundType = !ampCategoryReducer.loading && ampCategoryReducer.categories.find((cat: any) => cat.id === row.indicatorType); + return foundType ? foundType.value : row.indicatorType || ''; + } + }, { dataField: 'creationDate', text: translations['amp.indicatormanager:table-header-creation-date'], @@ -210,40 +250,36 @@ const IndicatorTable: React.FC = ({ translations }) => { ), }, + ], []); - const handleFilterIndicators = () => { - if (Number(selectedSector) === 0) { - setIndicators(fetchedIndicators); - return; + // Filtering logic for all fields + useEffect(() => { + let filtered = fetchedIndicators; + if (Number(selectedSector) !== 0) { + filtered = filtered.filter((indicator: IndicatorObjectType) => indicator.sectors.includes(Number(selectedSector))); } - const filteredIndicators = fetchedIndicators.filter((indicator: IndicatorObjectType) => { - return indicator.sectors.includes(Number(selectedSector)); - }); - setIndicators([]); - setIndicators(filteredIndicators); - } - - const handleFilterIndicatorsByProgram = () => { - if (Number(selectedProgram) === 0) { - setIndicators(fetchedIndicators); - return; + if (Number(selectedProgram) !== 0) { + filtered = filtered.filter((indicator: IndicatorObjectType) => indicator.programId === Number(selectedProgram)); + } + if (Number(selectedOutcome) !== 0) { + filtered = filtered.filter((indicator: IndicatorObjectType) => indicator.outcomeId === Number(selectedOutcome)); + } + if (Number(selectedOutput) !== 0) { + filtered = filtered.filter((indicator: IndicatorObjectType) => indicator.outputId === Number(selectedOutput)); + } + if (Number(selectedIndicatorType) !== 0) { + filtered = filtered.filter((indicator: IndicatorObjectType) => indicator.indicatorType === Number(selectedIndicatorType)); } - const filteredIndicators = fetchedIndicators.filter((indicator: IndicatorObjectType) => { - // @ts-ignore - return indicator.programId === Number(selectedProgram); - }); - setIndicators([]); - setIndicators(filteredIndicators); - } - - useEffect(() => { - handleFilterIndicators(); - }, [selectedSector]); - useEffect(() => { - handleFilterIndicatorsByProgram(); - }, [selectedProgram]); + if (indicatorCode) { + filtered = filtered.filter((indicator: IndicatorObjectType) => indicator.code?.toLowerCase().includes(indicatorCode.toLowerCase())); + } + if (indicatorName) { + filtered = filtered.filter((indicator: IndicatorObjectType) => indicator.name?.toLowerCase().includes(indicatorName.toLowerCase())); + } + setIndicators(filtered); + }, [fetchedIndicators, selectedSector, selectedProgram, selectedOutcome, selectedOutput, selectedIndicatorType]); return ( <> @@ -290,13 +326,16 @@ const IndicatorTable: React.FC = ({ translations }) => { translations={translations} filterBySector={globalSettings["indicator-filter-by-sector"]} filterByProgram={globalSettings["indicator-filter-by-program"]} + setSelectedOutcome={setSelectedOutcome} + setSelectedOutput={setSelectedOutput} + setSelectedIndicatorType={setSelectedIndicatorType} /> } ); }; -const InidcatorTableMemo = React.memo(IndicatorTable); +const IndicatorTableMemo = React.memo(IndicatorTable); const mapStateToProps = (state: any) => ({ translations: state.translationsReducer.translations, @@ -304,4 +343,4 @@ const mapStateToProps = (state: any) => ({ const mapDispatchToProps = (dispatch: any) => bindActionCreators({}, dispatch); -export default connect(mapStateToProps, mapDispatchToProps)(InidcatorTableMemo); +export default connect(mapStateToProps, mapDispatchToProps)(IndicatorTableMemo); diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/table/Table.module.css b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/table/Table.module.css index 1bb3de309be..70090978974 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/table/Table.module.css +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/table/Table.module.css @@ -58,3 +58,236 @@ .filter_select { width: 350px; } +/* Table.module.css - Improved Styles */ + +.actions_container { + display: flex; + flex-direction: column; + gap: 10px; +} + +.action_button { + display: flex; + align-items: center; + justify-content: center; + padding: 8px 12px; + border-radius: 4px; + font-weight: 500; + transition: all 0.2s ease; +} + +.action_button:hover { + transform: translateY(-1px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.export_button { + display: flex; + align-items: center; + justify-content: center; + padding: 8px 12px; + background-color: #6c757d; + color: white; + border: none; + border-radius: 4px; + font-weight: 500; + transition: all 0.2s ease; +} + +.export_button:hover { + background-color: #5a6268; + color: white; + transform: translateY(-1px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.filters_container { + background-color: #d8e0e2; /* Changed to requested color */ + padding: 16px; + border-radius: 8px; + border: 1px solid #c8d0d2; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.filters_header { + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid #c0c8ca; +} + +.filters_header h5 { + margin: 0; + color: #2c3e50; + font-weight: 600; + font-size: 1.1rem; +} + +.filter_item, .search_item { + margin-bottom: 12px; +} + +.filter_label { + display: block; + font-weight: 600; + margin-bottom: 6px; + color: #2c3e50; + font-size: 0.875rem; +} + +.filter_select { + width: 100%; +} + +.filter_select :global(.react-select__control) { + min-height: 38px; + border-radius: 4px; + border: 1px solid #ced4da; + box-shadow: none; + background-color: white; +} + +.filter_select :global(.react-select__control:hover) { + border-color: #80bdff; +} + +.filter_select :global(.react-select__control--is-focused) { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.filter_select :global(.react-select__value-container) { + padding: 2px 8px; +} + +.filter_select :global(.react-select__placeholder) { + color: #6c757d; +} + +.filter_select :global(.react-select__menu) { + z-index: 10; +} + +.search_wrapper { + position: relative; +} + +.search_bar { + width: 100%; + padding: 8px 12px 8px 36px; + border: 1px solid #ced4da; + border-radius: 4px; + font-size: 0.875rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + background-color: white; +} + +.search_bar:focus { + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.search_icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #6c757d; + font-size: 0.875rem; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .actions_container { + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + } + + .action_button, .export_button { + flex: 1; + min-width: 140px; + margin-right: 8px; + margin-bottom: 8px; + } + + .filters_container { + padding: 12px; + } +} + +@media (max-width: 576px) { + .actions_container { + flex-direction: column; + } + + .action_button, .export_button { + width: 100%; + margin-right: 0; + } + + .filter_item, .search_item { + margin-bottom: 16px; + } + + .filter_select :global(.react-select__control) { + min-height: 42px; + } +} + +/* Dark mode support - adjusted for the new background */ +@media (prefers-color-scheme: dark) { + .filters_container { + background-color: #d8e0e2; /* Keep the same color in dark mode */ + border-color: #b8c0c2; + } + + .filters_header { + border-bottom-color: #b8c0c2; + } + + .filters_header h5 { + color: #2c3e50; + } + + .filter_label { + color: #2c3e50; + } + + .filter_select :global(.react-select__control) { + background-color: white; + border-color: #ced4da; + color: #495057; + } + + .filter_select :global(.react-select__single-value) { + color: #495057; + } + + .filter_select :global(.react-select__menu) { + background-color: white; + } + + .filter_select :global(.react-select__option) { + background-color: white; + color: #495057; + } + + .filter_select :global(.react-select__option--is-focused) { + background-color: #f8f9fa; + } + + .search_bar { + background-color: white; + border-color: #ced4da; + color: #495057; + } + + .search_bar::placeholder { + color: #6c757d; + } + + .search_icon { + color: #6c757d; + } +} diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/table/Table.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/table/Table.tsx index e7ba81397ec..e8ca2846223 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/table/Table.tsx +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/table/Table.tsx @@ -25,6 +25,7 @@ import { useSelector, useDispatch } from 'react-redux'; import { setSizePerPage} from '../../reducers/fetchIndicatorsReducer'; import Select from "react-select"; import {formatProgramSchemeToSelect} from "../../utils/helpers"; +import { useNavigate } from 'react-router-dom'; interface SkeletonTableProps extends DefaultComponentProps { columns: any; @@ -34,6 +35,9 @@ interface SkeletonTableProps extends DefaultComponentProps { programs?: ProgramObjectType[]; setSelectedSector: React.Dispatch>; setSelectedProgram: React.Dispatch>; + setSelectedOutcome: React.Dispatch>; + setSelectedOutput: React.Dispatch>; + setSelectedIndicatorType: React.Dispatch>; filterBySector: boolean; filterByProgram: boolean; } @@ -72,6 +76,9 @@ const SkeletonTable: React.FC = (props) => { sectors, setSelectedSector, setSelectedProgram, + setSelectedOutcome, + setSelectedOutput, + setSelectedIndicatorType, translations, filterBySector, filterByProgram, @@ -80,6 +87,9 @@ const SkeletonTable: React.FC = (props) => { const dispatch = useDispatch(); const programSchemeReducer = useSelector((state: any) => state.fetchProgramsReducer); const sectorsReducer = useSelector((state: any) => state.fetchSectorsReducer); + const outcomesReducer = useSelector((state: any) => state.fetchOutcomesReducer); + const categoriesReducer = useSelector((state: any) => state.fetchAmpCategoryReducer); + const outputsReducer = useSelector((state: any) => state.fetchOutputsReducer); const programConfiguration: ProgramSchemeType [] = programSchemeReducer.programSchemes; const sizePerPage: number = useSelector((state: any) => state.fetchIndicatorsReducer.sizePerPage); @@ -107,6 +117,19 @@ const SkeletonTable: React.FC = (props) => { label: translations['amp.indicatormanager:all-sectors'] }]); + const [outcomeOptions, setOutcomeOptions] = useState([{ + value: 0, + label: translations['amp.indicatormanager:all-outcomes'] + }]); + const [outputOptions, setOutputOptions] = useState([{ + value: 0, + label: translations['amp.indicatormanager:all-outputs'] + }]); + const [indicatorTypeOptions, setIndicatorTypeOptions] = useState([{ + value: 0, + label: translations['amp.indicatormanager:all-indicator-types'] + }]); + const showAddNewIndicatorModalHandler = () => { setShowAddNewIndicatorModal(true); }; @@ -115,16 +138,40 @@ const SkeletonTable: React.FC = (props) => { const formatPrograms = formatProgramSchemeToSelect(programConfiguration); setProgramOptions(prevState => [...prevState, ...formatPrograms]); + if (sectors) { + const formatSectors = sectors.map((sector) => ({ + value: sector.id, + label: sector.name, + })); + setSectorOptions(prevState => [...prevState, ...formatSectors]); + } - if (sectors) { - const formatSectors = sectors.map((sector) => ({ - value: sector.id, - label: sector.name, - })); + // Set outcome options from Redux + if (outcomesReducer && outcomesReducer.outcomes) { + const formattedOutcomes = outcomesReducer.outcomes.map((outcome: any) => ({ + value: outcome.id, + label: outcome.name, + })); + setOutcomeOptions([{ value: 0, label: translations['amp.indicatormanager:all-outcomes'] }, ...formattedOutcomes]); + } - setSectorOptions(prevState => [...prevState, ...formatSectors]); - } - }, []); + if (outputsReducer && outputsReducer.outputs) { + const formattedOutputs = outputsReducer.outputs.map((output: any) => ({ + value: output.id, + label: output.name, + })); + setOutputOptions([{ value: 0, label: translations['amp.indicatormanager:all-outputs'] }, ...formattedOutputs]); + } + + // Set indicator type options from Redux + if (categoriesReducer && categoriesReducer.categories) { + const formattedIndicatorTypes = categoriesReducer.categories.filter((cat: any)=>cat.ampCategoryClass.keyName === 'indicator_type').map((indicatorType: any) => ({ + value: indicatorType.id, + label: indicatorType.value, + })); + setIndicatorTypeOptions([{ value: 0, label: translations['amp.indicatormanager:all-indicator-types'] }, ...formattedIndicatorTypes]); + } + }, [sectors, programConfiguration, translations]); useEffect(() => { setSelectedSector(0); @@ -169,33 +216,33 @@ const SkeletonTable: React.FC = (props) => { sizePerPage: sizePerPage, hidePageListOnlyOnePage: true, sizePerPageRenderer: ({ - options, - currSizePerPage, - onSizePerPageChange, - }: { - options: any; - currSizePerPage: number; - onSizePerPageChange: (value: number) => void; + options, + currSizePerPage, + onSizePerPageChange, + }: { + options: any; + currSizePerPage: number; + onSizePerPageChange: (value: number) => void; }) => ( - - handleSizePerPageChange(onSizePerPageChange, parseInt(e.target.value))} - > - { - options.map((option: any) => ( - - )) - } - - + + handleSizePerPageChange(onSizePerPageChange, parseInt(e.target.value))} + > + { + options.map((option: any) => ( + + )) + } + + ) }; @@ -206,192 +253,235 @@ const SkeletonTable: React.FC = (props) => { } }; - return ( - <> - - - - { - (props: ToolkitContextType) => ( -
- - - -

{title}

- - -
- -
+ const navigate = useNavigate(); - - -
- - - - {' '} - {translations['amp.indicatormanager:export-csv']} - -
+ return ( + <> + + + + { + (props: ToolkitContextType) => ( +
+ + + +

{title}

+ + +
+ +
- - {/* searchbar should be far right on the col */} - -
+ + +
+ + + + + + {' '} + {translations['amp.indicatormanager:export-csv']} + +
+ - {filterBySector && ( -
- {translations['amp.indicatormanager:sectors']} - { - sectorsReducer.sectors.length > 0 ? ( - setSelectedSector(item.value)} + components={{ IndicatorSeparator: () => null }} + menuPlacement="auto" + classNamePrefix="react-select" + /> + ) : ( { - setSelectedProgram(item.value) - }} - components={{ - IndicatorSeparator: () => null, - }} - /> - ) : ( - <> - { - !programSchemeReducer.loading && ( - setSelectedProgram(item.value)} + components={{ IndicatorSeparator: () => null }} + menuPlacement="auto" + classNamePrefix="react-select" + /> + ) : ( + setSelectedOutcome(opt?.value || 0)} + className={styles.filter_select} + components={{ IndicatorSeparator: () => null }} + menuPlacement="auto" + classNamePrefix="react-select" + />
- ) - } - + -
- -
-
+ +
+ {translations['amp.indicatormanager:output']} + setSelectedIndicatorType(opt?.value || 0)} + className={styles.filter_select} + components={{ IndicatorSeparator: () => null }} + menuPlacement="auto" + classNamePrefix="react-select" + /> +
+ + +
+ +
- - -
- ( -
-
{translations['amp.indicatormanager:no-data']}
-
- )} - /> -
- ) - } -
+
+ ( +
+
{translations['amp.indicatormanager:no-data']}
+
+ )} + /> +
+ ) + } + - - + + ); }; diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/config/initialTranslations.json b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/config/initialTranslations.json index 1d90982c158..396f17c44c5 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/config/initialTranslations.json +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/config/initialTranslations.json @@ -4,7 +4,7 @@ "amp.indicatormanager:table-title": "Indicator Manager", "amp.indicatormanager:sectors": "Sectors", "amp.indicatormanager:all-sectors": "All Sectors", - "amp.indicatormanager:search": "Search", + "amp.indicatormanager:search": "Search by name or code", "amp.indicatormanager:export-csv": "Export CSV", "amp.indicatormanager:table-header-id": "ID", "amp.indicatormanager:table-header-code": "Code", @@ -27,11 +27,11 @@ "amp.indicatormanager:no-data": "No Data Available", "amp.indicatormanager:all": "All", "amp.dashboard:add-new": "Add New Indicator", + "amp.dashboard:outcome-and-output-management": "Outcome and Output Management", "amp.indicatormanager:loading": "Loading", "amp.indicatormanager:indicator-name": "Indicator Name", "amp.indicatormanager:indicator-description": "Indicator Description", "amp.indicatormanager:indicator-code": "Indicator Code", - "amp.indicatormanager:indicator-type": "Indicator Type", "amp.indicatormanager:enable-base": "Enable Base Values Input", "amp.indicatormanager:enable-target": "Enable Target Values Input", "amp.indicatormanager:base-values": "Base Values", @@ -65,7 +65,7 @@ "amp.indicatormanager:cancel": "Cancel", "amp.indicatormanager:creating-indicator": "Creating Indicator", "amp.indicatormanager:updating-indicator": "Updating Indicator", - "amp.indicatormanager:indicator-updated-successfully": "Indicator updated successfully", + "amp.indicatormanager:indicator-updated-successfully": "Indicator updated successfully", "amp.indicatormanager:enter-indicator-name": "Enter Indicator Name", "amp.indicatormanager:enter-indicator-description": "Enter Indicator Description", "amp.indicatormanager:enter-indicator-code": "Enter Indicator Code", @@ -104,5 +104,107 @@ "amp.indicatormanager:no-description-available": "No Description available", "amp.indicatormanager:dd": "dd", "amp.indicatormanager:mm": "mm", - "amp.indicatormanager:yyyy": "yyyy" + "amp.indicatormanager:yyyy": "yyyy", + "amp.outcomeoutput:outcome-name": "Outcome Name", + "amp.outcomeoutput:outcome-description": "Outcome Description", + "amp.outcomeoutput:output-name": "Output Name", + "amp.outcomeoutput:output-description": "Output Description", + "amp.outcomeoutput:linked-outcomes": "Linked Outcomes", + "amp.outcomeoutput:save-outcome": "Save Outcome", + "amp.outcomeoutput:save-output": "Save Output", + "amp.outcomeoutput:cancel": "Cancel", + "amp.outcomeoutput:add-new-outcome": "Add New Outcome", + "amp.outcomeoutput:add-new-output": "Add New Output", + "amp.outcomeoutput:export-csv": "Export CSV", + "amp.outcomeoutput:search-placeholder": "Search Outcomes", + "amp.outcomeoutput:modal-title-outcome": "Add New Outcome", + "amp.outcomeoutput:modal-title-output": "Add New Output", + "amp.outcomeoutput:management-title": "Outcome and Output Management", + "amp.indicatormanager:core-info": "Core Indicator Information", + "amp.indicatormanager:program-sector-info": "Program & Sector Information", + "amp.indicatormanager:calculation-unit-info": "Calculation & Unit Information", + "amp.indicatormanager:disaggregation-responsible-info": "Disaggregation & Responsible Organizations", + "amp.indicatormanager:frequency-ascending-info": "Frequency & Ascending", + "amp.indicatormanager:outcome-output-info": "Outcome & Output", + "amp.indicatormanager:dates-info": "Dates", + "amp.indicatormanager:base-target-info": "Base & Target Values", + "amp.indicatormanager:category": "Category", + "amp.indicatormanager:select-category": "Select Category", + "amp.indicatormanager:select-program-scheme": "Select Program Scheme", + "amp.indicatormanager:select-program": "Select Program", + "amp.indicatormanager:select-sectors": "Select Sectors", + "amp.indicatormanager:select-disaggregation": "Select Disaggregation", + "amp.indicatormanager:select-responsible-organizations": "Select Responsible Organizations", + "amp.indicatormanager:select-ascending": "Select Ascending", + "amp.indicatormanager:select-outcome": "Select Outcome", + "amp.indicatormanager:select-output": "Select Output", + "amp.indicatormanager:creation-date": "Creation Date", + "amp.indicatormanager:base-original-value": "Base Original Value", + "amp.indicatormanager:base-original-value-date": "Base Original Value Date", + "amp.indicatormanager:base-revised-value": "Base Revised Value", + "amp.indicatormanager:base-revised-value-date": "Base Revised Value Date", + "amp.indicatormanager:target-original-value": "Target Original Value", + "amp.indicatormanager:target-original-value-date": "Target Original Value Date", + "amp.indicatormanager:target-revised-value": "Target Revised Value", + "amp.indicatormanager:target-revised-value-date": "Target Revised Value Date", + "amp.indicatormanager:unit-of-measure": "Unit of Measure", + "amp.indicatormanager:classification": "Classification", + "amp.indicatormanager:calculation-method": "Calculation Method", + "amp.indicatormanager:enter-calculation-method": "Enter Calculation Method", + "amp.indicatormanager:responsible-organizations": "Responsible Organizations", + "amp.indicatormanager:frequency": "Frequency", + "amp.indicatormanager:enter-frequency": "Enter Frequency", + "amp.indicatormanager:data": "Data", + "amp.indicatormanager:enter-data": "Enter Data", + "amp.indicatormanager:data-source": "Data Source", + "amp.indicatormanager:enter-data-source": "Enter Data Source", + "amp.indicatormanager:disaggregation": "Disaggregation", + "amp.indicatormanager:relevance-for-climate-change": "Relevance for Climate Change Adaptation", + "amp.indicatormanager:indicator-type": "Indicator Type", + "amp.indicatormanager:link-logframe": "Link to Logframe (Program Scheme)", + "amp.indicatormanager:value-tracking": "Value Tracking", + "amp.indicatormanager:other-considerations": "Other Considerations", + "amp.indicatormanager:categorization-linkage-info": "Categorization and Linkage", + "amp.indicatormanager:output": "Output", + "amp.indicatormanager:outcome": "Outcome", + "amp.indicatormanager:search-code": "Search by Code", + "amp.indicatormanager:search-name": "Search by Name", + "amp.indicatormanager:all-outputs": "All Outputs", + "amp.indicatormanager:all-outcomes": "All Outcomes", + "amp.indicatormanager:all-indicator-types": "All Indicator Types", + "amp.indicatormanager:yes": "Yes", + "amp.indicatormanager:no": "No", + "amp.outcomeoutput:actions": "Actions", + "amp.outcomeoutput:add-output-failed": "Failed to add output.", + "amp.outcomeoutput:error-adding-output": "Error adding output.", + "amp.outcomeoutput:fetch-output-details-failed": "Failed to fetch output details.", + "amp.outcomeoutput:error-fetching-output-details": "Error fetching output details.", + "amp.outcomeoutput:update-output-failed": "Failed to update output.", + "amp.outcomeoutput:error-updating-output": "Error updating output.", + "amp.outcomeoutput:delete-output": "Delete Output", + "amp.outcomeoutput:delete-output-confirm": "Are you sure you want to delete output", + "amp.outcomeoutput:delete-output-warning": "This action cannot be undone.", + "amp.outcomeoutput:error-deleting-output": "Error deleting output.", + "amp.outcomeoutput:delete":"Delete", + "amp.outcomeoutput:indicators-linked": "Indicators are linked to this output.", + "amp.outcomeoutput:proceed-orphan-indicators": "Do you want to proceed and orphan the indicators?", + "amp.outcomeoutput:yes-delete-anyway": "Yes, delete anyway", + "amp.outcomeoutput:cannot-delete-output": "Cannot delete output.", + "amp.outcomeoutput:unexpected-error": "An unexpected error occurred.", + "amp.outcomeoutput:output-management": "Output Management", + "amp.outcomeoutput:back": "Back", + "amp.disaggregationmanager:title": "Disaggregation Manager", + "amp.disaggregationmanager:category": "Disaggregation Category", + "amp.disaggregationmanager:options": "Options", + "amp.disaggregationmanager:actions": "Actions", + "amp.disaggregationmanager:edit": "Edit", + "amp.disaggregationmanager:delete": "Delete", + "amp.disaggregationmanager:no-options": "No options", + "amp.disaggregationmanager:add-option": "Add Option", + "amp.disaggregationmanager:edit-option-title": "Edit Option", + "amp.disaggregationmanager:option-value": "Option Value", + "amp.disaggregationmanager:option-value-placeholder": "Enter option value", + "amp.disaggregationmanager:cancel": "Cancel", + "amp.disaggregationmanager:save": "Save", + "amp.dashboard:disaggregation-management": "Disaggregation Management" } diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/index.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/index.tsx index 65ba46f57ab..57cf9852de3 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/index.tsx +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/index.tsx @@ -6,16 +6,14 @@ import StartUp from './components/StartUp'; import defaultTrnPack from './config/initialTranslations.json'; import './index.css'; import '../../../open-sans.css'; -import InidcatorTable from './components/table/IndicatorTable'; +import IndicatorTable from './components/table/IndicatorTable'; import { store } from './reducers/store'; const AdminIndicatorManagerApp = () => { return ( - - + - ); }; diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/pages/DisaggregationManagerPage.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/pages/DisaggregationManagerPage.tsx new file mode 100644 index 00000000000..f8704051d53 --- /dev/null +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/pages/DisaggregationManagerPage.tsx @@ -0,0 +1,205 @@ +import React, {useEffect, useRef, useState} from 'react'; +import { Button, Table, Modal, Form } from 'react-bootstrap'; +import { useSelector, useDispatch } from 'react-redux'; +import {getAmpCategories} from "../reducers/fetchAmpCategoryReducer"; +import initialTranslations from "../config/initialTranslations.json"; +import axios from 'axios'; +import {useNavigate} from "react-router-dom"; +import styles from "../components/modals/css/IndicatorModal.module.css"; + +interface CategoryValue { + id: number; + value: string; + children?: CategoryValue[]; +} +const translations = initialTranslations; + + +const DisaggregationManagerPage: React.FC = () => { + const [categories, setCategories] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(null); + const [showModal, setShowModal] = useState(false); + const [modalMode, setModalMode] = useState<'add' | 'edit'>('add'); + const [editingChild, setEditingChild] = useState(null); + const [optionsMap, setOptionsMap] = useState<{ [key: number]: any[] }>({}); + const dispatch = useDispatch(); + + const categoriesReducer = useSelector((state: any) => state.fetchAmpCategoryReducer); + + useEffect(() => { + dispatch(getAmpCategories()); + }, [dispatch]); + + useEffect(() => { + if (categoriesReducer && categoriesReducer.categories) { + const disaggregationCategories = categoriesReducer.categories.filter((cat: any) => cat.ampCategoryClass.keyName === 'indicator_disaggregation'); + setCategories(disaggregationCategories); + } + }, [categoriesReducer]); + const navigate = useNavigate() + + useEffect(() => { + async function fetchOptions() { + const map: { [key: number]: any[] } = {}; + for (const category of categories) { + try { + const res = await axios.get(`/rest/indicator_disaggregation/options/${category.id}`); + map[category.id] = res.data; + } catch { + map[category.id] = []; + } + } + setOptionsMap(map); + } + if (categories.length > 0) fetchOptions(); + }, [categories]); + + const handleAddChild = (category: CategoryValue) => { + setSelectedCategory(category); + setModalMode('add'); + setShowModal(true); + }; + + const handleEditChild = (category: CategoryValue, child: CategoryValue) => { + setSelectedCategory(category); + setEditingChild(child); + setModalMode('edit'); + setShowModal(true); + }; + + const refreshCategories = () => { + dispatch(getAmpCategories()); + }; + + const handleDeleteChild = async (category: CategoryValue, child: CategoryValue) => { + await fetch(`/rest/indicator_disaggregation/options/${child.id}`, { method: 'DELETE' }); + refreshCategories(); + }; + + const handleClose = () => setShowModal(false); + const nodeRef = useRef(null); + return ( +
+
+

{translations['amp.disaggregationmanager:title'] || 'Disaggregation Manager'}

+ +
+ + + + + + + + + + {categories.map(category => ( + + + + + + ))} + +
{translations['amp.disaggregationmanager:category'] || 'Disaggregation Category'}{translations['amp.disaggregationmanager:options'] || 'Options'}{translations['amp.disaggregationmanager:actions'] || 'Actions'}
{category.value} +
    + {optionsMap[category.id] && optionsMap[category.id].length > 0 ? optionsMap[category.id].map(child => ( +
  • + {child.value} + + +
  • + )) : {translations['amp.disaggregationmanager:no-options'] || 'No options'}} +
+
+ +
+ + + {modalMode === 'add' ? (translations['amp.disaggregationmanager:add-option'] || 'Add Option' + ':' + selectedCategory?.value) : (translations['amp.disaggregationmanager:edit-option-title'] || 'Edit Option' + ':' + selectedCategory?.value)} + +
{ + e.preventDefault(); + const form = e.currentTarget; + const formData = new FormData(form); + const childValue = formData.get('childValue') as string; + + if (!selectedCategory) return; + + if (modalMode === 'add') { + await fetch(`/rest/indicator_disaggregation/options/${selectedCategory.id}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ value: childValue }) + }); + } else if (modalMode === 'edit' && editingChild) { + await fetch(`/rest/indicator_disaggregation/options/${editingChild.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ value: childValue }) + }); + } + setShowModal(false); + setEditingChild(null); + refreshCategories(); + }} + > + + + + {translations['amp.disaggregationmanager:option-value'] || 'Option Value'} + + + + + + + + +
+
+
+ ); +}; + +export default DisaggregationManagerPage; diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/pages/OutcomeOutputManagementPage.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/pages/OutcomeOutputManagementPage.tsx new file mode 100644 index 00000000000..de4375b215e --- /dev/null +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/pages/OutcomeOutputManagementPage.tsx @@ -0,0 +1,493 @@ +import React, { useState, useEffect } from 'react'; +import { Col, Row, Button } from 'react-bootstrap'; +import BootstrapTable, { PaginationOptions } from '@musicstory/react-bootstrap-table-next'; +import '@musicstory/react-bootstrap-table2-filter/dist/react-bootstrap-table2-filter.min.css'; +import styles from '../components/table/Table.module.css'; +import OutcomeModal from '../components/modals/OutcomeModal'; +import OutputModal from '../components/modals/OutputModal'; +import action_style from '../components/table/IndicatorTable.module.css'; +import ToolkitProvider, { Search, CSVExport, ToolkitContextType } from '@murasoftware/react-bootstrap-table2-toolkit'; +import paginationFactory from '@musicstory/react-bootstrap-table2-paginator'; +import initialTranslations from '../config/initialTranslations.json'; +import './css/ModalZIndexFix.css'; // Add z-index to modal and backdrop to ensure visibility +import Swal from 'sweetalert2'; +import {useNavigate} from "react-router-dom"; +import {useSelector, useDispatch} from "react-redux"; +import {getOutcomes} from "../reducers/fetchOutcomesReducer"; + +interface Outcome { + id: number; + name: string; + description?: string; // Optional description for Outcome + outputs: Output[]; +} + +interface Output { + id: number; + name: string; + description?: string; // Optional description for Output +} + +const translations = initialTranslations; + +const OutcomeOutputManagementPage: React.FC = () => { + const [showAddNewOutcomeModal, setShowAddNewOutcomeModal] = useState(false); + const [showEditOutcomeModal, setShowEditOutcomeModal] = useState(false); + const [showOutputModal, setShowOutputModal] = useState(false); + const [editingOutcome, setEditingOutcome] = useState(null); + const [selectedOutcome, setSelectedOutcome] = useState(null); + const [editingOutput, setEditingOutput] = useState(null); + const outcomes = useSelector((state: any) => state.fetchOutcomesReducer).outcomes; + const dispatch = useDispatch(); + + const navigate = useNavigate(); + + // Fetch outcomes on initial mount + useEffect(() => { + dispatch(getOutcomes()); + }, [dispatch]); + + const columns = [ + { + dataField: 'name', + text: 'Outcome Name', + }, + { + dataField: 'actions', + text: 'Actions', + formatter: (_: any, row: Outcome) => ( + <> +
+ handleEditOutcome(row)} + style={{ fontSize: 20, color: '#198754' }} + className="fa fa-pencil" + aria-hidden="true" + /> +
+
+