From 2033e1f187a0d2e971cc7700ae2b6378aa640eb3 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Thu, 21 Aug 2025 22:42:11 +0300 Subject: [PATCH 001/138] AMP-31026: Output / outcome manager AMP-31027: List/View Existing Outcomes and Outputs AMP-31028:Add New Outcome AMP-31029:Add New Output --- .../components/modals/AddNewOutcomeModal.tsx | 72 +++++ .../components/modals/AddNewOutputModal.tsx | 106 +++++++ .../modals/OutcomeOutputManagementModal.tsx | 58 ++++ .../components/table/Table.tsx | 9 + .../config/initialTranslations.json | 1 + .../pages/OutcomeOutputManagementPage.tsx | 263 ++++++++++++++++++ .../reampv2-app/src/routing/routes.js | 9 + .../ampapi/endpoints/reports/ReportsUtil.java | 6 +- 8 files changed, 521 insertions(+), 3 deletions(-) create mode 100644 amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutcomeModal.tsx create mode 100644 amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutputModal.tsx create mode 100644 amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeOutputManagementModal.tsx create mode 100644 amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/pages/OutcomeOutputManagementPage.tsx diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutcomeModal.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutcomeModal.tsx new file mode 100644 index 00000000000..eff62adfac4 --- /dev/null +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutcomeModal.tsx @@ -0,0 +1,72 @@ +import React, { useState } from 'react'; +import { Modal, Button, Form } from 'react-bootstrap'; + +interface AddNewOutcomeModalProps { + show: boolean; + setShow: (show: boolean) => void; + onSubmit?: (outcome: { name: string; description?: string }) => void; + initialName?: string; + initialDescription?: string; +} + +const AddNewOutcomeModal: React.FC = ({ show, setShow, onSubmit, initialName = '', initialDescription = '' }) => { + const [name, setName] = useState(initialName); + const [description, setDescription] = useState(initialDescription); + + React.useEffect(() => { + setName(initialName); + setDescription(initialDescription); + }, [initialName, initialDescription, show]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (onSubmit) { + onSubmit({ name, description }); + } + setShow(false); + setName(''); + setDescription(''); + }; + + return ( + setShow(false)}> + + Add New Outcome + +
+ + + Outcome Name + setName(e.target.value)} + required + placeholder="Enter outcome name" + /> + + + Outcome Description (Optional) + setDescription(e.target.value)} + placeholder="Enter outcome description" + /> + + + + + + +
+
+ ); +}; + +export default AddNewOutcomeModal; diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutputModal.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutputModal.tsx new file mode 100644 index 00000000000..82c321bf939 --- /dev/null +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutputModal.tsx @@ -0,0 +1,106 @@ +import React, { useState } from 'react'; +import { Modal, Button, Form } from 'react-bootstrap'; + +interface Outcome { + id: number; + name: string; +} + +interface AddNewOutputModalProps { + show: boolean; + setShow: (show: boolean) => void; + outcomes: Outcome[]; + onSubmit?: (output: { name: string; description?: string; outcomeIds: number[] }) => void; + initialName?: string; + initialDescription?: string; + initialOutcomeIds?: number[]; +} + +const AddNewOutputModal: React.FC = ({ show, setShow, outcomes, onSubmit, initialName = '', initialDescription = '', initialOutcomeIds = [] }) => { + const [name, setName] = useState(initialName); + const [description, setDescription] = useState(initialDescription); + const [selectedOutcomes, setSelectedOutcomes] = useState(initialOutcomeIds); + + React.useEffect(() => { + setName(initialName); + setDescription(initialDescription); + setSelectedOutcomes(initialOutcomeIds); + }, [initialName, initialDescription, initialOutcomeIds, show]); + + const handleCheckboxChange = (id: number) => { + setSelectedOutcomes(prev => + prev.includes(id) ? prev.filter(oid => oid !== id) : [...prev, id] + ); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (onSubmit) { + onSubmit({ name, description, outcomeIds: selectedOutcomes }); + } + setShow(false); + setName(''); + setDescription(''); + setSelectedOutcomes([]); + }; + + return ( + setShow(false)}> + + Add New Output + +
+ + + Output Name + setName(e.target.value)} + required + placeholder="Enter output name" + /> + + + Output Description (Optional) + setDescription(e.target.value)} + placeholder="Enter output description" + /> + + + Link to Parent Outcome(s) +
+ {outcomes.length === 0 ? ( +
No outcomes available
+ ) : ( + outcomes.map(outcome => ( + handleCheckboxChange(outcome.id)} + /> + )) + )} +
+
+
+ + + + +
+
+ ); +}; + +export default AddNewOutputModal; diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeOutputManagementModal.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeOutputManagementModal.tsx new file mode 100644 index 00000000000..abd9921f3c5 --- /dev/null +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeOutputManagementModal.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Modal, Button } from 'react-bootstrap'; + +interface Outcome { + id: number; + name: string; + outputs: Output[]; +} + +interface Output { + id: number; + name: string; +} + +interface OutcomeOutputManagementModalProps { + show: boolean; + setShow: (show: boolean) => void; + outcomes?: Outcome[]; + translations: any; +} + +const OutcomeOutputManagementModal: React.FC = ({ show, setShow, outcomes = [], translations }) => { + return ( + setShow(false)} size="lg"> + + {translations['amp.dashboard:outcome-output-management'] || 'Outcome and Output Management'} + + + {outcomes.length === 0 ? ( +
{translations['amp.indicatormanager:no-data'] || 'No data available'}
+ ) : ( +
    + {outcomes.map(outcome => ( +
  • + {outcome.name} + {outcome.outputs.length > 0 && ( +
      + {outcome.outputs.map(output => ( +
    • {output.name}
    • + ))} +
    + )} +
  • + ))} +
+ )} +
+ + + +
+ ); +}; + +export default OutcomeOutputManagementModal; + 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..9384251e01b 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; @@ -206,6 +207,8 @@ const SkeletonTable: React.FC = (props) => { } }; + const navigate = useNavigate(); + return ( <> = (props) => { {' '} {translations['amp.dashboard:add-new']} + {' '} + { + const [showAddNewOutcomeModal, setShowAddNewOutcomeModal] = useState(false); + const [showEditOutcomeModal, setShowEditOutcomeModal] = useState(false); + const [showAddNewOutputModal, setShowAddNewOutputModal] = useState(false); + const [showEditOutputModal, setShowEditOutputModal] = useState(false); + const [editingOutcome, setEditingOutcome] = useState(null); + const [editingOutput, setEditingOutput] = useState(null); + + + const columns = [ + { + dataField: 'name', + text: 'Outcome Name', + }, + { + dataField: 'actions', + text: 'Actions', + formatter: (_: any, row: Outcome) => ( + <> + + {' '} + + + ), + headerStyle: { width: '160px' }, + align: 'center', + }, + ]; + + const handleAddOutcome = (outcome: { name: string; description?: string }) => { + // TODO: Add logic to save outcome + // For now, just log + console.log('New Outcome:', outcome); + }; + + const handleAddOutput = (output: { name: string; description?: string; outcomeIds: number[] }) => { + if (!output.outcomeIds || output.outcomeIds.length === 0) { + alert('You must associate the output with at least one outcome.'); + return; + } + // TODO: Add logic to save output + // For now, just log + console.log('New Output:', output); + }; + + const handleEditOutcome = (outcome: Outcome) => { + setEditingOutcome(outcome); + setShowEditOutcomeModal(true); + }; + + const handleSaveEditedOutcome = (updated: { name: string; description?: string }) => { + // TODO: Update outcome in state/backend + console.log('Edited Outcome:', { ...editingOutcome, ...updated }); + setShowEditOutcomeModal(false); + setEditingOutcome(null); + }; + + const handleEditOutput = (output: Output, parentOutcomeIds: number[]) => { + setEditingOutput({ ...output, outcomeIds: parentOutcomeIds }); + setShowEditOutputModal(true); + }; + + const handleSaveEditedOutput = (updated: { name: string; description?: string; outcomeIds: number[] }) => { + if (!updated.outcomeIds || updated.outcomeIds.length === 0) { + alert('You must associate the output with at least one outcome.'); + return; + } + // TODO: Update output in state/backend + console.log('Edited Output:', { ...editingOutput, ...updated }); + setShowEditOutputModal(false); + setEditingOutput(null); + }; + + const handleDeleteOutcome = (outcome: Outcome) => { + // TODO: Implement delete logic (e.g., update state or call backend) + if (window.confirm(`Are you sure you want to delete outcome: ${outcome.name}?`)) { + console.log('Delete Outcome:', outcome); + // Implement actual delete logic here + } + }; + + // For linking outputs, pass only id and name of outcomes + const outcomeOptions = dummyOutcomes.map(o => ({ id: o.id, name: o.name })); + + const expandRow = { + renderer: (row: Outcome) => ( +
+
+ {row.name} + +
+ {row.description && ( +
+ {row.description} +
+ )} + Outputs: +
    + {row.outputs && row.outputs.length > 0 ? row.outputs.map((output: Output) => ( +
  • +
    + {output.name} + + +
    + {output.description && ( +
    + {output.description} +
    + )} +
  • + )) :
  • No outputs
  • } +
+
+ ), + showExpandColumn: true, + expandByColumnOnly: true, + }; + + return ( + + + + + + + +

Outcome and Output Management

+ + +
+ +
+ + +
+ + {' '} + + {' '} + +
+ +
+
+ + + ); +}; + +export default OutcomeOutputManagementPage; diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/routing/routes.js b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/routing/routes.js index 3b0dc0623d8..fa67b673ee9 100755 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/routing/routes.js +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/routing/routes.js @@ -9,6 +9,7 @@ const NewReportApp = lazy(() => import('../modules/new_report')); const GeocoderApp = lazy(() => import('../modules/geocoder')); const AmpOfflineApp = lazy(() => import('../modules/ampoffline/Download')); const ReportGeneratorApp = lazy(() => import('../modules/report_generator')); +const OutcomeOutputManagementPage = lazy(() => import('../modules/admin/indicator_manager/pages/OutcomeOutputManagementPage')); /** @type {import('react-router-dom').RouteObject[]} */ const routes = [ @@ -76,6 +77,14 @@ const routes = [ ) + }, + { + path: "admin/outcome-output-management", + element: ( + Loading...}> + + + ) } ] } diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/reports/ReportsUtil.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/reports/ReportsUtil.java index 52a3c0bfe5b..2a691b55052 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/reports/ReportsUtil.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/reports/ReportsUtil.java @@ -448,7 +448,7 @@ private static void addHierarchies(ReportSpecification spec, ReportFormParameter for (String columnName : hierarchies) { ReportColumn column = new ReportColumn(columnName); if (!spec.getHierarchies().contains(column)) { - //add as a column if not present + //add as a column if not present if (!existingColumns.contains(column)) { existingColumns.add(spec.getHierarchies().size(), column); } @@ -680,7 +680,7 @@ public static boolean configureSorting(ReportSpecificationImpl spec, ReportFormP if (asc == null) { errors.add("sorting order is not specified, asc = null"); } - if (errors.size() > 0) { + if (!errors.isEmpty()) { logger.error("Ignoring invalid sorting request: " + errors); } else { int rootType = isTotals ? SortingInfo.ROOT_PATH_TOTALS : (isFunding @@ -923,7 +923,7 @@ public static String exportToMap(final ReportConfig config, final Long reportId) * @return JsonBean with saved Api state */ public static AmpApiState getApiState(String reportConfigId) { - // TODO: can we safely remove it from session afterwards? + // TODO: can we safely remove it from session afterwards? return (AmpApiState) TLSUtils.getRequest().getSession() .getAttribute(EPConstants.API_STATE_REPORT_EXPORT + reportConfigId); } From 34709754fe758a168c2deb123cf14bc15aa53626 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Thu, 21 Aug 2025 23:28:00 +0300 Subject: [PATCH 002/138] AMP-31026: Output / outcome manager AMP-31027: List/View Existing Outcomes and Outputs AMP-31028:Add New Outcome AMP-31029:Add New Output --- ...ddNewOutcomeModal.tsx => OutcomeModal.tsx} | 4 +- .../modals/OutcomeOutputManagementModal.tsx | 58 -------- ...{AddNewOutputModal.tsx => OutputModal.tsx} | 4 +- .../pages/OutcomeOutputManagementPage.tsx | 132 +++++++++++------ .../manager/AmpOutcomeOutputEndpoint.java | 88 ++++++++++++ .../indicator/manager/dto/AmpOutcomeDTO.java | 20 +++ .../indicator/manager/dto/AmpOutputDTO.java | 20 +++ .../service/AmpOutcomeOutputService.java | 134 ++++++++++++++++++ .../module/aim/dbentity/AmpOutcome.java | 23 +++ .../module/aim/dbentity/AmpOutput.java | 23 +++ .../module/aim/dbentity/AmpOutcome.hbm.xml | 19 +++ .../module/aim/dbentity/AmpOutput.hbm.xml | 19 +++ .../moduleConfig/aim/module-config.xml | 2 + 13 files changed, 440 insertions(+), 106 deletions(-) rename amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/{AddNewOutcomeModal.tsx => OutcomeModal.tsx} (92%) delete mode 100644 amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeOutputManagementModal.tsx rename amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/{AddNewOutputModal.tsx => OutputModal.tsx} (94%) create mode 100644 amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOutcomeOutputEndpoint.java create mode 100644 amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/dto/AmpOutcomeDTO.java create mode 100644 amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/dto/AmpOutputDTO.java create mode 100644 amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/service/AmpOutcomeOutputService.java create mode 100644 amp/src/main/java/org/digijava/module/aim/dbentity/AmpOutcome.java create mode 100644 amp/src/main/java/org/digijava/module/aim/dbentity/AmpOutput.java create mode 100644 amp/src/main/resources/org/digijava/module/aim/dbentity/AmpOutcome.hbm.xml create mode 100644 amp/src/main/resources/org/digijava/module/aim/dbentity/AmpOutput.hbm.xml diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutcomeModal.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeModal.tsx similarity index 92% rename from amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutcomeModal.tsx rename to amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeModal.tsx index eff62adfac4..8f63de46040 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutcomeModal.tsx +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeModal.tsx @@ -9,7 +9,7 @@ interface AddNewOutcomeModalProps { initialDescription?: string; } -const AddNewOutcomeModal: React.FC = ({ show, setShow, onSubmit, initialName = '', initialDescription = '' }) => { +const OutcomeModal: React.FC = ({ show, setShow, onSubmit, initialName = '', initialDescription = '' }) => { const [name, setName] = useState(initialName); const [description, setDescription] = useState(initialDescription); @@ -69,4 +69,4 @@ const AddNewOutcomeModal: React.FC = ({ show, setShow, ); }; -export default AddNewOutcomeModal; +export default OutcomeModal; diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeOutputManagementModal.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeOutputManagementModal.tsx deleted file mode 100644 index abd9921f3c5..00000000000 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutcomeOutputManagementModal.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import { Modal, Button } from 'react-bootstrap'; - -interface Outcome { - id: number; - name: string; - outputs: Output[]; -} - -interface Output { - id: number; - name: string; -} - -interface OutcomeOutputManagementModalProps { - show: boolean; - setShow: (show: boolean) => void; - outcomes?: Outcome[]; - translations: any; -} - -const OutcomeOutputManagementModal: React.FC = ({ show, setShow, outcomes = [], translations }) => { - return ( - setShow(false)} size="lg"> - - {translations['amp.dashboard:outcome-output-management'] || 'Outcome and Output Management'} - - - {outcomes.length === 0 ? ( -
{translations['amp.indicatormanager:no-data'] || 'No data available'}
- ) : ( -
    - {outcomes.map(outcome => ( -
  • - {outcome.name} - {outcome.outputs.length > 0 && ( -
      - {outcome.outputs.map(output => ( -
    • {output.name}
    • - ))} -
    - )} -
  • - ))} -
- )} -
- - - -
- ); -}; - -export default OutcomeOutputManagementModal; - diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutputModal.tsx b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutputModal.tsx similarity index 94% rename from amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutputModal.tsx rename to amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutputModal.tsx index 82c321bf939..4721929efef 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/AddNewOutputModal.tsx +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/components/modals/OutputModal.tsx @@ -16,7 +16,7 @@ interface AddNewOutputModalProps { initialOutcomeIds?: number[]; } -const AddNewOutputModal: React.FC = ({ show, setShow, outcomes, onSubmit, initialName = '', initialDescription = '', initialOutcomeIds = [] }) => { +const OutputModal: React.FC = ({ show, setShow, outcomes, onSubmit, initialName = '', initialDescription = '', initialOutcomeIds = [] }) => { const [name, setName] = useState(initialName); const [description, setDescription] = useState(initialDescription); const [selectedOutcomes, setSelectedOutcomes] = useState(initialOutcomeIds); @@ -103,4 +103,4 @@ const AddNewOutputModal: React.FC = ({ show, setShow, ou ); }; -export default AddNewOutputModal; +export default OutputModal; 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 index c354e14ece9..7b1ec726e0f 100644 --- 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 @@ -1,10 +1,10 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Col, Row, Button } from 'react-bootstrap'; import BootstrapTable from 'react-bootstrap-table-next'; import 'react-bootstrap-table-next/dist/react-bootstrap-table2.min.css'; import styles from '../components/table/Table.module.css'; -import AddNewOutcomeModal from '../components/modals/AddNewOutcomeModal'; -import AddNewOutputModal from '../components/modals/AddNewOutputModal'; +import OutcomeModal from '../components/modals/OutcomeModal'; +import OutputModal from '../components/modals/OutputModal'; interface Outcome { id: number; @@ -19,27 +19,6 @@ interface Output { description?: string; // Optional description for Output } -const dummyOutcomes: Outcome[] = [ - { - id: 1, - name: 'Outcome 1', - description: 'Description for Outcome 1', - outputs: [ - { id: 101, name: 'Output A', description: 'Description for Output A' }, - { id: 102, name: 'Output B' } - ] - }, - { - id: 2, - name: 'Outcome 2', - outputs: [ - { id: 201, name: 'Output C' } - ] - } -]; - - - const OutcomeOutputManagementPage: React.FC = () => { const [showAddNewOutcomeModal, setShowAddNewOutcomeModal] = useState(false); const [showEditOutcomeModal, setShowEditOutcomeModal] = useState(false); @@ -47,7 +26,13 @@ const OutcomeOutputManagementPage: React.FC = () => { const [showEditOutputModal, setShowEditOutputModal] = useState(false); const [editingOutcome, setEditingOutcome] = useState(null); const [editingOutput, setEditingOutput] = useState(null); + const [outcomes, setOutcomes] = useState([]); + useEffect(() => { + fetch('/amp-outcome-output/outcomes') + .then(res => res.json()) + .then(data => setOutcomes(data)); + }, []); const columns = [ { @@ -81,20 +66,48 @@ const OutcomeOutputManagementPage: React.FC = () => { }, ]; - const handleAddOutcome = (outcome: { name: string; description?: string }) => { - // TODO: Add logic to save outcome - // For now, just log - console.log('New Outcome:', outcome); + const handleAddOutcome = async (outcome: { name: string; description?: string }) => { + try { + const res = await fetch('/amp-outcome-output/outcome', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(outcome) + }); + if (res.ok) { + fetch('/amp-outcome-output/outcomes') + .then(res => res.json()) + .then(data => setOutcomes(data)); + } else { + alert('Failed to add outcome'); + } + } catch (e) { + console.error('Error adding outcome', e); + alert('Error adding outcome'); + } }; - const handleAddOutput = (output: { name: string; description?: string; outcomeIds: number[] }) => { + const handleAddOutput = async (output: { name: string; description?: string; outcomeIds: number[] }) => { if (!output.outcomeIds || output.outcomeIds.length === 0) { alert('You must associate the output with at least one outcome.'); return; } - // TODO: Add logic to save output - // For now, just log - console.log('New Output:', output); + try { + const res = await fetch('/amp-outcome-output/output', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(output) + }); + if (res.ok) { + fetch('/amp-outcome-output/outcomes') + .then(res => res.json()) + .then(data => setOutcomes(data)); + } else { + alert('Failed to add output'); + } + } catch (e) { + console.error('Error adding output', e); + alert('Error adding output'); + } }; const handleEditOutcome = (outcome: Outcome) => { @@ -102,9 +115,25 @@ const OutcomeOutputManagementPage: React.FC = () => { setShowEditOutcomeModal(true); }; - const handleSaveEditedOutcome = (updated: { name: string; description?: string }) => { - // TODO: Update outcome in state/backend - console.log('Edited Outcome:', { ...editingOutcome, ...updated }); + const handleSaveEditedOutcome = async (updated: { name: string; description?: string }) => { + if (!editingOutcome) return; + try { + const res = await fetch(`/amp-outcome-output/outcome/${editingOutcome.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updated) + }); + if (res.ok) { + fetch('/amp-outcome-output/outcomes') + .then(res => res.json()) + .then(data => setOutcomes(data)); + } else { + alert('Failed to update outcome'); + } + } catch (e) { + console.error('Error updating outcome', e); + alert('Error updating outcome'); + } setShowEditOutcomeModal(false); setEditingOutcome(null); }; @@ -114,13 +143,28 @@ const OutcomeOutputManagementPage: React.FC = () => { setShowEditOutputModal(true); }; - const handleSaveEditedOutput = (updated: { name: string; description?: string; outcomeIds: number[] }) => { + const handleSaveEditedOutput = async (updated: { name: string; description?: string; outcomeIds: number[] }) => { + if (!editingOutput) return; if (!updated.outcomeIds || updated.outcomeIds.length === 0) { alert('You must associate the output with at least one outcome.'); return; } - // TODO: Update output in state/backend - console.log('Edited Output:', { ...editingOutput, ...updated }); + try { + const res = await fetch(`/amp-outcome-output/output/${editingOutput.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updated) + }); + if (res.ok) { + fetch('/amp-outcome-output/outcomes') + .then(res => res.json()) + .then(data => setOutcomes(data)); + } else { + alert('Failed to update output'); + } + } catch (e) { + alert('Error updating output'); + } setShowEditOutputModal(false); setEditingOutput(null); }; @@ -134,7 +178,7 @@ const OutcomeOutputManagementPage: React.FC = () => { }; // For linking outputs, pass only id and name of outcomes - const outcomeOptions = dummyOutcomes.map(o => ({ id: o.id, name: o.name })); + const outcomeOptions = outcomes.map(o => ({ id: o.id, name: o.name })); const expandRow = { renderer: (row: Outcome) => ( @@ -194,25 +238,25 @@ const OutcomeOutputManagementPage: React.FC = () => { return ( - - - - {
getAllOutcomes() { + return service.getAllOutcomes(); + } + + @GET + @Path("/outputs") + public List getAllOutputs() { + return service.getAllOutputs(); + } + + @POST + @Path("/outcome") + public Response createOutcome(AmpOutcomeDTO dto) { + AmpOutcomeDTO created = service.createOutcome(dto); + return Response.ok(created).build(); + } + + @POST + @Path("/output") + public Response createOutput(AmpOutputDTO dto) { + if (dto.getOutcomeIds() == null || dto.getOutcomeIds().isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST).entity("Output must be linked to at least one Outcome.").build(); + } + AmpOutputDTO created = service.createOutput(dto); + return Response.ok(created).build(); + } + + @PUT + @Path("/outcome/{id}") + public Response updateOutcome(@PathParam("id") Long id, AmpOutcomeDTO dto) { + AmpOutcomeDTO updated = service.updateOutcome(id, dto); + if (updated == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(updated).build(); + } + + @PUT + @Path("/output/{id}") + public Response updateOutput(@PathParam("id") Long id, AmpOutputDTO dto) { + if (dto.getOutcomeIds() == null || dto.getOutcomeIds().isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST).entity("Output must be linked to at least one Outcome.").build(); + } + AmpOutputDTO updated = service.updateOutput(id, dto); + if (updated == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(updated).build(); + } + + @DELETE + @Path("/outcome/{id}") + public Response deleteOutcome(@PathParam("id") Long id) { + boolean deleted = service.deleteOutcome(id); + if (!deleted) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok().build(); + } + + @DELETE + @Path("/output/{id}") + public Response deleteOutput(@PathParam("id") Long id) { + boolean deleted = service.deleteOutput(id); + if (!deleted) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok().build(); + } +} diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/dto/AmpOutcomeDTO.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/dto/AmpOutcomeDTO.java new file mode 100644 index 00000000000..972a81cbcf8 --- /dev/null +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/dto/AmpOutcomeDTO.java @@ -0,0 +1,20 @@ +package org.digijava.kernel.ampapi.endpoints.indicator.manager.dto; + +import java.util.List; + +public class AmpOutcomeDTO { + private Long id; + private String name; + private String description; + private List outputIds; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public List getOutputIds() { return outputIds; } + public void setOutputIds(List outputIds) { this.outputIds = outputIds; } +} + diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/dto/AmpOutputDTO.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/dto/AmpOutputDTO.java new file mode 100644 index 00000000000..7b37dfd81fd --- /dev/null +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/dto/AmpOutputDTO.java @@ -0,0 +1,20 @@ +package org.digijava.kernel.ampapi.endpoints.indicator.manager.dto; + +import java.util.List; + +public class AmpOutputDTO { + private Long id; + private String name; + private String description; + private List outcomeIds; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public List getOutcomeIds() { return outcomeIds; } + public void setOutcomeIds(List outcomeIds) { this.outcomeIds = outcomeIds; } +} + diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/service/AmpOutcomeOutputService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/service/AmpOutcomeOutputService.java new file mode 100644 index 00000000000..f451b8752df --- /dev/null +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/service/AmpOutcomeOutputService.java @@ -0,0 +1,134 @@ +package org.digijava.kernel.ampapi.endpoints.indicator.manager.service; + +import org.digijava.module.aim.dbentity.AmpOutcome; +import org.digijava.module.aim.dbentity.AmpOutput; +import org.digijava.kernel.ampapi.endpoints.indicator.manager.dto.AmpOutcomeDTO; +import org.digijava.kernel.ampapi.endpoints.indicator.manager.dto.AmpOutputDTO; +import org.digijava.kernel.persistence.PersistenceManager; +import org.hibernate.Session; +import org.hibernate.Transaction; +import java.util.*; +import java.util.stream.Collectors; + +public class AmpOutcomeOutputService { + public List getAllOutcomes() { + Session session = PersistenceManager.getSession(); + List outcomes = session.createQuery("from AmpOutcome", AmpOutcome.class).list(); + return outcomes.stream().map(this::toOutcomeDTO).collect(Collectors.toList()); + } + + public List getAllOutputs() { + Session session = PersistenceManager.getSession(); + List outputs = session.createQuery("from AmpOutput", AmpOutput.class).list(); + return outputs.stream().map(this::toOutputDTO).collect(Collectors.toList()); + } + + public AmpOutcomeDTO createOutcome(AmpOutcomeDTO dto) { + Session session = PersistenceManager.getSession(); + Transaction tx = session.beginTransaction(); + AmpOutcome outcome = new AmpOutcome(); + outcome.setName(dto.getName()); + outcome.setDescription(dto.getDescription()); + session.save(outcome); + tx.commit(); + return toOutcomeDTO(outcome); + } + + public AmpOutputDTO createOutput(AmpOutputDTO dto) { + Session session = PersistenceManager.getSession(); + Transaction tx = session.beginTransaction(); + AmpOutput output = new AmpOutput(); + output.setName(dto.getName()); + output.setDescription(dto.getDescription()); + Set outcomes = new HashSet<>(); + if (dto.getOutcomeIds() != null) { + for (Long id : dto.getOutcomeIds()) { + AmpOutcome outcome = session.get(AmpOutcome.class, id); + if (outcome != null) { + outcomes.add(outcome); + } + } + } + output.setOutcomes(outcomes); + session.save(output); + tx.commit(); + return toOutputDTO(output); + } + + public AmpOutcomeDTO updateOutcome(Long id, AmpOutcomeDTO dto) { + Session session = PersistenceManager.getSession(); + Transaction tx = session.beginTransaction(); + AmpOutcome outcome = session.get(AmpOutcome.class, id); + if (outcome == null) return null; + outcome.setName(dto.getName()); + outcome.setDescription(dto.getDescription()); + session.update(outcome); + tx.commit(); + return toOutcomeDTO(outcome); + } + + public AmpOutputDTO updateOutput(Long id, AmpOutputDTO dto) { + Session session = PersistenceManager.getSession(); + Transaction tx = session.beginTransaction(); + AmpOutput output = session.get(AmpOutput.class, id); + if (output == null) return null; + output.setName(dto.getName()); + output.setDescription(dto.getDescription()); + Set outcomes = new HashSet<>(); + if (dto.getOutcomeIds() != null) { + for (Long oid : dto.getOutcomeIds()) { + AmpOutcome outcome = session.get(AmpOutcome.class, oid); + if (outcome != null) { + outcomes.add(outcome); + } + } + } + output.setOutcomes(outcomes); + session.update(output); + tx.commit(); + return toOutputDTO(output); + } + + public boolean deleteOutcome(Long id) { + Session session = PersistenceManager.getSession(); + Transaction tx = session.beginTransaction(); + AmpOutcome outcome = session.get(AmpOutcome.class, id); + if (outcome == null) return false; + session.delete(outcome); + tx.commit(); + return true; + } + + public boolean deleteOutput(Long id) { + Session session = PersistenceManager.getSession(); + Transaction tx = session.beginTransaction(); + AmpOutput output = session.get(AmpOutput.class, id); + if (output == null) return false; + session.delete(output); + tx.commit(); + return true; + } + + private AmpOutcomeDTO toOutcomeDTO(AmpOutcome outcome) { + AmpOutcomeDTO dto = new AmpOutcomeDTO(); + dto.setId(outcome.getId()); + dto.setName(outcome.getName()); + dto.setDescription(outcome.getDescription()); + if (outcome.getOutputs() != null) { + dto.setOutputIds(outcome.getOutputs().stream().map(AmpOutput::getId).collect(Collectors.toList())); + } + return dto; + } + + private AmpOutputDTO toOutputDTO(AmpOutput output) { + AmpOutputDTO dto = new AmpOutputDTO(); + dto.setId(output.getId()); + dto.setName(output.getName()); + dto.setDescription(output.getDescription()); + if (output.getOutcomes() != null) { + dto.setOutcomeIds(output.getOutcomes().stream().map(AmpOutcome::getId).collect(Collectors.toList())); + } + return dto; + } +} + diff --git a/amp/src/main/java/org/digijava/module/aim/dbentity/AmpOutcome.java b/amp/src/main/java/org/digijava/module/aim/dbentity/AmpOutcome.java new file mode 100644 index 00000000000..80e938c9c37 --- /dev/null +++ b/amp/src/main/java/org/digijava/module/aim/dbentity/AmpOutcome.java @@ -0,0 +1,23 @@ +package org.digijava.module.aim.dbentity; + +import java.io.Serializable; +import java.util.Set; + +public class AmpOutcome implements Serializable { + private Long id; + private String name; + private String description; + private Set outputs; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public Set getOutputs() { return outputs; } + public void setOutputs(Set outputs) { this.outputs = outputs; } +} diff --git a/amp/src/main/java/org/digijava/module/aim/dbentity/AmpOutput.java b/amp/src/main/java/org/digijava/module/aim/dbentity/AmpOutput.java new file mode 100644 index 00000000000..e84a143f6b5 --- /dev/null +++ b/amp/src/main/java/org/digijava/module/aim/dbentity/AmpOutput.java @@ -0,0 +1,23 @@ +package org.digijava.module.aim.dbentity; + +import java.io.Serializable; +import java.util.Set; + +public class AmpOutput implements Serializable { + private Long id; + private String name; + private String description; + private Set outcomes; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public Set getOutcomes() { return outcomes; } + public void setOutcomes(Set outcomes) { this.outcomes = outcomes; } +} diff --git a/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpOutcome.hbm.xml b/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpOutcome.hbm.xml new file mode 100644 index 00000000000..bb147cc47f1 --- /dev/null +++ b/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpOutcome.hbm.xml @@ -0,0 +1,19 @@ + + + + + + + AMP_OUTCOME_seq + + + + + + + + + + + diff --git a/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpOutput.hbm.xml b/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpOutput.hbm.xml new file mode 100644 index 00000000000..19e85e405ff --- /dev/null +++ b/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpOutput.hbm.xml @@ -0,0 +1,19 @@ + + + + + + + AMP_OUTPUT_seq + + + + + + + + + + + diff --git a/amp/src/main/webapp/WEB-INF/moduleConfig/aim/module-config.xml b/amp/src/main/webapp/WEB-INF/moduleConfig/aim/module-config.xml index 1a182e40e20..688a7d5d169 100644 --- a/amp/src/main/webapp/WEB-INF/moduleConfig/aim/module-config.xml +++ b/amp/src/main/webapp/WEB-INF/moduleConfig/aim/module-config.xml @@ -279,6 +279,8 @@ org.digijava.module.aim.action.dataimporter.dbentity.DataImporterConfig org.digijava.module.aim.action.dataimporter.dbentity.DataImporterConfigValues org.digijava.module.aim.action.dataimporter.dbentity.ImportedProjectCurrency + org.digijava.module.aim.dbentity.AmpOutput + org.digijava.module.aim.dbentity.AmpOutcome From 6fdc6a5e7a66c544ac10794904b218c1f78739fc Mon Sep 17 00:00:00 2001 From: brianbrix Date: Thu, 21 Aug 2025 23:45:58 +0300 Subject: [PATCH 003/138] AMP-31026: Output / outcome manager AMP-31027: List/View Existing Outcomes and Outputs AMP-31028:Add New Outcome AMP-31029:Add New Output --- .../indicator_manager/pages/OutcomeOutputManagementPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 7b1ec726e0f..4ea269c2fe0 100644 --- 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 @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { Col, Row, Button } from 'react-bootstrap'; import BootstrapTable from 'react-bootstrap-table-next'; -import 'react-bootstrap-table-next/dist/react-bootstrap-table2.min.css'; +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'; From 5b950400c561e5d57b94b925c755134d2ee810f4 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Thu, 21 Aug 2025 23:54:04 +0300 Subject: [PATCH 004/138] AMP-31026: Output / outcome manager AMP-31027: List/View Existing Outcomes and Outputs AMP-31028:Add New Outcome AMP-31029:Add New Output --- .../pages/OutcomeOutputManagementPage.tsx | 158 +++++++++++------- 1 file changed, 95 insertions(+), 63 deletions(-) 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 index 4ea269c2fe0..a892279dfee 100644 --- 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 @@ -5,6 +5,8 @@ import '@musicstory/react-bootstrap-table2-filter/dist/react-bootstrap-table2-fi import styles from '../components/table/Table.module.css'; import OutcomeModal from '../components/modals/OutcomeModal'; import OutputModal from '../components/modals/OutputModal'; +import ToolkitProvider, { Search, CSVExport, ToolkitContextType } from '@murasoftware/react-bootstrap-table2-toolkit'; +import paginationFactory from '@musicstory/react-bootstrap-table2-paginator'; interface Outcome { id: number; @@ -236,71 +238,101 @@ const OutcomeOutputManagementPage: React.FC = () => { expandByColumnOnly: true, }; + const { SearchBar } = Search; + const { ExportCSVButton } = CSVExport; + return ( - - - - - - - -

Outcome and Output Management

- - + + {(props: ToolkitContextType) => ( +
+ + + + + + +

Outcome and Output Management

+ + +
+ +
+ + +
+ + {' '} + + {' '} + + Export CSV + +
+ + +
+
+ +
+
+ +

- - - - -
- - {' '} - - {' '} - -
- -
-
- - + +
+ )} +
); }; From 8ac983a1c2171c69ed3ac25f440c4852e1532eba Mon Sep 17 00:00:00 2001 From: brianbrix Date: Fri, 22 Aug 2025 00:02:34 +0300 Subject: [PATCH 005/138] AMP-31026: Output / outcome manager AMP-31027: List/View Existing Outcomes and Outputs AMP-31028:Add New Outcome AMP-31029:Add New Output --- .../indicator_manager/pages/OutcomeOutputManagementPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a892279dfee..5600417af34 100644 --- 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 @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { Col, Row, Button } from 'react-bootstrap'; -import BootstrapTable from 'react-bootstrap-table-next'; +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'; From 90fa750011f6347305de85299bbe501141a7d0b6 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Fri, 22 Aug 2025 06:35:00 +0300 Subject: [PATCH 006/138] AMP-31026: Output / outcome manager AMP-31027: List/View Existing Outcomes and Outputs AMP-31028:Add New Outcome AMP-31029:Add New Output --- .../pages/OutcomeOutputManagementPage.tsx | 14 +++++------ ...Endpoint.java => AmpOucComeEndpoints.java} | 24 ++++++++++++++++++- 2 files changed, 30 insertions(+), 8 deletions(-) rename amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/{AmpOutcomeOutputEndpoint.java => AmpOucComeEndpoints.java} (67%) 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 index 5600417af34..a1f53a3e5ab 100644 --- 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 @@ -70,13 +70,13 @@ const OutcomeOutputManagementPage: React.FC = () => { const handleAddOutcome = async (outcome: { name: string; description?: string }) => { try { - const res = await fetch('/amp-outcome-output/outcome', { + const res = await fetch('/rest/amp-outcome-output/outcome', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(outcome) }); if (res.ok) { - fetch('/amp-outcome-output/outcomes') + fetch('/rest/amp-outcome-output/outcomes') .then(res => res.json()) .then(data => setOutcomes(data)); } else { @@ -94,7 +94,7 @@ const OutcomeOutputManagementPage: React.FC = () => { return; } try { - const res = await fetch('/amp-outcome-output/output', { + const res = await fetch('/rest/amp-outcome-output/output', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(output) @@ -120,13 +120,13 @@ const OutcomeOutputManagementPage: React.FC = () => { const handleSaveEditedOutcome = async (updated: { name: string; description?: string }) => { if (!editingOutcome) return; try { - const res = await fetch(`/amp-outcome-output/outcome/${editingOutcome.id}`, { + const res = await fetch(`/rest/amp-outcome-output/outcome/${editingOutcome.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updated) }); if (res.ok) { - fetch('/amp-outcome-output/outcomes') + fetch('/rest/amp-outcome-output/outcomes') .then(res => res.json()) .then(data => setOutcomes(data)); } else { @@ -152,13 +152,13 @@ const OutcomeOutputManagementPage: React.FC = () => { return; } try { - const res = await fetch(`/amp-outcome-output/output/${editingOutput.id}`, { + const res = await fetch(`/rest/amp-outcome-output/output/${editingOutput.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updated) }); if (res.ok) { - fetch('/amp-outcome-output/outcomes') + fetch('/rest/amp-outcome-output/outcomes') .then(res => res.json()) .then(data => setOutcomes(data)); } else { diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOutcomeOutputEndpoint.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOucComeEndpoints.java similarity index 67% rename from amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOutcomeOutputEndpoint.java rename to amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOucComeEndpoints.java index 0f4c3753dbb..c6ce3af0a18 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOutcomeOutputEndpoint.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOucComeEndpoints.java @@ -1,8 +1,13 @@ package org.digijava.kernel.ampapi.endpoints.indicator.manager; +import io.swagger.annotations.Api; +import org.digijava.kernel.ampapi.endpoints.security.AuthRule; +import org.digijava.kernel.ampapi.endpoints.util.ApiMethod; +import io.swagger.annotations.ApiOperation; import org.digijava.kernel.ampapi.endpoints.indicator.manager.dto.AmpOutcomeDTO; import org.digijava.kernel.ampapi.endpoints.indicator.manager.dto.AmpOutputDTO; import org.digijava.kernel.ampapi.endpoints.indicator.manager.service.AmpOutcomeOutputService; + import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -11,23 +16,30 @@ @Path("/amp-outcome-output") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -public class AmpOutcomeOutputEndpoint { +@Api("Amp Outcome Output") +public class AmpOucComeEndpoints { private final AmpOutcomeOutputService service = new AmpOutcomeOutputService(); @GET @Path("/outcomes") + @ApiMethod(authTypes = AuthRule.IN_ADMIN, id = "getAllOutcomes") + @ApiOperation(value = "Get all outcomes", notes = "Returns a list of all outcomes") public List getAllOutcomes() { return service.getAllOutcomes(); } @GET @Path("/outputs") + @ApiMethod(authTypes = AuthRule.IN_ADMIN, id = "getAllOutputs") + @ApiOperation(value = "Get all outputs", notes = "Returns a list of all outputs") public List getAllOutputs() { return service.getAllOutputs(); } @POST @Path("/outcome") + @ApiMethod(authTypes = AuthRule.IN_ADMIN, id = "createOutcome") + @ApiOperation(value = "Create outcome", notes = "Creates a new outcome") public Response createOutcome(AmpOutcomeDTO dto) { AmpOutcomeDTO created = service.createOutcome(dto); return Response.ok(created).build(); @@ -35,6 +47,8 @@ public Response createOutcome(AmpOutcomeDTO dto) { @POST @Path("/output") + @ApiMethod(authTypes = AuthRule.IN_ADMIN, id = "createOutput") + @ApiOperation(value = "Create output", notes = "Creates a new output") public Response createOutput(AmpOutputDTO dto) { if (dto.getOutcomeIds() == null || dto.getOutcomeIds().isEmpty()) { return Response.status(Response.Status.BAD_REQUEST).entity("Output must be linked to at least one Outcome.").build(); @@ -45,6 +59,8 @@ public Response createOutput(AmpOutputDTO dto) { @PUT @Path("/outcome/{id}") + @ApiMethod(authTypes = AuthRule.IN_ADMIN, id = "updateOutcome") + @ApiOperation(value = "Update outcome", notes = "Updates an existing outcome") public Response updateOutcome(@PathParam("id") Long id, AmpOutcomeDTO dto) { AmpOutcomeDTO updated = service.updateOutcome(id, dto); if (updated == null) { @@ -55,6 +71,8 @@ public Response updateOutcome(@PathParam("id") Long id, AmpOutcomeDTO dto) { @PUT @Path("/output/{id}") + @ApiMethod(authTypes = AuthRule.IN_ADMIN, id = "updateOutput") + @ApiOperation(value = "Update output", notes = "Updates an existing output") public Response updateOutput(@PathParam("id") Long id, AmpOutputDTO dto) { if (dto.getOutcomeIds() == null || dto.getOutcomeIds().isEmpty()) { return Response.status(Response.Status.BAD_REQUEST).entity("Output must be linked to at least one Outcome.").build(); @@ -68,6 +86,8 @@ public Response updateOutput(@PathParam("id") Long id, AmpOutputDTO dto) { @DELETE @Path("/outcome/{id}") + @ApiMethod(authTypes = AuthRule.IN_ADMIN, id = "deleteOutcome") + @ApiOperation(value = "Delete outcome", notes = "Deletes an outcome by ID") public Response deleteOutcome(@PathParam("id") Long id) { boolean deleted = service.deleteOutcome(id); if (!deleted) { @@ -78,6 +98,8 @@ public Response deleteOutcome(@PathParam("id") Long id) { @DELETE @Path("/output/{id}") + @ApiMethod(authTypes = AuthRule.IN_ADMIN, id = "deleteOutput") + @ApiOperation(value = "Delete output", notes = "Deletes an output by ID") public Response deleteOutput(@PathParam("id") Long id) { boolean deleted = service.deleteOutput(id); if (!deleted) { From fa05685d1fece7ee8583933817f50b9c4be88332 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Fri, 22 Aug 2025 06:55:13 +0300 Subject: [PATCH 007/138] AMP-31026: Output / outcome manager AMP-31027: List/View Existing Outcomes and Outputs AMP-31028:Add New Outcome AMP-31029:Add New Output --- .../indicator_manager/pages/OutcomeOutputManagementPage.tsx | 4 ++-- ...mpOucComeEndpoints.java => AmpOutcomeOutputEndpoints.java} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/{AmpOucComeEndpoints.java => AmpOutcomeOutputEndpoints.java} (99%) 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 index a1f53a3e5ab..4e13268e923 100644 --- 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 @@ -31,7 +31,7 @@ const OutcomeOutputManagementPage: React.FC = () => { const [outcomes, setOutcomes] = useState([]); useEffect(() => { - fetch('/amp-outcome-output/outcomes') + fetch('/rest/amp-outcome-output/outcomes') .then(res => res.json()) .then(data => setOutcomes(data)); }, []); @@ -100,7 +100,7 @@ const OutcomeOutputManagementPage: React.FC = () => { body: JSON.stringify(output) }); if (res.ok) { - fetch('/amp-outcome-output/outcomes') + fetch('/rest/amp-outcome-output/outcomes') .then(res => res.json()) .then(data => setOutcomes(data)); } else { diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOucComeEndpoints.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOutcomeOutputEndpoints.java similarity index 99% rename from amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOucComeEndpoints.java rename to amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOutcomeOutputEndpoints.java index c6ce3af0a18..eb61dd42407 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOucComeEndpoints.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/AmpOutcomeOutputEndpoints.java @@ -17,7 +17,7 @@ @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Api("Amp Outcome Output") -public class AmpOucComeEndpoints { +public class AmpOutcomeOutputEndpoints { private final AmpOutcomeOutputService service = new AmpOutcomeOutputService(); @GET From 056896698b53cfce8e2b290307f0a73585664db8 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Fri, 22 Aug 2025 07:04:22 +0300 Subject: [PATCH 008/138] AMP-31026: Output / outcome manager AMP-31027: List/View Existing Outcomes and Outputs AMP-31028:Add New Outcome AMP-31029:Add New Output --- .../indicator/manager/dto/AmpOutcomeDTO.java | 19 ++++++++++++++++--- .../service/AmpOutcomeOutputService.java | 1 + 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/dto/AmpOutcomeDTO.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/dto/AmpOutcomeDTO.java index 972a81cbcf8..00e6925f380 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/dto/AmpOutcomeDTO.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/dto/AmpOutcomeDTO.java @@ -1,11 +1,15 @@ package org.digijava.kernel.ampapi.endpoints.indicator.manager.dto; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.List; public class AmpOutcomeDTO { private Long id; private String name; private String description; + private List outputs; private List outputIds; public Long getId() { return id; } @@ -14,7 +18,16 @@ public class AmpOutcomeDTO { public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } - public List getOutputIds() { return outputIds; } - public void setOutputIds(List outputIds) { this.outputIds = outputIds; } -} + public List getOutputs() { return outputs; } + public void setOutputs(List outputs) { this.outputs = outputs; } + @JsonIgnore + public List getOutputIds() { + return outputIds; + } + @JsonProperty + public void setOutputIds(List outputIds) { + this.outputIds = outputIds; + } + +} diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/service/AmpOutcomeOutputService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/service/AmpOutcomeOutputService.java index f451b8752df..ad97fb028b1 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/service/AmpOutcomeOutputService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/service/AmpOutcomeOutputService.java @@ -114,6 +114,7 @@ private AmpOutcomeDTO toOutcomeDTO(AmpOutcome outcome) { dto.setId(outcome.getId()); dto.setName(outcome.getName()); dto.setDescription(outcome.getDescription()); + dto.setOutputs(outcome.getOutputs().stream().map(this::toOutputDTO).collect(Collectors.toList())); if (outcome.getOutputs() != null) { dto.setOutputIds(outcome.getOutputs().stream().map(AmpOutput::getId).collect(Collectors.toList())); } From d000ad4fe1e5c6c98680f8aba5fef03e9ad66de8 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Fri, 22 Aug 2025 07:32:45 +0300 Subject: [PATCH 009/138] AMP-31026: Output / outcome manager AMP-31027: List/View Existing Outcomes and Outputs AMP-31028:Add New Outcome AMP-31029:Add New Output --- .../indicator_manager/pages/OutcomeOutputManagementPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 4e13268e923..1e46c23034c 100644 --- 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 @@ -289,11 +289,11 @@ const OutcomeOutputManagementPage: React.FC = () => {
- {' '} - {' '} From 959910e0a3793651ce9bb672f0b0174ae284fd8c Mon Sep 17 00:00:00 2001 From: brianbrix Date: Fri, 22 Aug 2025 07:57:51 +0300 Subject: [PATCH 010/138] AMP-31026: Output / outcome manager AMP-31027: List/View Existing Outcomes and Outputs AMP-31028:Add New Outcome AMP-31029:Add New Output --- .../components/modals/OutcomeModal.tsx | 119 +++++++------ .../components/modals/OutputModal.tsx | 161 +++++++++--------- 2 files changed, 145 insertions(+), 135 deletions(-) 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 index 8f63de46040..ce152dd7f59 100644 --- 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 @@ -1,5 +1,8 @@ -import React, { useState } from 'react'; -import { Modal, Button, Form } from 'react-bootstrap'; +import React from 'react'; +import { Modal, Button, Form, Row, Col } from 'react-bootstrap'; +import { Formik, Form as FormikForm, Field } from 'formik'; +import * as Yup from 'yup'; +import styles from './css/IndicatorModal.module.css'; interface AddNewOutcomeModalProps { show: boolean; @@ -10,61 +13,67 @@ interface AddNewOutcomeModalProps { } const OutcomeModal: React.FC = ({ show, setShow, onSubmit, initialName = '', initialDescription = '' }) => { - const [name, setName] = useState(initialName); - const [description, setDescription] = useState(initialDescription); - - React.useEffect(() => { - setName(initialName); - setDescription(initialDescription); - }, [initialName, initialDescription, show]); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (onSubmit) { - onSubmit({ name, description }); - } - setShow(false); - setName(''); - setDescription(''); - }; + const validationSchema = Yup.object().shape({ + name: Yup.string().required('Outcome name is required'), + description: Yup.string() + }); return ( - setShow(false)}> - - Add New Outcome - -
- - - Outcome Name - setName(e.target.value)} - required - placeholder="Enter outcome name" - /> - - - Outcome Description (Optional) - setDescription(e.target.value)} - placeholder="Enter outcome description" - /> - - - - - - -
+ setShow(false)} centered> + { + if (onSubmit) onSubmit(values); + setShow(false); + resetForm(); + }} + > + {({ errors, touched, handleSubmit }) => ( + + + Add New Outcome + + + + Outcome Name + + + {errors.name && touched.name && ( +
{errors.name}
+ )} + +
+ + Description + + + + +
+ + + + +
+ )} +
); }; 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 index 4721929efef..c91700f30c3 100644 --- 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 @@ -1,5 +1,9 @@ -import React, { useState } from 'react'; -import { Modal, Button, Form } from 'react-bootstrap'; +import React from 'react'; +import { Modal, Button, Form, Row, Col } from 'react-bootstrap'; +import { Formik, Form as FormikForm, Field } from 'formik'; +import * as Yup from 'yup'; +import Select from 'react-select'; +import styles from './css/IndicatorModal.module.css'; interface Outcome { id: number; @@ -17,88 +21,85 @@ interface AddNewOutputModalProps { } const OutputModal: React.FC = ({ show, setShow, outcomes, onSubmit, initialName = '', initialDescription = '', initialOutcomeIds = [] }) => { - const [name, setName] = useState(initialName); - const [description, setDescription] = useState(initialDescription); - const [selectedOutcomes, setSelectedOutcomes] = useState(initialOutcomeIds); + const validationSchema = Yup.object().shape({ + name: Yup.string().required('Output name is required'), + description: Yup.string(), + outcomeIds: Yup.array().min(1, 'Select at least one outcome') + }); - React.useEffect(() => { - setName(initialName); - setDescription(initialDescription); - setSelectedOutcomes(initialOutcomeIds); - }, [initialName, initialDescription, initialOutcomeIds, show]); - - const handleCheckboxChange = (id: number) => { - setSelectedOutcomes(prev => - prev.includes(id) ? prev.filter(oid => oid !== id) : [...prev, id] - ); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (onSubmit) { - onSubmit({ name, description, outcomeIds: selectedOutcomes }); - } - setShow(false); - setName(''); - setDescription(''); - setSelectedOutcomes([]); - }; + const outcomeOptions = outcomes.map(o => ({ value: o.id, label: o.name })); return ( - setShow(false)}> - - Add New Output - -
- - - Output Name - setName(e.target.value)} - required - placeholder="Enter output name" - /> - - - Output Description (Optional) - setDescription(e.target.value)} - placeholder="Enter output description" - /> - - - Link to Parent Outcome(s) -
- {outcomes.length === 0 ? ( -
No outcomes available
- ) : ( - outcomes.map(outcome => ( - handleCheckboxChange(outcome.id)} + setShow(false)} centered> + { + if (onSubmit) onSubmit(values); + setShow(false); + resetForm(); + }} + > + {({ errors, touched, handleSubmit, setFieldValue, values }) => ( + + + Add New Output + + + + Output Name + + + {errors.name && touched.name && ( +
{errors.name}
+ )} + +
+ + Description + + + + + + Linked Outcomes + + values.outcomeIds.includes(opt.value))} onChange={selected => setFieldValue('outcomeIds', selected.map((opt: any) => opt.value))} - placeholder="Select outcomes..." + placeholder={translations['amp.outcomeoutput:linked-outcomes']} /> {errors.outcomeIds && touched.outcomeIds && (
{errors.outcomeIds}
@@ -91,10 +92,10 @@ const OutputModal: React.FC = ({ show, setShow, outcomes
From d96d3154678462328c7f31489bd9c1811a761040 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Fri, 22 Aug 2025 09:39:13 +0300 Subject: [PATCH 014/138] AMP-31026: Output / outcome manager AMP-31027: List/View Existing Outcomes and Outputs AMP-31028:Add New Outcome AMP-31029:Add New Output --- .../components/modals/OutcomeModal.tsx | 16 +++---- .../components/modals/OutputModal.tsx | 22 +++++----- .../pages/OutcomeOutputManagementPage.tsx | 43 ++++++++++--------- .../pages/css/ModalZIndexFix.css | 7 +++ 4 files changed, 49 insertions(+), 39 deletions(-) create mode 100644 amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/pages/css/ModalZIndexFix.css 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 index 0c13665822a..0ad1df4cd41 100644 --- 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 @@ -15,7 +15,7 @@ interface AddNewOutcomeModalProps { const OutcomeModal: React.FC = ({ show, setShow, onSubmit, initialName = '', initialDescription = '', translations = {} }) => { const validationSchema = Yup.object().shape({ - name: Yup.string().required(translations['amp.outcomeoutput:outcome-name'] + ' ' + translations['amp.indicatormanager:errors-name-required']), + name: Yup.string().required(translations['amp.outcomeoutput:errors-name-required'] || 'Name is required'), description: Yup.string() }); @@ -34,17 +34,17 @@ const OutcomeModal: React.FC = ({ show, setShow, onSubm {({ errors, touched, handleSubmit }) => ( - {translations['amp.outcomeoutput:modal-title-outcome']} + {translations['amp.outcomeoutput:modal-title-outcome'] || 'Add New Outcome'} - {translations['amp.outcomeoutput:outcome-name']} + {translations['amp.outcomeoutput:outcome-name'] || 'Outcome Name'} {errors.name && touched.name && ( @@ -53,23 +53,23 @@ const OutcomeModal: React.FC = ({ show, setShow, onSubm - {translations['amp.outcomeoutput:outcome-description']} + {translations['amp.outcomeoutput:outcome-description'] || 'Outcome Description'} 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 index e68898d5af9..d8889496e5e 100644 --- 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 @@ -23,9 +23,9 @@ interface AddNewOutputModalProps { const OutputModal: React.FC = ({ show, setShow, outcomes, onSubmit, initialName = '', initialDescription = '', initialOutcomeIds = [], translations = {} }) => { const validationSchema = Yup.object().shape({ - name: Yup.string().required(translations['amp.outcomeoutput:output-name'] + ' ' + translations['amp.indicatormanager:errors-name-required']), + name: Yup.string().required(translations['amp.outcomeoutput:errors-name-required'] || 'Name is required'), description: Yup.string(), - outcomeIds: Yup.array().min(1, translations['amp.outcomeoutput:linked-outcomes'] + ' ' + translations['amp.indicatormanager:errors-name-required']) + outcomeIds: Yup.array().min(1, translations['amp.outcomeoutput:errors-linked-outcomes-required'] || 'Select at least one outcome') }); const outcomeOptions = outcomes.map(o => ({ value: o.id, label: o.name })); @@ -45,17 +45,17 @@ const OutputModal: React.FC = ({ show, setShow, outcomes {({ errors, touched, handleSubmit, setFieldValue, values }) => ( - {translations['amp.outcomeoutput:modal-title-output']} + {translations['amp.outcomeoutput:modal-title-output'] || 'Add New Output'} - {translations['amp.outcomeoutput:output-name']} + {translations['amp.outcomeoutput:output-name'] || 'Output Name'} {errors.name && touched.name && ( @@ -64,25 +64,25 @@ const OutputModal: React.FC = ({ show, setShow, outcomes - {translations['amp.outcomeoutput:output-description']} + {translations['amp.outcomeoutput:output-description'] || 'Output Description'} - {translations['amp.outcomeoutput:linked-outcomes']} + {translations['amp.outcomeoutput:linked-outcomes'] || 'Linked Outcomes'} values.outcomeIds.includes(opt.value))} - onChange={selected => setFieldValue('outcomeIds', selected.map((opt: any) => opt.value))} - placeholder={translations['amp.outcomeoutput:linked-outcomes'] || 'Linked Outcomes'} - /> - {errors.outcomeIds && touched.outcomeIds && ( -
{errors.outcomeIds}
- )} - -
+
+ + + {translations['amp.outcomeoutput:output-name'] || 'Output Name'} + + + {props.errors.name} + + + + + + {translations['amp.outcomeoutput:output-description'] || 'Output Description'} + + + {props.errors.description} + + + + + + {translations['amp.outcomeoutput:linked-outcomes'] || 'Linked Outcomes'} + { - // set the formik value with the selected values and remove the label - const selectedValues = values.map((value: any) => parseInt(value.value)) - props.setFieldValue('sectors', selectedValues); - }} - isClearable - getOptionValue={(option) => option.value} - onBlur={props.handleBlur} - className={`basic-multi-select ${(props.errors.sectors && props.touched.sectors) && styles.text_is_invalid}`} - classNamePrefix="select" - /> - ) : ( - { - // set the formik value with the selected values and remove the label - props.setFieldValue('indicatorsCategory', parseInt(value?.value)); - }} - isClearable - getOptionValue={(option: any) => option.value} + + Relevance for Climate Change Adaptation + + + + + + Type + ({ value: outcome.id, label: outcome.name }))} + placeholder="Select outcome" + onChange={(selectedValue) => { + setSelectedOutcomeId(selectedValue ? (selectedValue as { value: number }).value : null); + props.setFieldValue('outcomeId', selectedValue ? (selectedValue as { value: number }).value : null); + }} + isClearable + getOptionValue={(option) => String((option as { value: any }).value)} + onBlur={props.handleBlur} + className={styles.input_field} + classNamePrefix="select" + /> + + + + + Output + { + const selectedValues = values.map((value: any) => value.value) + props.setFieldValue('logframeLinks', selectedValues); + }} + isClearable + getOptionValue={(option) => String(option.value)} + onBlur={props.handleBlur} + className={styles.input_field} + classNamePrefix="select" + /> + + + {/* Sector (multi, mandatory) */} + + + Sector + { + const selectedValues = values.map((value: any) => parseInt(value.value)) + props.setFieldValue('disaggregation', selectedValues); + }} + isClearable + getOptionValue={(option: { value: number; label: string } | null) => String(option?.value)} + onBlur={props.handleBlur} + className={styles.input_field} + classNamePrefix="select" + /> + + + + + Unit of Measure + { + const selectedValues = Array.isArray(selected) + ? selected.map((option) => option.value) + : []; + props.setFieldValue('responsibleOrganizations', selectedValues); + }} + isClearable + getOptionValue={(option) => String((option as { value: any }).value)} + getOptionLabel={(option) => (option as { label: string }).label} + onBlur={props.handleBlur} + className={styles.input_field} + classNamePrefix="select" + /> + + + + + Frequency + - ) - } + + - - {filterByProgram && ( - <> - - - {translations["amp.indicatormanager:program-scheme"]} - { - programSchemes.length > 0 ? ( - - ) - } - - - - {programFieldVisible && ( - - - {translations["amp.indicatormanager:programs"]} - { - programs.length > 0 ? ( - - } - - - - )} - - )} - - - - -

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

-
-
- - - - {translations['amp.indicatormanager:original-value']} - - - - {props.errors.base?.originalValue} - - - - - {translations["amp.indicatormanager:original-value-date"]} - { - if (value) { - props.setFieldValue('base.originalValueDate', value); - } - }} - onClear={() => { - props.setFieldValue('base.originalValueDate', null); - }} - onBlur={props.handleBlur} - disabled={baseOriginalValueDateDisabled} - className={`${styles.input_field} ${(props.errors.base?.originalValueDate && props.touched.base?.originalValueDate) && styles.text_is_invalid}`}/> - - - {props.errors.base?.originalValueDate} - - - - - - - {translations["amp.indicatormanager:revised-value"]} - - - - {props.errors.base?.revisedValue} - - - - - {translations['amp.indicatormanager:revised-value-date']} - { - if (value) { - props.setFieldValue('base.revisedValueDate', value); - } - }} - onClear={() => { - props.setFieldValue('base.revisedValueDate', null); - }} - onBlur={props.handleBlur} - name="base.revisedValueDate" - className={`${styles.input_field} ${(props.errors.base?.revisedValueDate && props.touched.base?.revisedValueDate) && styles.text_is_invalid}`} - /> - - - {props.errors.base?.revisedValueDate} - - - -
- - -

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

- - - {translations["amp.indicatormanager:target-value"]} - - - - {props.errors.target?.originalValue} - - - - {translations["amp.indicatormanager:target-value-date"]} - { - if (value) { - props.setFieldValue('target.originalValueDate', value); - } - }} - onClear={() => { - props.setFieldValue('target.originalValueDate', null); - }} - onBlur={props.handleBlur} - disabled={targetOriginalValueDateDisabled} - className={`${styles.input_field} ${(props.errors.target?.originalValueDate && props.touched.target?.originalValueDate) && styles.text_is_invalid}`} /> - - - {props.errors.target?.originalValueDate} - - - - - - - {translations["amp.indicatormanager:revised-value"]} - - - - {props.errors.target?.revisedValue} - - - - - {translations["amp.indicatormanager:revised-value-date"]} - { - if (value) { - props.setFieldValue('target.revisedValueDate', value); - } - }} - onClear={() => { - props.setFieldValue('target.revisedValueDate', null); - }} - onBlur={props.handleBlur} - name="target.revisedValueDate" - className={`${styles.input_field} ${(props.errors.target?.revisedValueDate && props.touched.target?.revisedValueDate) && styles.text_is_invalid}`} - /> - - - {props.errors.target?.revisedValueDate} - - - -
@@ -720,3 +855,4 @@ const AddNewIndicatorModal: React.FC = (props) => { }; 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..72ad3fe2b77 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 @@ -36,13 +36,24 @@ interface IndicatorFormValues { name: string; description?: string; code: string; + relevanceForClimateChange?: string; + indicatorType?: string; 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; base: BaseAndTargetValueType; target: BaseAndTargetValueType; - indicatorsCategory?: string; + outcomeId?: number; + outputId?: number; } const EditIndicatorModal: React.FC = (props) => { @@ -76,6 +87,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,6 +108,11 @@ 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 }[]>([]); + const convertDateToISO = (date?: string) => { if (!date) { return ''; @@ -261,6 +282,8 @@ const EditIndicatorModal: React.FC = (props) => { }, []); + + useEffect(() => { getDefaultSectors(); getDefaultCategory(); @@ -302,7 +325,17 @@ const EditIndicatorModal: React.FC = (props) => { name: indicator?.name || '', description: indicator?.description || '', code: indicator?.code || '', + relevanceForClimateChange: indicator?.relevanceForClimateChange || '', + indicatorType: indicator?.indicatorType || '', sectors: indicator?.sectors || [], + 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: '', ascending: indicator?.ascending || false, creationDate: indicator?.creationDate ? convertDateToISO(indicator?.creationDate) : '', @@ -318,9 +351,49 @@ 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 }; + 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) { + 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 ? parseInt(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 + }; + + 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)); }} > @@ -398,8 +468,10 @@ const EditIndicatorModal: React.FC = (props) => {
+ {/* Core Indicator Information */} +
Core Indicator Information
- + {translations["amp.indicatormanager:indicator-name"]} = (props) => { {props.errors.name} - {translations["amp.indicatormanager:indicator-code"]} = (props) => { - - - {translations["amp.indicatormanager:ascending"]} + + Relevance for Climate Change Adaptation + + + + + + Type ({ value: outcome.id, label: outcome.name }))} + onChange={(selectedValue) => { + setSelectedOutcomeId(selectedValue?.value ?? null); + props.setFieldValue('outcomeId', selectedValue?.value); + }} onBlur={props.handleBlur} - onChange={(value) => { - props.setFieldValue("ascending", value?.value); + className={`basic-multi-select ${(props.errors.outcomeId && props.touched.outcomeId) && styles.text_is_invalid}`} + classNamePrefix="select" + value={allOutcomes.find(outcome => outcome.id === selectedOutcomeId) ? { value: selectedOutcomeId, label: allOutcomes.find(outcome => outcome.id === selectedOutcomeId)?.name } : null} + /> + + + Output + { + 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} + /> + + {programFieldVisible && ( + + Program + { + 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} + /> + + + {/* Data Definition and Sourcing */} +
Data Definition and Sourcing
+ + + Data + + placeholder="Describe the nature of the data to be collected" + /> + + + Data Source + - - {filterBySector && ( - - - {translations["amp.indicatormanager:sectors"]} - { - (sectors.length > 0 && defaultSectors !== undefined)? ( - - ) - } - - - )} - - - - {translations["amp.indicatormanager:indicators-category"]} - { - categories.length > 0 ? ( - - ) - } + + Disaggregation + { + 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} + /> - - {filterByProgram && ( - <> - - - {translations["amp.indicatormanager:program-scheme"]} - { - programSchemes.length > 0 ? ( - - ) - } - - - - {programFieldVisible && ( - - - {translations["amp.indicatormanager:programs"]} - { - (programs.length > 0) ? ( - - } - - - - )} - - )} - - + + + Calculation Method + + + + {/* Responsibility and Frequency */} +
Responsibility and Frequency
+ + + Responsible Organization(s) + { + props.setFieldValue('frequency', selectedValue?.value); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.frequency && props.touched.frequency) && styles.text_is_invalid}`} + classNamePrefix="select" + value={frequencyOptions.find(opt => opt.value === props.values.frequency) || null} + /> + + + {/* Value Tracking */} +
Value Tracking
- -

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

-
+

Base Value

- - {translations['amp.indicatormanager:original-value']} + Original Value = (props) => { name="base.originalValue" type="number" className={`${styles.input_field} ${(props.errors.base?.originalValue && props.touched.base?.originalValue) && styles.text_is_invalid}`} - placeholder={translations["amp.indicatormanager:enter-original-value"]} /> - + placeholder="Enter original value" /> {props.errors.base?.originalValue} - - {translations["amp.indicatormanager:original-value-date"]} + Original Value Date { if (value) { @@ -664,16 +791,14 @@ const EditIndicatorModal: React.FC = (props) => { id="baseOriginalValueDate" inputRef={baseOriginalValueDateRef} /> - {props.errors.base?.originalValueDate} - - {translations["amp.indicatormanager:revised-value"]} + Revised Value = (props) => { name="base.revisedValue" type="number" className={`${styles.input_field} ${(props.errors.base?.revisedValue && props.touched.base?.revisedValue) && styles.text_is_invalid}`} - placeholder={translations["amp.indicatormanager:enter-revised-value"]} /> - + placeholder="Enter revised value" /> {props.errors.base?.revisedValue} - - {translations['amp.indicatormanager:revised-value-date']} + Revised Value Date { if (value) { @@ -707,19 +830,17 @@ const EditIndicatorModal: React.FC = (props) => { id="baseRevisedValueDate" inputRef={baseRevisedValueDateRef} /> - {props.errors.base?.revisedValueDate}
- -

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

+

Target Value

- {translations["amp.indicatormanager:target-value"]} + Original Value = (props) => { name="target.originalValue" type="number" className={`${styles.input_field} ${(props.errors.target?.originalValue && props.touched.target?.originalValue) && styles.text_is_invalid}`} - placeholder={translations["amp.indicatormanager:enter-target-value"]} /> - + placeholder="Enter target value" /> {props.errors.target?.originalValue} - {translations["amp.indicatormanager:target-value-date"]} + Original Value Date { if (value) { @@ -755,10 +875,9 @@ const EditIndicatorModal: React.FC = (props) => { /> - - {translations["amp.indicatormanager:revised-value"]} + Revised Value = (props) => { name="target.revisedValue" type="number" className={`${styles.input_field} ${(props.errors.target?.revisedValue && props.touched.target?.revisedValue) && styles.text_is_invalid}`} - placeholder={translations["amp.indicatormanager:enter-revised-value"]} /> - + placeholder="Enter revised value" /> {props.errors.target?.revisedValue} - - {translations["amp.indicatormanager:revised-value-date"]} + Revised Value Date { if (value) { @@ -792,13 +909,45 @@ const EditIndicatorModal: React.FC = (props) => { id="targetRevisedValueDate" inputRef={targetRevisedValueDateRef} /> - {props.errors.target?.revisedValueDate}
+ {/* Other Considerations */} +
Other Considerations
+ + + Creation Date + + + + Ascending + ({ value: outcome.id, label: outcome.name }))} - placeholder="Select outcome" + placeholder={translations["amp.indicatormanager:select-outcome"]} onChange={(selectedValue) => { setSelectedOutcomeId(selectedValue ? (selectedValue as { value: number }).value : null); props.setFieldValue('outcomeId', selectedValue ? (selectedValue as { value: number }).value : null); 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 72ad3fe2b77..e93407718e1 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 @@ -469,7 +469,7 @@ const EditIndicatorModal: React.FC = (props) => {
{/* Core Indicator Information */} -
Core Indicator Information
+
{translations["amp.indicatormanager:core-info"]}
{translations["amp.indicatormanager:indicator-name"]} @@ -524,7 +524,7 @@ const EditIndicatorModal: React.FC = (props) => { - Relevance for Climate Change Adaptation + {translations["amp.indicatormanager:relevance-for-climate-change"]} = (props) => { name="relevanceForClimateChange" type="text" className={styles.input_field} - placeholder="Describe relevance for climate change adaptation" + placeholder={translations["amp.indicatormanager:relevance-for-climate-change"]} /> @@ -553,10 +553,10 @@ const EditIndicatorModal: React.FC = (props) => { {/* Categorization and Linkage */} -
Categorization and Linkage
+
{translations["amp.indicatormanager:categorization-linkage-info"] || "Categorization and Linkage"}
- Outcome + {translations["amp.indicatormanager:outcome"]} ({ value: output.id, label: output.name }))} @@ -645,10 +645,10 @@ const EditIndicatorModal: React.FC = (props) => { {/* Data Definition and Sourcing */} -
Data Definition and Sourcing
+
{translations["amp.indicatormanager:data-definition-sourcing-info"] || "Data Definition and Sourcing"}
- Data + {translations["amp.indicatormanager:data"]} = (props) => { name="data" type="text" className={styles.input_field} - placeholder="Describe the nature of the data to be collected" + placeholder={translations["amp.indicatormanager:enter-data"]} /> - Data Source + {translations["amp.indicatormanager:data-source"]} = (props) => { name="dataSource" type="text" className={styles.input_field} - placeholder="Specify where the data will be obtained" + placeholder={translations["amp.indicatormanager:enter-data-source"]} /> 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 a9a6595e3d5..48169fe59ca 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 @@ -66,7 +66,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", @@ -120,5 +120,50 @@ "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.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:enter-unit-of-measure": "Enter Unit of Measure", + "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" } diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/reducers/fetchOutcomesReducer.ts b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/reducers/fetchOutcomesReducer.ts index aa5b3d2fe72..1469b87f9cb 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/reducers/fetchOutcomesReducer.ts +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/reducers/fetchOutcomesReducer.ts @@ -45,3 +45,5 @@ export const getOutcomes = () => async (dispatch: Dispatch) => { dispatch({ type: FETCH_OUTCOMES_FAILURE, payload: error.message || 'Failed to fetch outcomes' }); } }; + +export default fetchOutcomesReducer; diff --git a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/reducers/fetchResponsibleOrgsReducer.ts b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/reducers/fetchResponsibleOrgsReducer.ts index 8a0694e86b4..d3ba76d3353 100644 --- a/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/reducers/fetchResponsibleOrgsReducer.ts +++ b/amp/TEMPLATE/reampv2/packages/reampv2-app/src/modules/admin/indicator_manager/reducers/fetchResponsibleOrgsReducer.ts @@ -46,3 +46,4 @@ export const getResponsibleOrgs = () => async (dispatch: Dispatch) => { } }; +export default fetchResponsibleOrgsReducer; From 3faa5e6cceddf022cf087c6966b8834118653e8c Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sun, 24 Aug 2025 01:14:48 +0300 Subject: [PATCH 031/138] AMP-31025: ADD / Edit indicator --- .../endpoints/indicator/manager/IndicatorManagerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java index ab59d2fc339..15f40401573 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java @@ -529,7 +529,7 @@ public Set getResponsibleOrganizations() { Session session = PersistenceManager.getSession(); String sql = "SELECT DISTINCT o.amp_org_id, o.name FROM amp_organisation o JOIN amp_org_role org_role ON o.amp_org_id = org_role.organisation WHERE org_role.role =(SELECT amp_role_id FROM amp_role WHERE role_code = 'RO' LIMIT 1) ORDER BY o.name;"; List results = session.createSQLQuery(sql).list(); - List orgs = new ArrayList<>(); + Set orgs = new HashSet<>(); for (Object[] row : results) { Long orgId = ((Number) row[0]).longValue(); String orgName = (String) row[1]; From a5b3e591d73c504053f9d8c8ae5d3770c86e81e9 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sun, 24 Aug 2025 01:15:19 +0300 Subject: [PATCH 032/138] AMP-31025: ADD / Edit indicator --- .../endpoints/indicator/manager/IndicatorManagerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java index 15f40401573..2f7563e40a1 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java @@ -528,7 +528,7 @@ public List getCategoryValues () { public Set getResponsibleOrganizations() { Session session = PersistenceManager.getSession(); String sql = "SELECT DISTINCT o.amp_org_id, o.name FROM amp_organisation o JOIN amp_org_role org_role ON o.amp_org_id = org_role.organisation WHERE org_role.role =(SELECT amp_role_id FROM amp_role WHERE role_code = 'RO' LIMIT 1) ORDER BY o.name;"; - List results = session.createSQLQuery(sql).list(); + List results = session.createQuery(sql).list(); Set orgs = new HashSet<>(); for (Object[] row : results) { Long orgId = ((Number) row[0]).longValue(); From f9c464f59918e729ffb846639524151859d08f86 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sun, 24 Aug 2025 01:15:53 +0300 Subject: [PATCH 033/138] AMP-31025: ADD / Edit indicator --- .../endpoints/indicator/manager/IndicatorManagerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java index 2f7563e40a1..89bd77e3f60 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java @@ -528,7 +528,7 @@ public List getCategoryValues () { public Set getResponsibleOrganizations() { Session session = PersistenceManager.getSession(); String sql = "SELECT DISTINCT o.amp_org_id, o.name FROM amp_organisation o JOIN amp_org_role org_role ON o.amp_org_id = org_role.organisation WHERE org_role.role =(SELECT amp_role_id FROM amp_role WHERE role_code = 'RO' LIMIT 1) ORDER BY o.name;"; - List results = session.createQuery(sql).list(); + List results = session.createNativeQuery(sql).list(); Set orgs = new HashSet<>(); for (Object[] row : results) { Long orgId = ((Number) row[0]).longValue(); From 22e8c13d474439385f6517c39d5f70ca6f75ade2 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sun, 24 Aug 2025 01:27:34 +0300 Subject: [PATCH 034/138] AMP-31025: ADD / Edit indicator --- ...es-1.xml => AMP-31025-Add-Indicator-Categories-v1.xml} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename amp/src/main/resources/xmlpatches/4.0/{AMP-31025-Add-Indicator-Categories-1.xml => AMP-31025-Add-Indicator-Categories-v1.xml} (92%) diff --git a/amp/src/main/resources/xmlpatches/4.0/AMP-31025-Add-Indicator-Categories-1.xml b/amp/src/main/resources/xmlpatches/4.0/AMP-31025-Add-Indicator-Categories-v1.xml similarity index 92% rename from amp/src/main/resources/xmlpatches/4.0/AMP-31025-Add-Indicator-Categories-1.xml rename to amp/src/main/resources/xmlpatches/4.0/AMP-31025-Add-Indicator-Categories-v1.xml index cce75578e5d..b88622f1b05 100644 --- a/amp/src/main/resources/xmlpatches/4.0/AMP-31025-Add-Indicator-Categories-1.xml +++ b/amp/src/main/resources/xmlpatches/4.0/AMP-31025-Add-Indicator-Categories-v1.xml @@ -11,14 +11,14 @@ -- Indicator Type INSERT INTO amp_category_class (id, category_name, description, keyname, is_multiselect, is_ordered) VALUES ((SELECT nextval('AMP_CATEGORY_CLASS_seq')), 'Indicator Type', 'Type of indicator', 'indicator_type', false, false); - INSERT INTO amp_category_value (amp_cat_val_id, value, index, deleted, id) + INSERT INTO amp_category_value (id, category_value, index_column, deleted, amp_category_class_id) VALUES ((SELECT nextval('AMP_CATEGORY_VALUE_seq')), 'Adaptation', 1, false, (SELECT id FROM amp_category_class WHERE keyname = 'indicator_type')), ((SELECT nextval('AMP_CATEGORY_VALUE_seq')), 'Mitigation', 2, false, (SELECT id FROM amp_category_class WHERE keyname = 'indicator_type')); -- Indicator Disaggregation INSERT INTO amp_category_class (id, category_name, description, keyname, is_multiselect, is_ordered) VALUES ((SELECT nextval('AMP_CATEGORY_CLASS_seq')), 'Indicator Disaggregation', 'Disaggregation dimensions for indicators', 'indicator_disaggregation', true, false); - INSERT INTO amp_category_value (amp_cat_val_id, value, index, deleted, id) + INSERT INTO amp_category_value (id, category_value, index_column, deleted, amp_category_class_id) VALUES ((SELECT nextval('AMP_CATEGORY_VALUE_seq')), 'Gender', 1, false, (SELECT id FROM amp_category_class WHERE keyname = 'indicator_disaggregation')), ((SELECT nextval('AMP_CATEGORY_VALUE_seq')), 'Age Group', 2, false, (SELECT id FROM amp_category_class WHERE keyname = 'indicator_disaggregation')), ((SELECT nextval('AMP_CATEGORY_VALUE_seq')), 'Geographical Region/Location', 3, false, (SELECT id FROM amp_category_class WHERE keyname = 'indicator_disaggregation')), @@ -29,7 +29,7 @@ -- Indicator Unit of Measure INSERT INTO amp_category_class (id, category_name, description, keyname, is_multiselect, is_ordered) VALUES ((SELECT nextval('AMP_CATEGORY_CLASS_seq')), 'Indicator Unit of Measure', 'Unit in which indicator is measured', 'indicator_unit_of_measure', false, false); - INSERT INTO amp_category_value (amp_cat_val_id, value, index, deleted, id) + INSERT INTO amp_category_value (id, category_value, index_column, deleted, amp_category_class_id) VALUES ((SELECT nextval('AMP_CATEGORY_VALUE_seq')), '#', 1, false, (SELECT id FROM amp_category_class WHERE keyname = 'indicator_unit_of_measure')), ((SELECT nextval('AMP_CATEGORY_VALUE_seq')), 'hectares', 2, false, (SELECT id FROM amp_category_class WHERE keyname = 'indicator_unit_of_measure')), ((SELECT nextval('AMP_CATEGORY_VALUE_seq')), 'percent', 3, false, (SELECT id FROM amp_category_class WHERE keyname = 'indicator_unit_of_measure')), @@ -41,7 +41,7 @@ -- Indicator Frequency INSERT INTO amp_category_class (id, category_name, description, keyname, is_multiselect, is_ordered) VALUES ((SELECT nextval('AMP_CATEGORY_CLASS_seq')), 'Indicator Frequency', 'Reporting frequency for indicator', 'indicator_frequency', false, true); - INSERT INTO amp_category_value (amp_cat_val_id, value, index, deleted, id) + INSERT INTO amp_category_value (id, category_value, index_column, deleted, amp_category_class_id) VALUES ((SELECT nextval('AMP_CATEGORY_VALUE_seq')), 'Monthly', 1, false, (SELECT id FROM amp_category_class WHERE keyname = 'indicator_frequency')), ((SELECT nextval('AMP_CATEGORY_VALUE_seq')), 'Quarterly', 2, false, (SELECT id FROM amp_category_class WHERE keyname = 'indicator_frequency')), ((SELECT nextval('AMP_CATEGORY_VALUE_seq')), 'Yearly', 3, false, (SELECT id FROM amp_category_class WHERE keyname = 'indicator_frequency')), From 6044943052d25e6deb366cc8fdcf6669a0a30f12 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sun, 24 Aug 2025 01:41:14 +0300 Subject: [PATCH 035/138] AMP-31025: ADD / Edit indicator --- .../endpoints/indicator/manager/IndicatorManagerService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java index 89bd77e3f60..7f90fca3d29 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java @@ -516,7 +516,10 @@ public void validateIndicatorCode(String code, Session session) { public List getCategoryValues () { Session session = PersistenceManager.getSession(); - List categoryValues = session.createQuery("select o from " + AmpCategoryValue.class.getName() + " o ").list(); + List categoryValues = session.createQuery("select o from " + AmpCategoryValue.class.getName() + " o " + + "where o.ampCategoryClass.keyName like :keyName") + .setString("keyName", "%indicator%") + .list(); return categoryValues.stream() .map(AmpCategoryValueDTO::new) From 5e5e8b4d7f16b435ada34cb624f546741a2282aa Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 25 Aug 2025 08:49:55 +0300 Subject: [PATCH 036/138] AMP-31025: ADD / Edit indicator --- .../modals/AddNewIndicatorModal.tsx | 118 ++++++++---------- .../components/modals/EditIndicatorModal.tsx | 116 +++++++++-------- 2 files changed, 113 insertions(+), 121 deletions(-) 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 0004730022e..f171dc0b7c2 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 @@ -442,41 +442,7 @@ const AddNewIndicatorModal: React.FC = (props) => { - - - {translations["amp.indicatormanager:ascending"]} - { + if (value) props.setFieldValue('ascending', value.value) + }} + defaultValue={{ + value: false, + label: translations["amp.indicatormanager:true"] + }} + /> + + {props.errors.ascending} + + + + + {translations["amp.indicatormanager:table-header-creation-date"]} +
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 e93407718e1..d03da2b5056 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 @@ -506,14 +506,15 @@ const EditIndicatorModal: React.FC = (props) => {
- + {translations["amp.indicatormanager:indicator-description"]} @@ -523,14 +524,15 @@ const EditIndicatorModal: React.FC = (props) => { - + {translations["amp.indicatormanager:relevance-for-climate-change"]} @@ -753,12 +755,11 @@ const EditIndicatorModal: React.FC = (props) => { {/* Value Tracking */}
Value Tracking
+

Base Values

+ {/* Original Value and Date in one row */} -

Base Value

-
- - - Original Value + + Original Base Value = (props) => { {props.errors.base?.originalValue} - - + + Original Value Date = (props) => { {props.errors.base?.originalValueDate} - + + {/* Revised Value and Date in one row */} - - Revised Value + + Revised Base Value = (props) => { {props.errors.base?.revisedValue} - - + + Revised Value Date = (props) => { {props.errors.base?.revisedValueDate} - +
-

Target Value

+

Target Values

+ {/* Original Value and Date in one row */} - - Original Value + + Original Target Value = (props) => { {props.errors.target?.originalValue} - - + + Original Value Date = (props) => { id="targetOriginalValueDate" inputRef={targetOriginalValueDateRef} /> - + + {/* Revised Value and Date in one row */} - - Revised Value + + Revised Target Value = (props) => { {props.errors.target?.revisedValue} - - + + Revised Value Date = (props) => { {props.errors.target?.revisedValueDate} - +
- {/* Other Considerations */} + {/* Other Considerations - Separate Group */}
Other Considerations
- - Creation Date - - - Ascending + {translations["amp.indicatormanager:ascending"]} { - if (value) props.setFieldValue('indicatorType', value.value) - }} - isClearable - placeholder="Select type" - onBlur={props.handleBlur} - className={styles.input_field} - classNamePrefix="select" - /> - - - {/* Categorization and Linkage */} -
{translations["amp.indicatormanager:categorization-linkage-info"] || "Categorization and Linkage"}
- - - {translations["amp.indicatormanager:outcome"]} - ({ value: output.id, label: output.name }))} - placeholder="Select output" - onChange={(selectedValue) => { - props.setFieldValue('outputId', selectedValue ? selectedValue.value : null); - }} - isClearable - getOptionValue={(option) => String((option as { value: any }).value)} - onBlur={props.handleBlur} - className={styles.input_field} - classNamePrefix="select" - isDisabled={filteredOutputs.length === 0} - /> - - - - - Link to Logframe (Program Scheme) - { - const selectedValues = values.map((value: any) => parseInt(value.value)) - props.setFieldValue('sectors', selectedValues); - }} - isClearable - getOptionValue={(option) => String(option.value)} - onBlur={props.handleBlur} - className={styles.input_field} - classNamePrefix="select" - /> - - - {/* Data Definition and Sourcing */} -
Data Definition and Sourcing
- - - Data - - - - - - Data Source - - - - {/* Disaggregation, Unit of Measure, Calculation Method */} - - - Disaggregation - { - if (value) props.setFieldValue('unitOfMeasure', value.value) - }} - isClearable - placeholder="Select unit of measure" - onBlur={props.handleBlur} - className={styles.input_field} - classNamePrefix="select" - /> - - - - - Calculation Method - - - - {/* Responsibility and Frequency */} -
Responsibility and Frequency
- - - Responsible Organization(s) - { - if (value) props.setFieldValue('frequency', value.value) - }} - isClearable - placeholder="Select frequency" - onBlur={props.handleBlur} - className={styles.input_field} - classNamePrefix="select" - /> - - - {/* Value Tracking - New Section */} -
Value Tracking
- - - Value Tracking -
- Base Values - {/* Original Value and Date in one row */} - - - Original Base Value - + +
+ {/* Core Indicator Information */} +
{translations["amp.indicatormanager:core-info"]}
+ + + {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:relevance-for-climate-change"]} + + + + + + Type + ({ value: outcome.id, label: outcome.name }))} + onChange={(selectedValue) => { + setSelectedOutcomeId(selectedValue?.value ?? null); + props.setFieldValue('outcomeId', selectedValue?.value); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.outcomeId && props.touched.outcomeId) && styles.text_is_invalid}`} + classNamePrefix="select" + value={allOutcomes.find(outcome => outcome.id === selectedOutcomeId) ? { value: selectedOutcomeId, label: allOutcomes.find(outcome => outcome.id === selectedOutcomeId)?.name } : null} + /> + + + {translations["amp.indicatormanager:output"]} + { + if (selectedValue) { + handleProgramSchemeChange(selectedValue.value, props); + } + }} + isClearable + getOptionValue={(option) => option.value} + onBlur={props.handleBlur} + className={`basic-multi-select ${styles.input_field}`} + classNamePrefix="select" + /> + + {programFieldVisible && ( + + Program + { + const selectedValues = values.map((value: any) => parseInt(value.value)) + props.setFieldValue('sectors', selectedValues); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.sectors && props.touched.sectors) && styles.text_is_invalid}`} + classNamePrefix="select" + /> + + + {/* Data Definition and Sourcing */} +
{translations["amp.indicatormanager:data-definition-sourcing-info"] || "Data Definition and Sourcing"}
+ + + {translations["amp.indicatormanager:data"]} + + + + {translations["amp.indicatormanager:data-source"]} + + + + + + Disaggregation + { + 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 + + + + {/* Responsibility and Frequency */} +
Responsibility and Frequency
+ + + Responsible Organization(s) + { + props.setFieldValue('frequency', selectedValue?.value); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.frequency && props.touched.frequency) && styles.text_is_invalid}`} + classNamePrefix="select" + value={frequencyOptions.find(opt => opt.value === props.values.frequency) || null} + /> + + + {/* Value Tracking */} +
Value Tracking
+ + +

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

+
+ {/* Original Value and Date in one row */} + + + {translations['amp.indicatormanager:original-value']} + - - - Original ValueDate - + + + {props.errors.base?.originalValue} + + + + + {translations["amp.indicatormanager:original-value-date"]} + props.setFieldValue('base.originalValueDate', val)} + value={props.values.base.originalValueDate} + onChange={(value) => { + if (value) { + props.setFieldValue('base.originalValueDate', value); + } + }} + onClear={() => { + props.setFieldValue('base.originalValueDate', null); + }} onBlur={props.handleBlur} - /> - - - {/* Revised Value and Date in one row */} - - - Revised Base Value - + + + {props.errors.base?.originalValueDate} + +
+ + {/* Revised Value and Date in one row */} + + + {translations["amp.indicatormanager:revised-value"]} + - - - Revised Value Date - + + + {props.errors.base?.revisedValue} + + + + + {translations['amp.indicatormanager:revised-value-date']} + { + if (value) { + props.setFieldValue('base.revisedValueDate', value); + } + }} + onClear={() => { + props.setFieldValue('base.revisedValueDate', null); + }} + onBlur={props.handleBlur} name="base.revisedValueDate" - value={props.values.base.revisedValueDate || ''} - onChange={val => props.setFieldValue('base.revisedValueDate', val)} + className={`${styles.input_field} ${(props.errors.base?.revisedValueDate && props.touched.base?.revisedValueDate) && styles.text_is_invalid}`} + /> + + + {props.errors.base?.revisedValueDate} + + + + + +

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

+ {/* Original Value and Date in one row */} + + + {translations["amp.indicatormanager:target-value"]} + - - - Target Value - {/* Original Value and Date in one row */} - - - Original Target Value - + + + {props.errors.target?.originalValue} + +
+ + {translations["amp.indicatormanager:target-value-date"]} + { + if (value) { + props.setFieldValue('target.originalValueDate', value); + } + }} + onClear={() => { + props.setFieldValue('target.originalValueDate', null); + }} + onBlur={props.handleBlur} + disabled={targetOriginalValueDateDisabled} + className={`${styles.input_field} ${(props.errors.target?.originalValueDate && props.touched.target?.originalValueDate) && styles.text_is_invalid}`} /> + + + {props.errors.target?.originalValueDate} + + + + {/* Revised Value and Date in one row */} + + + {translations["amp.indicatormanager:revised-value"]} + - - - Original Value Date - props.setFieldValue('target.originalValueDate', val)} - onBlur={props.handleBlur} - /> - - - {/* Revised Value and Date in one row */} - - - Revised Target Value - + + + {props.errors.target?.revisedValue} + + + + + {translations["amp.indicatormanager:revised-value-date"]} + { + if (value) { + props.setFieldValue('target.revisedValueDate', value); + } + }} + onClear={() => { + props.setFieldValue('target.revisedValueDate', null); + }} onBlur={props.handleBlur} - isInvalid={!!props.errors.target?.revisedValue} - /> - - - Revised Value Date - props.setFieldValue('target.revisedValueDate', val)} - onBlur={props.handleBlur} - /> - - -
- -
- {/* Other Considerations - Separate Group */} -
Other Considerations
- - - {translations["amp.indicatormanager:ascending"]} - { + if (value) props.setFieldValue('ascending', value.value) + }} + defaultValue={{ + value: false, + label: translations["amp.indicatormanager:true"] + }} + /> + + {props.errors.ascending} + + + + + {translations["amp.indicatormanager:table-header-creation-date"]} + + + +
+ + + + + + + )} 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 d03da2b5056..71efcecd8f6 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 @@ -755,171 +755,183 @@ const EditIndicatorModal: React.FC = (props) => { {/* Value Tracking */}
Value Tracking
-

Base Values

+ +

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

+
{/* Original Value and Date in one row */} - - Original Base Value + + {translations['amp.indicatormanager:original-value']} + defaultValue={props.values.base?.originalValue} + onChange={props.handleChange} + onBlur={props.handleBlur} + name="base.originalValue" + type="number" + className={`${styles.input_field} ${(props.errors.base?.originalValue && props.touched.base?.originalValue) && styles.text_is_invalid}`} + placeholder={translations["amp.indicatormanager:enter-original-value"]} /> + {props.errors.base?.originalValue} - - - Original Value Date + + + + {translations["amp.indicatormanager:original-value-date"]} { - if (value) { - props.setFieldValue("base.originalValueDate", value); - } - }} - onClear={() => { - props.setFieldValue("base.originalValueDate", null); - }} - onBlur={props.handleBlur} - name="base.originalValueDate" - disabled={baseOriginalValueDateDisabled} - className={`${styles.input_field} ${(props.errors.base?.originalValueDate && props.touched.base?.originalValueDate) && styles.text_is_invalid}`} - id="baseOriginalValueDate" - inputRef={baseOriginalValueDateRef} + translations={translations} + value={props.values.base?.originalValueDate} + onChange={(value) => { + if (value) { + props.setFieldValue("base.originalValueDate", value); + } + }} + onClear={() => { + props.setFieldValue("base.originalValueDate", null); + }} + onBlur={props.handleBlur} + name="base.originalValueDate" + disabled={baseOriginalValueDateDisabled} + className={`${styles.input_field} ${(props.errors.base?.originalValueDate && props.touched.base?.originalValueDate) && styles.text_is_invalid}`} + id="baseOriginalValueDate" + inputRef={baseOriginalValueDateRef} /> + {props.errors.base?.originalValueDate} - + {/* Revised Value and Date in one row */} - - Revised Base Value + + {translations["amp.indicatormanager:revised-value"]} + defaultValue={props.values.base.revisedValue} + onChange={props.handleChange} + onBlur={props.handleBlur} + name="base.revisedValue" + type="number" + className={`${styles.input_field} ${(props.errors.base?.revisedValue && props.touched.base?.revisedValue) && styles.text_is_invalid}`} + placeholder={translations["amp.indicatormanager:enter-revised-value"]} /> + {props.errors.base?.revisedValue} - - - Revised Value Date + + + + {translations['amp.indicatormanager:revised-value-date']} { - if (value) { - props.setFieldValue("base.revisedValueDate", value); - } - }} - onClear={() => { - props.setFieldValue("base.revisedValueDate", null); - }} - 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} + translations={translations} + value={props.values.base.revisedValueDate} + onChange={(value) =>{ + if (value) { + props.setFieldValue("base.revisedValueDate", value); + } + }} + onClear={() => { + props.setFieldValue("base.revisedValueDate", null); + }} + 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} /> + {props.errors.base?.revisedValueDate} - +
-

Target Values

+

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

{/* Original Value and Date in one row */} - - Original Target Value + + {translations["amp.indicatormanager:target-value"]} + defaultValue={props.values.target.originalValue} + onChange={props.handleChange} + onBlur={props.handleBlur} + name="target.originalValue" + type="number" + className={`${styles.input_field} ${(props.errors.target?.originalValue && props.touched.target?.originalValue) && styles.text_is_invalid}`} + placeholder={translations["amp.indicatormanager:enter-target-value"]} /> + {props.errors.target?.originalValue} - - - Original Value Date + + + {translations["amp.indicatormanager:target-value-date"]} { - if (value) { - props.setFieldValue("target.originalValueDate", value); - } - }} - onClear={() => { - props.setFieldValue("target.originalValueDate", null); - }} - disabled={targetOriginalValueDateDisabled} - onBlur={props.handleBlur} - name="target.originalValueDate" - className={`${styles.input_field} ${(props.errors.target?.originalValueDate && props.touched.target?.originalValueDate) && styles.text_is_invalid}`} - id="targetOriginalValueDate" - inputRef={targetOriginalValueDateRef} + translations={translations} + value={props.values.target.originalValueDate} + onChange={(value) => { + if (value) { + props.setFieldValue("target.originalValueDate", value); + } + }} + onClear={() => { + props.setFieldValue("target.originalValueDate", null); + }} + disabled={targetOriginalValueDateDisabled} + onBlur={props.handleBlur} + name="target.originalValueDate" + className={`${styles.input_field} ${(props.errors.target?.originalValueDate && props.touched.target?.originalValueDate) && styles.text_is_invalid}`} + id="targetOriginalValueDate" + inputRef={targetOriginalValueDateRef} /> - + {/* Revised Value and Date in one row */} - - Revised Target Value + + {translations["amp.indicatormanager:revised-value"]} + defaultValue={props.values.target.revisedValue} + onChange={props.handleChange} + onBlur={props.handleBlur} + name="target.revisedValue" + type="number" + className={`${styles.input_field} ${(props.errors.target?.revisedValue && props.touched.target?.revisedValue) && styles.text_is_invalid}`} + placeholder={translations["amp.indicatormanager:enter-revised-value"]} /> + {props.errors.target?.revisedValue} - - - Revised Value Date + + + + {translations["amp.indicatormanager:revised-value-date"]} { - if (value) { - props.setFieldValue("target.revisedValueDate", value); - } - }} - onClear={() => { - props.setFieldValue("target.revisedValueDate", null); - }} - 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} + translations={translations} + value={props.values.target.revisedValueDate} + onChange={(value) => { + if (value) { + props.setFieldValue("target.revisedValueDate", value); + } + }} + onClear={() => { + props.setFieldValue("target.revisedValueDate", null); + }} + 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} /> + {props.errors.target?.revisedValueDate} - +
- {/* Other Considerations - Separate Group */} + {/* Other Considerations */}
Other Considerations
From 3023409079ee5ee45fd410eb957a6aa42b0220c9 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 25 Aug 2025 10:21:00 +0300 Subject: [PATCH 038/138] AMP-31025: ADD / Edit indicator --- .../manager/IndicatorManagerService.java | 93 +++++++++++++++++-- .../indicator/manager/MEIndicatorDTO.java | 12 ++- .../module/aim/dbentity/AmpIndicator.java | 31 ++++--- 3 files changed, 108 insertions(+), 28 deletions(-) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java index 7f90fca3d29..7115600279a 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java @@ -94,11 +94,51 @@ public MEIndicatorDTO createMEIndicator(final MEIndicatorDTO indicatorRequest) { indicator.setName(indicatorRequest.getName()); indicator.setDescription(indicatorRequest.getDescription()); - indicator.setCode(indicatorRequest.getCode()); indicator.setType(indicatorRequest.isAscending() ? "A" : "D"); indicator.setCreationDate(indicatorRequest.getCreationDate()); + // Set new fields from MEIndicatorDTO + indicator.setRelevanceForClimateChange(indicatorRequest.getRelevanceForClimateChange()); + if (indicatorRequest.getIndicatorType() != null) { + AmpCategoryValue indicatorTypeCat = (AmpCategoryValue) session.get(AmpCategoryValue.class, indicatorRequest.getIndicatorType()); + indicator.setIndicatorType(indicatorTypeCat); + } else { + indicator.setIndicatorType(null); + } + indicator.setLogframeLinks(indicatorRequest.getLogframeLinks()); + indicator.setData(indicatorRequest.getData()); + indicator.setDataSource(indicatorRequest.getDataSource()); + if (indicatorRequest.getDisaggregation() != null && !indicatorRequest.getDisaggregation().isEmpty()) { + Set disaggregationCats = indicatorRequest.getDisaggregation().stream() + .map(id -> (AmpCategoryValue) session.get(AmpCategoryValue.class, id)) + .collect(Collectors.toSet()); + indicator.setDisaggregation(disaggregationCats); + } else { + indicator.setDisaggregation(new HashSet<>()); + } + if (indicatorRequest.getUnitOfMeasure() != null) { + AmpCategoryValue unitOfMeasureCat = (AmpCategoryValue) session.get(AmpCategoryValue.class, indicatorRequest.getUnitOfMeasure()); + indicator.setUnitOfMeasure(unitOfMeasureCat); + } else { + indicator.setUnitOfMeasure(null); + } + indicator.setCalculationMethod(indicatorRequest.getCalculationMethod()); + if (indicatorRequest.getResponsibleOrganizations() != null && !indicatorRequest.getResponsibleOrganizations().isEmpty()) { + List orgs = indicatorRequest.getResponsibleOrganizations().stream() + .map(id -> (AmpOrganisation) session.get(AmpOrganisation.class, id)) + .collect(Collectors.toList()); + indicator.setResponsibleOrganizations(orgs); + } else { + indicator.setResponsibleOrganizations(new java.util.ArrayList<>()); + } + if (indicatorRequest.getFrequency() != null) { + AmpCategoryValue frequencyCat = (AmpCategoryValue) session.get(AmpCategoryValue.class, indicatorRequest.getFrequency()); + indicator.setFrequency(frequencyCat); + } else { + indicator.setFrequency(null); + } + Set indicatorValues = new HashSet<>(); AmpTheme program = null; @@ -299,35 +339,73 @@ public MEIndicatorDTO updateMEIndicator(final Long indicatorId, final MEIndicato indicator.setName(indRequest.getName()); indicator.setDescription(indRequest.getDescription()); - indicator.setCode(indRequest.getCode()); indicator.setType(indRequest.isAscending() ? "A" : "D"); indicator.setCreationDate(indRequest.getCreationDate()); - AmpTheme program = null; + // Update new fields from MEIndicatorDTO + indicator.setRelevanceForClimateChange(indRequest.getRelevanceForClimateChange()); + // Convert Long indicatorType to AmpCategoryValue + if (indRequest.getIndicatorType() != null) { + AmpCategoryValue indicatorTypeCat = (AmpCategoryValue) session.get(AmpCategoryValue.class, indRequest.getIndicatorType()); + indicator.setIndicatorType(indicatorTypeCat); + } else { + indicator.setIndicatorType(null); + } + indicator.setLogframeLinks(indRequest.getLogframeLinks()); + indicator.setData(indRequest.getData()); + indicator.setDataSource(indRequest.getDataSource()); + // Convert Set disaggregation to Set + if (indRequest.getDisaggregation() != null && !indRequest.getDisaggregation().isEmpty()) { + Set disaggregationCats = indRequest.getDisaggregation().stream() + .map(id -> (AmpCategoryValue) session.get(AmpCategoryValue.class, id)) + .collect(Collectors.toSet()); + indicator.setDisaggregation(disaggregationCats); + } else { + indicator.setDisaggregation(new HashSet<>()); + } + // Convert Long unitOfMeasure to AmpCategoryValue + if (indRequest.getUnitOfMeasure() != null) { + AmpCategoryValue unitOfMeasureCat = (AmpCategoryValue) session.get(AmpCategoryValue.class, indRequest.getUnitOfMeasure()); + indicator.setUnitOfMeasure(unitOfMeasureCat); + } else { + indicator.setUnitOfMeasure(null); + } + indicator.setCalculationMethod(indRequest.getCalculationMethod()); + if (indRequest.getResponsibleOrganizations() != null && !indRequest.getResponsibleOrganizations().isEmpty()) { + List orgs = indRequest.getResponsibleOrganizations().stream() + .map(id -> (AmpOrganisation) session.get(AmpOrganisation.class, id)) + .collect(Collectors.toList()); + indicator.setResponsibleOrganizations(orgs); + } else { + indicator.setResponsibleOrganizations(new java.util.ArrayList<>()); + } + if (indRequest.getFrequency() != null) { + AmpCategoryValue frequencyCat = (AmpCategoryValue) session.get(AmpCategoryValue.class, indRequest.getFrequency()); + indicator.setFrequency(frequencyCat); + } else { + indicator.setFrequency(null); + } + AmpTheme program = null; if (indRequest.getProgramId() != null) { program = ProgramUtil.getTheme(indRequest.getProgramId()); indicator.setProgram(program); -// validateProgramSettingsAndGlobalValues(indRequest, indicator); } Set updatedValues = new HashSet<>(); - if (indRequest.getBaseValue() != null) { AmpIndicatorGlobalValue validatedBaseValues = validateBaseValues(indRequest); updatedValues.add(validatedBaseValues); indicator.getIndicatorValues().add(validatedBaseValues); indicator.getBaseValue().setIndicator(indicator); } - if (indRequest.getTargetValue() != null) { AmpIndicatorGlobalValue validatedTargetValues = validateTargetValues(indRequest); updatedValues.add(validatedTargetValues); indicator.getIndicatorValues().add(validatedTargetValues); indicator.getTargetValue().setIndicator(indicator); } - indicator.getIndicatorValues().clear(); updatedValues.forEach(value -> value.setIndicator(indicator)); indicator.getIndicatorValues().addAll(updatedValues); @@ -372,7 +450,6 @@ public MEIndicatorDTO updateMEIndicator(final Long indicatorId, final MEIndicato return new MEIndicatorDTO(indicator); } - throw new ApiRuntimeException(BAD_REQUEST, ApiError.toError("Indicator with id " + indicatorId + " not found")); } diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/MEIndicatorDTO.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/MEIndicatorDTO.java index c2b7260a14c..2f928757f38 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/MEIndicatorDTO.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/MEIndicatorDTO.java @@ -11,7 +11,9 @@ import org.digijava.kernel.ampapi.endpoints.serializers.LocalizedDateSerializer; import org.digijava.module.aim.dbentity.AmpIndicator; import org.digijava.module.aim.dbentity.AmpIndicatorGlobalValue; +import org.digijava.module.aim.dbentity.AmpOrganisation; import org.digijava.module.aim.dbentity.AmpSector; +import org.digijava.module.categorymanager.dbentity.AmpCategoryValue; import javax.validation.constraints.NotNull; import java.util.*; @@ -122,15 +124,15 @@ public MEIndicatorDTO(final AmpIndicator indicator) { this.outputId = indicator.getOutput() != null ? indicator.getOutput().getId() : null; this.outcomeId = indicator.getOutcome() != null ? indicator.getOutcome().getId() : null; this.relevanceForClimateChange = indicator.getRelevanceForClimateChange(); - this.indicatorType = indicator.getIndicatorType(); + this.indicatorType = indicator.getIndicatorType()!=null ? indicator.getIndicatorType().getId() : null; this.logframeLinks = indicator.getLogframeLinks(); this.data = indicator.getData(); this.dataSource = indicator.getDataSource(); - this.disaggregation = indicator.getDisaggregation(); - this.unitOfMeasure = indicator.getUnitOfMeasure(); + this.disaggregation = indicator.getDisaggregation()!=null ? indicator.getDisaggregation().stream().map(AmpCategoryValue::getId).collect(Collectors.toSet()) : null; + this.unitOfMeasure = indicator.getUnitOfMeasure()!=null ? indicator.getUnitOfMeasure().getId() : null; this.calculationMethod = indicator.getCalculationMethod(); - this.responsibleOrganizations = indicator.getResponsibleOrganizations(); - this.frequency = indicator.getFrequency(); + this.responsibleOrganizations = indicator.getResponsibleOrganizations()!=null ? indicator.getResponsibleOrganizations().stream().map(AmpOrganisation::getAmpOrgId).collect(Collectors.toSet()) : null; + this.frequency = indicator.getFrequency()!=null ? indicator.getFrequency().getId() : null; } public Long getId() { diff --git a/amp/src/main/java/org/digijava/module/aim/dbentity/AmpIndicator.java b/amp/src/main/java/org/digijava/module/aim/dbentity/AmpIndicator.java index 474c86c391f..26eddf98ace 100644 --- a/amp/src/main/java/org/digijava/module/aim/dbentity/AmpIndicator.java +++ b/amp/src/main/java/org/digijava/module/aim/dbentity/AmpIndicator.java @@ -9,6 +9,7 @@ import java.io.Serializable; import java.util.Date; +import java.util.List; import java.util.Set; @TranslatableClass (displayName = "Indicator") @@ -77,15 +78,15 @@ public class AmpIndicator implements Serializable, Identifiable private AmpOutput output; private String relevanceForClimateChange; - private Long indicatorType; + private AmpCategoryValue indicatorType; private Set logframeLinks; private String data; private String dataSource; - private Set disaggregation; - private Long unitOfMeasure; + private Set disaggregation; + private AmpCategoryValue unitOfMeasure; private String calculationMethod; - private Set responsibleOrganizations; - private Long frequency; + private List responsibleOrganizations; + private AmpCategoryValue frequency; public Long getIndicatorId() { return indicatorId; @@ -248,10 +249,10 @@ public String getRelevanceForClimateChange() { public void setRelevanceForClimateChange(String relevanceForClimateChange) { this.relevanceForClimateChange = relevanceForClimateChange; } - public Long getIndicatorType() { + public AmpCategoryValue getIndicatorType() { return indicatorType; } - public void setIndicatorType(Long indicatorType) { + public void setIndicatorType(AmpCategoryValue indicatorType) { this.indicatorType = indicatorType; } public Set getLogframeLinks() { @@ -272,16 +273,16 @@ public String getDataSource() { public void setDataSource(String dataSource) { this.dataSource = dataSource; } - public Set getDisaggregation() { + public Set getDisaggregation() { return disaggregation; } - public void setDisaggregation(Set disaggregation) { + public void setDisaggregation(Set disaggregation) { this.disaggregation = disaggregation; } - public Long getUnitOfMeasure() { + public AmpCategoryValue getUnitOfMeasure() { return unitOfMeasure; } - public void setUnitOfMeasure(Long unitOfMeasure) { + public void setUnitOfMeasure(AmpCategoryValue unitOfMeasure) { this.unitOfMeasure = unitOfMeasure; } public String getCalculationMethod() { @@ -290,16 +291,16 @@ public String getCalculationMethod() { public void setCalculationMethod(String calculationMethod) { this.calculationMethod = calculationMethod; } - public Set getResponsibleOrganizations() { + public List getResponsibleOrganizations() { return responsibleOrganizations; } - public void setResponsibleOrganizations(Set responsibleOrganizations) { + public void setResponsibleOrganizations(List responsibleOrganizations) { this.responsibleOrganizations = responsibleOrganizations; } - public Long getFrequency() { + public AmpCategoryValue getFrequency() { return frequency; } - public void setFrequency(Long frequency) { + public void setFrequency(AmpCategoryValue frequency) { this.frequency = frequency; } } From 02b454dcec34516340bb974277fc4c70e36e69e0 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 25 Aug 2025 11:41:52 +0300 Subject: [PATCH 039/138] AMP-31025: ADD / Edit indicator --- .../endpoints/indicator/manager/IndicatorManagerService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java index 7115600279a..f0fb4745684 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java @@ -439,6 +439,7 @@ public MEIndicatorDTO updateMEIndicator(final Long indicatorId, final MEIndicato } session.update(indicator); + session.flush(); if (program != null) { try { IndicatorUtil.assignIndicatorToTheme(program, indicator); From 0a87a4e4ef29c918c911862d5a559006eda2811f Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 25 Aug 2025 11:50:03 +0300 Subject: [PATCH 040/138] AMP-31025: ADD / Edit indicator --- .../manager/IndicatorManagerService.java | 1 - .../module/aim/dbentity/AmpIndicator.hbm.xml | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java index f0fb4745684..7115600279a 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java @@ -439,7 +439,6 @@ public MEIndicatorDTO updateMEIndicator(final Long indicatorId, final MEIndicato } session.update(indicator); - session.flush(); if (program != null) { try { IndicatorUtil.assignIndicatorToTheme(program, indicator); diff --git a/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpIndicator.hbm.xml b/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpIndicator.hbm.xml index 42a747fb2a7..dd6c168fc4c 100644 --- a/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpIndicator.hbm.xml +++ b/amp/src/main/resources/org/digijava/module/aim/dbentity/AmpIndicator.hbm.xml @@ -51,5 +51,22 @@ + + + + + + + + + + + + + + + + + From 510f2e6cb56d5788d934fe6f8ac28b4ba3198eb6 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 25 Aug 2025 12:31:49 +0300 Subject: [PATCH 041/138] AMP-31025: ADD / Edit indicator --- .../indicator/manager/IndicatorManagerService.java | 12 ++++++------ .../digijava/module/aim/dbentity/AmpIndicator.java | 7 +++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java index 7115600279a..fd9507674f6 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/manager/IndicatorManagerService.java @@ -125,12 +125,12 @@ public MEIndicatorDTO createMEIndicator(final MEIndicatorDTO indicatorRequest) { } indicator.setCalculationMethod(indicatorRequest.getCalculationMethod()); if (indicatorRequest.getResponsibleOrganizations() != null && !indicatorRequest.getResponsibleOrganizations().isEmpty()) { - List orgs = indicatorRequest.getResponsibleOrganizations().stream() + Set orgs = indicatorRequest.getResponsibleOrganizations().stream() .map(id -> (AmpOrganisation) session.get(AmpOrganisation.class, id)) - .collect(Collectors.toList()); + .collect(Collectors.toSet()); indicator.setResponsibleOrganizations(orgs); } else { - indicator.setResponsibleOrganizations(new java.util.ArrayList<>()); + indicator.setResponsibleOrganizations(new java.util.HashSet<>()); } if (indicatorRequest.getFrequency() != null) { AmpCategoryValue frequencyCat = (AmpCategoryValue) session.get(AmpCategoryValue.class, indicatorRequest.getFrequency()); @@ -373,12 +373,12 @@ public MEIndicatorDTO updateMEIndicator(final Long indicatorId, final MEIndicato } indicator.setCalculationMethod(indRequest.getCalculationMethod()); if (indRequest.getResponsibleOrganizations() != null && !indRequest.getResponsibleOrganizations().isEmpty()) { - List orgs = indRequest.getResponsibleOrganizations().stream() + Set orgs = indRequest.getResponsibleOrganizations().stream() .map(id -> (AmpOrganisation) session.get(AmpOrganisation.class, id)) - .collect(Collectors.toList()); + .collect(Collectors.toSet()); indicator.setResponsibleOrganizations(orgs); } else { - indicator.setResponsibleOrganizations(new java.util.ArrayList<>()); + indicator.setResponsibleOrganizations(new java.util.HashSet<>()); } if (indRequest.getFrequency() != null) { AmpCategoryValue frequencyCat = (AmpCategoryValue) session.get(AmpCategoryValue.class, indRequest.getFrequency()); diff --git a/amp/src/main/java/org/digijava/module/aim/dbentity/AmpIndicator.java b/amp/src/main/java/org/digijava/module/aim/dbentity/AmpIndicator.java index 26eddf98ace..2a69f6d49d4 100644 --- a/amp/src/main/java/org/digijava/module/aim/dbentity/AmpIndicator.java +++ b/amp/src/main/java/org/digijava/module/aim/dbentity/AmpIndicator.java @@ -9,7 +9,6 @@ import java.io.Serializable; import java.util.Date; -import java.util.List; import java.util.Set; @TranslatableClass (displayName = "Indicator") @@ -85,7 +84,7 @@ public class AmpIndicator implements Serializable, Identifiable private Set disaggregation; private AmpCategoryValue unitOfMeasure; private String calculationMethod; - private List responsibleOrganizations; + private Set responsibleOrganizations; private AmpCategoryValue frequency; public Long getIndicatorId() { @@ -291,10 +290,10 @@ public String getCalculationMethod() { public void setCalculationMethod(String calculationMethod) { this.calculationMethod = calculationMethod; } - public List getResponsibleOrganizations() { + public Set getResponsibleOrganizations() { return responsibleOrganizations; } - public void setResponsibleOrganizations(List responsibleOrganizations) { + public void setResponsibleOrganizations(Set responsibleOrganizations) { this.responsibleOrganizations = responsibleOrganizations; } public AmpCategoryValue getFrequency() { From e94cccb2f6801f20caeab7a45ce5813e7d37f559 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 25 Aug 2025 13:11:47 +0300 Subject: [PATCH 042/138] AMP-31025: ADD / Edit indicator --- .../modals/AddNewIndicatorModal.tsx | 736 +++++++------- .../components/modals/EditIndicatorModal.tsx | 902 +++++++++--------- .../pages/OutcomeOutputManagementPage.tsx | 51 +- .../manager/AmpOutcomeOutputEndpoints.java | 25 +- .../service/AmpOutcomeOutputService.java | 90 +- 5 files changed, 963 insertions(+), 841 deletions(-) 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 e72b0528af7..625d5bd18c8 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 @@ -386,354 +386,363 @@ const AddNewIndicatorModal: React.FC = (props) => {
{/* Core Indicator Information */} -
{translations["amp.indicatormanager:core-info"]}
- - - {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:relevance-for-climate-change"]} - - - - - - Type - ({ value: outcome.id, label: outcome.name }))} - onChange={(selectedValue) => { - setSelectedOutcomeId(selectedValue?.value ?? null); - props.setFieldValue('outcomeId', selectedValue?.value); - }} - onBlur={props.handleBlur} - className={`basic-multi-select ${(props.errors.outcomeId && props.touched.outcomeId) && styles.text_is_invalid}`} - classNamePrefix="select" - value={allOutcomes.find(outcome => outcome.id === selectedOutcomeId) ? { value: selectedOutcomeId, label: allOutcomes.find(outcome => outcome.id === selectedOutcomeId)?.name } : null} - /> - - - {translations["amp.indicatormanager:output"]} - { - if (selectedValue) { - handleProgramSchemeChange(selectedValue.value, props); - } - }} - isClearable - getOptionValue={(option) => option.value} - onBlur={props.handleBlur} - className={`basic-multi-select ${styles.input_field}`} - classNamePrefix="select" - /> - - {programFieldVisible && ( - - Program - { - const selectedValues = values.map((value: any) => parseInt(value.value)) - props.setFieldValue('sectors', selectedValues); - }} - onBlur={props.handleBlur} - className={`basic-multi-select ${(props.errors.sectors && props.touched.sectors) && styles.text_is_invalid}`} - classNamePrefix="select" - /> - - - {/* Data Definition and Sourcing */} -
{translations["amp.indicatormanager:data-definition-sourcing-info"] || "Data Definition and Sourcing"}
- - - {translations["amp.indicatormanager:data"]} - - - - {translations["amp.indicatormanager:data-source"]} - - - - - - Disaggregation - { - 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 - - - - {/* Responsibility and Frequency */} -
Responsibility and Frequency
- - - Responsible Organization(s) - { - props.setFieldValue('frequency', selectedValue?.value); - }} - onBlur={props.handleBlur} - className={`basic-multi-select ${(props.errors.frequency && props.touched.frequency) && styles.text_is_invalid}`} - classNamePrefix="select" - value={frequencyOptions.find(opt => opt.value === props.values.frequency) || null} - /> - - - {/* Value Tracking */} -
Value Tracking
- - -

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

-
- {/* Original Value and Date in one row */} +
{translations["amp.indicatormanager:core-info"]}
+
- - {translations['amp.indicatormanager:original-value']} + + {translations["amp.indicatormanager:indicator-name"]} - + name="name" + className={`${styles.input_field} ${(props.errors.name && props.touched.name) && styles.text_is_invalid}`} + isInvalid={!!props.errors.name} + required + aria-required type="text" + placeholder={translations["amp.indicatormanager:enter-indicator-name"]} + /> - {props.errors.base?.originalValue} + {props.errors.name} - - - {translations["amp.indicatormanager:original-value-date"]} - { - if (value) { - props.setFieldValue('base.originalValueDate', value); - } - }} - onClear={() => { - props.setFieldValue('base.originalValueDate', null); - }} + + {translations["amp.indicatormanager:indicator-code"]} + - + name="code" + required + type="text" + className={`${styles.input_field} ${(props.errors.code && props.touched.code) && styles.text_is_invalid}`} + placeholder={translations["amp.indicatormanager:enter-indicator-code"]} + /> - {props.errors.base?.originalValueDate} + {props.errors.code} - {/* Revised Value and Date in one row */} - - {translations["amp.indicatormanager:revised-value"]} + + {translations["amp.indicatormanager:indicator-description"]} - + name="description" + as="textarea" + rows={2} + className={`${styles.input_field} ${(props.errors.description && props.touched.description) && styles.text_is_invalid}`} + placeholder={translations["amp.indicatormanager:enter-indicator-description"]} + /> - {props.errors.base?.revisedValue} + {props.errors.description} - - - {translations['amp.indicatormanager:revised-value-date']} - { - if (value) { - props.setFieldValue('base.revisedValueDate', value); + + + + {translations["amp.indicatormanager:relevance-for-climate-change"]} + + + + + + Type + ({ value: outcome.id, label: outcome.name }))} + onChange={(selectedValue) => { + setSelectedOutcomeId(selectedValue?.value ?? null); + props.setFieldValue('outcomeId', selectedValue?.value); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.outcomeId && props.touched.outcomeId) && styles.text_is_invalid}`} + classNamePrefix="select" + value={allOutcomes.find(outcome => outcome.id === selectedOutcomeId) ? { value: selectedOutcomeId, label: allOutcomes.find(outcome => outcome.id === selectedOutcomeId)?.name } : null} + /> + + + {translations["amp.indicatormanager:output"]} + { + if (selectedValue) { + handleProgramSchemeChange(selectedValue.value, props); } }} - onClear={() => { - props.setFieldValue('base.revisedValueDate', null); + isClearable + getOptionValue={(option) => option.value} + onBlur={props.handleBlur} + className={`basic-multi-select ${styles.input_field}`} + classNamePrefix="select" + /> + + {programFieldVisible && ( + + Program + { + const selectedValues = values.map((value: any) => parseInt(value.value)) + props.setFieldValue('sectors', selectedValues); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.sectors && props.touched.sectors) && styles.text_is_invalid}`} + classNamePrefix="select" + /> + + +
+ {/* Data Definition and Sourcing */} +
{translations["amp.indicatormanager:data-definition-sourcing-info"] || "Data Definition and Sourcing"}
+
+ + + {translations["amp.indicatormanager:data"]} + + + + {translations["amp.indicatormanager:data-source"]} + + + + + + Disaggregation + { + 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 + + + +
+ {/* Responsibility and Frequency */} +
Responsibility and Frequency
+
+ + + Responsible Organization(s) + { + props.setFieldValue('frequency', selectedValue?.value); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.frequency && props.touched.frequency) && styles.text_is_invalid}`} + classNamePrefix="select" + value={frequencyOptions.find(opt => opt.value === props.values.frequency) || null} + /> + + +
+ {/* Value Tracking */} +
Value Tracking
+
+ + +

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

+
+ {/* Original Value and Date in one row */} + + + {translations['amp.indicatormanager:original-value']} + + + + {props.errors.base?.originalValue} + + + + + {translations["amp.indicatormanager:original-value-date"]} + { + if (value) { + props.setFieldValue('base.originalValueDate', value); + } + }} + onClear={() => { + props.setFieldValue('base.originalValueDate', null); + }} + onBlur={props.handleBlur} + disabled={baseOriginalValueDateDisabled} + className={`${styles.input_field} ${(props.errors.base?.originalValueDate && props.touched.base?.originalValueDate) && styles.text_is_invalid}`}/> + + + {props.errors.base?.originalValueDate} + + + + {/* Revised Value and Date in one row */} + + + {translations["amp.indicatormanager:revised-value"]} + + + + {props.errors.base?.revisedValue} + + + + + {translations['amp.indicatormanager:revised-value-date']} + { + if (value) { + props.setFieldValue('base.revisedValueDate', value); + } + }} + onClear={() => { + props.setFieldValue('base.revisedValueDate', null); }} onBlur={props.handleBlur} name="base.revisedValueDate" @@ -830,41 +839,44 @@ const AddNewIndicatorModal: React.FC = (props) => {
{/* Other Considerations */} -
Other Considerations
- - - {translations["amp.indicatormanager:ascending"]} - { + if (value) props.setFieldValue('ascending', value.value) + }} + defaultValue={{ + value: false, + label: translations["amp.indicatormanager:true"] + }} + /> + + {props.errors.ascending} + + + + + {translations["amp.indicatormanager:table-header-creation-date"]} + + + +
+
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 71efcecd8f6..9b52d705a10 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 @@ -469,325 +469,334 @@ const EditIndicatorModal: React.FC = (props) => {
{/* Core Indicator Information */} -
{translations["amp.indicatormanager:core-info"]}
- - - {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:relevance-for-climate-change"]} - - - - - - Type - { + 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) - ({ value: outcome.id, label: outcome.name }))} onChange={(selectedValue) => { - props.setFieldValue("programId", selectedValue?.value); + setSelectedOutcomeId(selectedValue?.value ?? null); + props.setFieldValue('outcomeId', selectedValue?.value); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.outcomeId && props.touched.outcomeId) && styles.text_is_invalid}`} + classNamePrefix="select" + value={allOutcomes.find(outcome => outcome.id === selectedOutcomeId) ? { value: selectedOutcomeId, label: allOutcomes.find(outcome => outcome.id === selectedOutcomeId)?.name } : null} + /> + + + {translations["amp.indicatormanager:output"]} + { + if (selectedValue) { + setDefaultProgramScheme(selectedValue); + handleProgramSchemeChange(selectedValue.value, props); + } }} 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}`} + className={`basic-multi-select ${styles.input_field}`} classNamePrefix="select" - defaultValue={defaultProgram} + value={defaultProgramScheme} /> - )} - - - - Sector - { + 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('disaggregation', selectedValues.map((v: any) => v.value)); - }} - onBlur={props.handleBlur} - className={`basic-multi-select ${(props.errors.disaggregation && props.touched.disaggregation) && styles.text_is_invalid}`} - classNamePrefix="select" - value={disaggregationOptions.filter(opt => props.values.disaggregation?.includes(opt.value))} - /> - - - Unit of Measure - { - 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 - { + props.setFieldValue('disaggregation', selectedValues.map((v: any) => v.value)); + }} + onBlur={props.handleBlur} + className={`basic-multi-select ${(props.errors.disaggregation && props.touched.disaggregation) && styles.text_is_invalid}`} + classNamePrefix="select" + value={disaggregationOptions.filter(opt => props.values.disaggregation?.includes(opt.value))} + /> + + + Unit of Measure + { + 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} - - - {/* Other Considerations */} -
Other Considerations
- - - {translations["amp.indicatormanager:ascending"]} - setSelectedOutcome(opt?.value || 0)} + className={styles.filter_select} + components={{ IndicatorSeparator: () => null }} + /> +
+ {/* Output filter */} +
+ {translations['amp.indicatormanager:output']} + setSelectedIndicatorType(opt?.value || 0)} + className={styles.filter_select} + components={{ IndicatorSeparator: () => null }} + /> +
+
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 48169fe59ca..e642af8f6a7 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", @@ -32,7 +32,6 @@ "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", @@ -165,5 +164,11 @@ "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: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" } 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 index fdacf881be5..9c0481be94c 100644 --- 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 @@ -33,7 +33,7 @@ const OutcomeOutputManagementPage: React.FC = () => { const [showEditOutcomeModal, setShowEditOutcomeModal] = useState(false); const [editingOutcome, setEditingOutcome] = useState(null); const [outcomes, setOutcomes] = useState([]); - + const navigate = useNavigate(); useEffect(() => { fetch('/rest/amp-outcome-output/outcomes') @@ -52,21 +52,26 @@ const OutcomeOutputManagementPage: React.FC = () => { text: 'Actions', formatter: (_: any, row: Outcome) => ( <> - + handleEditOutcome(row)} + style={{ fontSize: 20, color: '#198754' }} + className="fa fa-pencil" + aria-hidden="true" + /> + {' '} - +