From 4ea87a9854352ad0f5a62dc51ea7c95e2d7068a4 Mon Sep 17 00:00:00 2001
From: Remco Tukker <remco.tukker@geant.org>
Date: Tue, 25 Apr 2023 21:01:07 +0200
Subject: [PATCH] first version of organisation pages

---
 webapp/src/App.tsx                      |   4 +
 webapp/src/Schema.tsx                   |   9 +-
 webapp/src/helpers/dataconversion.tsx   |  22 ++++-
 webapp/src/pages/CompendiumData.tsx     |  12 ++-
 webapp/src/pages/ParentOrganisation.tsx | 116 +++++++++++++++++++++++
 webapp/src/pages/SubOrganisation.tsx    | 118 ++++++++++++++++++++++++
 6 files changed, 277 insertions(+), 4 deletions(-)
 create mode 100644 webapp/src/pages/ParentOrganisation.tsx
 create mode 100644 webapp/src/pages/SubOrganisation.tsx

diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx
index 95e7bf4a..316b04dc 100644
--- a/webapp/src/App.tsx
+++ b/webapp/src/App.tsx
@@ -10,6 +10,8 @@ import FundingSourcePage from "./pages/FundingSource";
 import ChargingStructurePage from "./pages/ChargingStructure";
 import StaffGraph from "./pages/StaffGraph";
 import { FilterSelection } from "./Schema";
+import SubOrganisation from "./pages/SubOrganisation";
+import ParentOrganisation from "./pages/ParentOrganisation";
 
 
 function App(): ReactElement {
@@ -31,6 +33,8 @@ function App(): ReactElement {
           <Route path="/data/employment" element={<StaffGraph filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} />
           <Route path="/data/roles" element={<StaffGraph roles filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} />
           <Route path="/charging" element={<ChargingStructurePage />} />
+          <Route path="/suborganisations" element={<SubOrganisation filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} />
+          <Route path="/parentorganisation" element={<ParentOrganisation filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} />
           <Route path="*" element={<Landing />} />
         </Routes>
         <GeantFooter />
diff --git a/webapp/src/Schema.tsx b/webapp/src/Schema.tsx
index 4d3a935d..449752b6 100644
--- a/webapp/src/Schema.tsx
+++ b/webapp/src/Schema.tsx
@@ -122,4 +122,11 @@ export interface NrenStaffDataset {
         stack: string
         hidden: boolean
     }[]
-}
\ No newline at end of file
+}
+
+export interface Organisation {
+    nren: string,
+    year: number,
+    name: string,
+    role?: string
+}
diff --git a/webapp/src/helpers/dataconversion.tsx b/webapp/src/helpers/dataconversion.tsx
index a40258cf..62d6e31a 100644
--- a/webapp/src/helpers/dataconversion.tsx
+++ b/webapp/src/helpers/dataconversion.tsx
@@ -3,7 +3,7 @@ import {
     FundingSource,
     FundingSourceDataset,
     ChargingStructure,
-    ChargingStructureDataset, Budget, BasicDataset, NrenStaff, NrenStaffDataset
+    ChargingStructureDataset, Budget, BasicDataset, NrenStaff, NrenStaffDataset, Organisation
 } from "../Schema";
 
 const DEFAULT_CHARGING_STRUCTURE_DATA = [
@@ -244,6 +244,26 @@ export function createChargingStructureDataset(chargingStructureData: ChargingSt
 
 }
 
+export function createOrganisationDataLookup(organisationEntries: Organisation[]) {
+    const nrenMap = new Map<string, Map<number, Organisation[]>>();
+
+    organisationEntries.forEach(entry => {
+        let nrenEntry = nrenMap.get(entry.nren);
+        if (!nrenEntry) {
+            nrenEntry = new Map<number, Organisation[]>();
+        }
+        let suborgList = nrenEntry.get(entry.year);
+        if (!suborgList) {
+            suborgList = [];
+        }
+        suborgList.push(entry);
+        nrenEntry.set(entry.year, suborgList);
+        nrenMap.set(entry.nren, nrenEntry);
+    });
+    return nrenMap;
+}
+
+
 export const createNRENStaffDataset = (data: NrenStaff[], roles: boolean) => {
     function CreateDataLookup(data: NrenStaff[]) {
         const dataLookup = new Map<string, Map<string, number>>();
diff --git a/webapp/src/pages/CompendiumData.tsx b/webapp/src/pages/CompendiumData.tsx
index 330196e3..998d8e43 100644
--- a/webapp/src/pages/CompendiumData.tsx
+++ b/webapp/src/pages/CompendiumData.tsx
@@ -56,8 +56,16 @@ function CompendiumData(): ReactElement {
                                         <span>Types of employment for NRENs</span>
                                     </Link>
                                 </Row>
-
-
+                                <Row>
+                                    <Link to="/suborganisations" className="link-text-underline">
+                                        <span>NREN Suborganisations</span>
+                                    </Link>
+                                </Row>
+                                <Row>
+                                    <Link to="/parentorganisation" className="link-text-underline">
+                                        <span>NREN Parent Organisations</span>
+                                    </Link>
+                                </Row>
                             </div>
                         </CollapsibleBox>
                     </div>
diff --git a/webapp/src/pages/ParentOrganisation.tsx b/webapp/src/pages/ParentOrganisation.tsx
new file mode 100644
index 00000000..725428e5
--- /dev/null
+++ b/webapp/src/pages/ParentOrganisation.tsx
@@ -0,0 +1,116 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Col, Container, Row, Table } from "react-bootstrap";
+import { createOrganisationDataLookup } from "../helpers/dataconversion";
+import Filter from "../components/graphing/Filter"
+
+import {
+    Organisation,
+    FilterSelection
+} from "../Schema";
+
+async function getData(): Promise<Organisation[]> {
+    try {
+        const response = await fetch('/api/organization/parent');
+        return response.json();
+    } catch (error) {
+        console.error(`Failed to load data: ${error}`);
+        throw error;
+    }
+}
+
+function getYearsAndNrens(sourceData: Organisation[]) {
+    const years = new Set<number>();
+    const nrens = new Set<string>();
+    sourceData.forEach(datapoint => {
+        years.add(datapoint.year);
+        nrens.add(datapoint.nren);
+    });
+    return { years: years, nrens: nrens };
+}
+
+interface inputProps {
+    filterSelection: FilterSelection
+    setFilterSelection: React.Dispatch<React.SetStateAction<FilterSelection>>
+}
+
+function ParentOrganisation({ filterSelection, setFilterSelection }: inputProps) {
+    const [organisationData, setOrganisationData] = useState<Organisation[]>();
+
+    const { years, nrens } = useMemo(
+        () => getYearsAndNrens(organisationData || []),
+        [organisationData]
+    );
+
+    const selectedData = (organisationData || []).filter(data =>
+        filterSelection.selectedYears.includes(data.year) && filterSelection.selectedNrens.includes(data.nren)
+    );
+    const organisationDataset = createOrganisationDataLookup(selectedData);
+
+    useEffect(() => {
+        const loadData = async () => {
+            const data = await getData();
+            setOrganisationData(data);
+
+            // filter fallback for when nothing is selected (only last year for all nrens)
+            const { years, nrens } = getYearsAndNrens(data);
+            setFilterSelection(previous => {
+                const visibleYears = previous.selectedYears.filter(year => years.has(year));
+                const newSelectedYears = visibleYears.length ? previous.selectedYears : [Math.max(...years)];
+                const visibleNrens = previous.selectedNrens.filter(nren => nrens.has(nren));
+                const newSelectedNrens = visibleNrens.length ? previous.selectedNrens : [...nrens];
+                return { selectedYears: newSelectedYears, selectedNrens: newSelectedNrens };
+            });
+        }
+        loadData()
+    }, [setFilterSelection]);
+
+    function sortOrganisation(a: Organisation, b: Organisation) {
+        return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
+    }
+
+    return (
+        <Container>
+            <Row>
+                <Col xs={9}>
+                    <Row>
+                        <h3>NREN Parent Organisations</h3>
+                    </Row>
+                    <Row>
+                        <Table>
+                            <thead>
+                                <tr>
+                                    <th>NREN</th>
+                                    <th>Year</th>
+                                    <th>Parent Organisation</th>
+                                </tr>
+                            </thead>
+                            <tbody>
+                                {Array.from(organisationDataset).sort().map(([_, nrenEntry]) => (
+                                    Array.from(nrenEntry).sort().map(([_, nrenDataList], yearIndex) => (
+                                        nrenDataList.sort(sortOrganisation).map((nrenData, nrenDataIndex) => (
+                                            <tr key={nrenData.nren + nrenData.year + nrenData.name}>
+                                                <td>{yearIndex == 0 && nrenDataIndex == 0 && nrenData.nren}</td>
+                                                <td>{nrenDataIndex == 0 && nrenData.year}</td>
+                                                <td>{nrenData.name}</td>
+                                            </tr>
+                                        ))
+                                    )
+                                )
+
+                                ))}
+                            </tbody>
+                        </Table>
+                    </Row>
+                </Col>
+                <Col xs={3}>
+                    <Filter
+                        filterOptions={{ availableYears: [...years], availableNrens: [...nrens] }}
+                        filterSelection={filterSelection}
+                        setFilterSelection={setFilterSelection}
+                    />
+                </Col>
+            </Row>
+        </Container>
+    );
+}
+export default ParentOrganisation;
diff --git a/webapp/src/pages/SubOrganisation.tsx b/webapp/src/pages/SubOrganisation.tsx
new file mode 100644
index 00000000..6bc2602b
--- /dev/null
+++ b/webapp/src/pages/SubOrganisation.tsx
@@ -0,0 +1,118 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Col, Container, Row, Table } from "react-bootstrap";
+import { createOrganisationDataLookup } from "../helpers/dataconversion";
+import Filter from "../components/graphing/Filter"
+
+import {
+    Organisation,
+    FilterSelection
+} from "../Schema";
+
+async function getData(): Promise<Organisation[]> {
+    try {
+        const response = await fetch('/api/organization/sub');
+        return response.json();
+    } catch (error) {
+        console.error(`Failed to load data: ${error}`);
+        throw error;
+    }
+}
+
+function getYearsAndNrens(sourceData: Organisation[]) {
+    const years = new Set<number>();
+    const nrens = new Set<string>();
+    sourceData.forEach(datapoint => {
+        years.add(datapoint.year);
+        nrens.add(datapoint.nren);
+    });
+    return { years: years, nrens: nrens };
+}
+
+interface inputProps {
+    filterSelection: FilterSelection
+    setFilterSelection: React.Dispatch<React.SetStateAction<FilterSelection>>
+}
+
+function SubOrganisation({ filterSelection, setFilterSelection }: inputProps) {
+    const [organisationData, setOrganisationData] = useState<Organisation[]>();
+
+    const { years, nrens } = useMemo(
+        () => getYearsAndNrens(organisationData || []),
+        [organisationData]
+    );
+
+    const selectedData = (organisationData || []).filter(data =>
+        filterSelection.selectedYears.includes(data.year) && filterSelection.selectedNrens.includes(data.nren)
+    );
+    const organisationDataset = createOrganisationDataLookup(selectedData);
+
+    useEffect(() => {
+        const loadData = async () => {
+            const data = await getData();
+            setOrganisationData(data);
+
+            // filter fallback for when nothing is selected (only last year for all nrens)
+            const { years, nrens } = getYearsAndNrens(data);
+            setFilterSelection(previous => {
+                const visibleYears = previous.selectedYears.filter(year => years.has(year));
+                const newSelectedYears = visibleYears.length ? previous.selectedYears : [Math.max(...years)];
+                const visibleNrens = previous.selectedNrens.filter(nren => nrens.has(nren));
+                const newSelectedNrens = visibleNrens.length ? previous.selectedNrens : [...nrens];
+                return { selectedYears: newSelectedYears, selectedNrens: newSelectedNrens };
+            });
+        }
+        loadData()
+    }, [setFilterSelection]);
+
+    function sortOrganisation(a: Organisation, b: Organisation) {
+        return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
+    }
+
+    return (
+        <Container>
+            <Row>
+                <Col xs={9}>
+                    <Row>
+                        <h3>NREN Suborganisations</h3>
+                    </Row>
+                    <Row>
+                        <Table>
+                            <thead>
+                                <tr>
+                                    <th>NREN</th>
+                                    <th>Year</th>
+                                    <th>Suborganisation</th>
+                                    <th>Role</th>
+                                </tr>
+                            </thead>
+                            <tbody>
+                                {Array.from(organisationDataset).sort().map(([_, nrenEntry]) => (
+                                    Array.from(nrenEntry).sort().map(([_, nrenDataList], yearIndex) => (
+                                        nrenDataList.sort(sortOrganisation).map((nrenData, nrenDataIndex) => (
+                                            <tr key={nrenData.nren + nrenData.year + nrenData.name}>
+                                                <td>{yearIndex == 0 && nrenDataIndex == 0 && nrenData.nren}</td>
+                                                <td>{nrenDataIndex == 0 && nrenData.year}</td>
+                                                <td>{nrenData.name}</td>
+                                                <td>{nrenData.role}</td>
+                                            </tr>
+                                        ))
+                                    )
+                                )
+
+                                ))}
+                            </tbody>
+                        </Table>
+                    </Row>
+                </Col>
+                <Col xs={3}>
+                    <Filter
+                        filterOptions={{ availableYears: [...years], availableNrens: [...nrens] }}
+                        filterSelection={filterSelection}
+                        setFilterSelection={setFilterSelection}
+                    />
+                </Col>
+            </Row>
+        </Container>
+    );
+}
+export default SubOrganisation;
-- 
GitLab