mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33c0aa71d9 | ||
|
|
2e4572c871 | ||
|
|
f6b14777fe | ||
|
|
f251f9456e | ||
|
|
f364eb4f67 | ||
|
|
4896f9e458 | ||
|
|
096535e470 | ||
|
|
7c949b47a7 | ||
|
|
c3b7d96b06 | ||
|
|
9a3783bc45 | ||
|
|
4882d1d9d3 | ||
|
|
9575d86d9c | ||
|
|
85c7e86ed9 | ||
|
|
d527c6ba5f | ||
|
|
6d5b91782c | ||
|
|
b8762aae34 | ||
|
|
4da78df1e6 | ||
|
|
93258ad12a | ||
|
|
0028b69661 | ||
|
|
ef902b1c0c | ||
|
|
d8e3ee092f | ||
|
|
8763b1868e | ||
|
|
700d799b00 | ||
|
|
b0ecc3d4f5 | ||
|
|
e681d5a94f | ||
|
|
980cd8ecf0 | ||
|
|
357c0b3f44 | ||
|
|
6467f5046e | ||
|
|
21ff440ebd | ||
|
|
73228fb26f | ||
|
|
2c1807fe10 | ||
|
|
50091a3098 | ||
|
|
3ba7965402 | ||
|
|
067a53a072 | ||
|
|
9a3956832d | ||
|
|
831b638c99 | ||
|
|
38c343b8d2 | ||
|
|
c05346ad9a | ||
|
|
dd12cd3626 | ||
|
|
55eebe32a6 | ||
|
|
8099dff1b6 | ||
|
|
1528ff3d80 |
@@ -64,6 +64,7 @@ describe('Population Statistics tests', () => {
|
||||
|
||||
it('Population statistics is usable on general level', () => {
|
||||
cy.visit(pathToCSBach2017)
|
||||
cy.cs('filtered-students')
|
||||
setPopStatsUntil('toukokuu 2020')
|
||||
|
||||
cy.get('.card').within(() => {
|
||||
@@ -74,7 +75,8 @@ describe('Population Statistics tests', () => {
|
||||
cy.contains('Excludes students with non-degree study right')
|
||||
cy.contains('Excludes students who have transferred out of this programme')
|
||||
})
|
||||
cy.contains('Courses of population').click({ force: true })
|
||||
cy.cs('filtered-students')
|
||||
cy.contains('Courses of population').click()
|
||||
|
||||
cy.intercept('/api/v3/courseyearlystats**').as('coursePage')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
@@ -165,7 +167,9 @@ describe('Population Statistics tests', () => {
|
||||
|
||||
it('Credit Statistics, Credits Gained tab shows stats by admissions', () => {
|
||||
cy.visit(pathToCSMaster2019)
|
||||
cy.contains('Credit statistics').click()
|
||||
cy.cs('filtered-students')
|
||||
cy.contains('Credit statistics')
|
||||
cy.get("[data-cy='credit-statistics']").click()
|
||||
cy.get('.credits-gained-divider').click()
|
||||
cy.get("[data-cy='credits-gained-table-Avoin väylä']").should('exist')
|
||||
cy.get("[data-cy='credits-gained-table-Yhteispisteet']").should('exist')
|
||||
@@ -175,7 +179,7 @@ describe('Population Statistics tests', () => {
|
||||
|
||||
it('Credit Statistics, Statistics pane works', () => {
|
||||
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('Credit statistics').click()
|
||||
cy.contains('Credit statistics').click({ force: true })
|
||||
cy.get("[data-cy='credit-stats-tab'] > .menu > :nth-child(2)").click()
|
||||
|
||||
cy.get("[data-cy='credit-stats-table-name-header']").should('contain', 'Statistic for n = 149 Students')
|
||||
|
||||
Generated
+3
-3
@@ -3353,9 +3353,9 @@
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
},
|
||||
"validator": {
|
||||
"version": "13.6.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.6.0.tgz",
|
||||
"integrity": "sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg=="
|
||||
"version": "13.7.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
|
||||
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw=="
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2",
|
||||
|
||||
@@ -368,39 +368,9 @@ const byNameAndOrCodeLike = async (name, code) => {
|
||||
})
|
||||
|
||||
const courses = rawCourses
|
||||
.map(
|
||||
({
|
||||
id,
|
||||
code,
|
||||
name,
|
||||
latest_instance_date,
|
||||
is_study_module,
|
||||
coursetypecode,
|
||||
startdate,
|
||||
max_attainment_date,
|
||||
min_attainment_date,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
substitutions,
|
||||
organizations,
|
||||
}) => {
|
||||
return {
|
||||
id,
|
||||
code,
|
||||
name,
|
||||
latest_instance_date,
|
||||
is_study_module,
|
||||
coursetypecode,
|
||||
startdate,
|
||||
max_attainment_date,
|
||||
min_attainment_date,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
substitutions,
|
||||
organizations: organizations.map(o => o.id),
|
||||
}
|
||||
}
|
||||
)
|
||||
.map(course => {
|
||||
return { ...course.dataValues, organizations: course.organizations.map(o => o.id) }
|
||||
})
|
||||
.sort(a => (a.code.match(/^[A-Za-z]{3}[0-9]{1}/) ? -1 : 1))
|
||||
|
||||
let substitutionGroupIndex = 0
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
@@ -30,7 +31,7 @@
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/prefer-stateless-function": "error",
|
||||
"import/no-unresolved": "error",
|
||||
"import/no-unresolved": "off",
|
||||
"react/jsx-no-bind": "error",
|
||||
"prefer-object-spread/prefer-object-spread": "error",
|
||||
"babel/no-invalid-this": "error",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Generated
+10
-10
@@ -3160,11 +3160,11 @@
|
||||
"integrity": "sha512-99FZt8qS/xukgxU/8daV8WT7wAakqBzt6lF3XCweO6pwcf50/NgxxBj6ZC7/ejR+F4PWeFpkb9lAMH3y2quBXA=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
|
||||
"version": "0.21.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.2.tgz",
|
||||
"integrity": "sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.10.0"
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"axobject-query": {
|
||||
@@ -14919,9 +14919,9 @@
|
||||
"integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA=="
|
||||
},
|
||||
"tar": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
|
||||
"integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==",
|
||||
"version": "6.1.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
|
||||
"integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
|
||||
"requires": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
@@ -15143,9 +15143,9 @@
|
||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
|
||||
},
|
||||
"tmpl": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
|
||||
"integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE="
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="
|
||||
},
|
||||
"to-arraybuffer": {
|
||||
"version": "1.0.1",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"@reduxjs/toolkit": "^1.6.2",
|
||||
"@sentry/browser": "^6.9.0",
|
||||
"@sentry/tracing": "^6.11.0",
|
||||
"axios": "^0.21.1",
|
||||
"axios": "^0.21.2",
|
||||
"formik": "^2.2.9",
|
||||
"highcharts": "^9.1.2",
|
||||
"highcharts-custom-events": "^3.0.10",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import axios from 'axios'
|
||||
import * as Sentry from '@sentry/browser'
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||
import { getMocked, setMocking } from '../common'
|
||||
import { apiBasePath, isDev } from '../conf'
|
||||
import { loginPrefix, logoutPrefix } from '../redux/auth'
|
||||
|
||||
const getHeaders = () => {
|
||||
export const getHeaders = () => {
|
||||
// Set up dev user for development environment, mimicking production admin user
|
||||
const devUserHeaders = {
|
||||
uid: 'mluukkai',
|
||||
@@ -140,3 +141,23 @@ export const handleAuth = store => next => async action => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redux-toolkit query based API
|
||||
// Api can be extended with enhanceEndpoints method
|
||||
// All tags used for invalidating cache must be defined here
|
||||
export const RTKApi = createApi({
|
||||
reducerPath: 'api',
|
||||
tagTypes: ['StudyGuidanceGroups'],
|
||||
baseQuery: fetchBaseQuery({
|
||||
baseUrl: apiBasePath,
|
||||
prepareHeaders: (headers, { getState }) => {
|
||||
// Get token from state and add it to headers
|
||||
const token = getState().auth.encodedToken
|
||||
if (token) headers.set('x-access-token', token)
|
||||
// Add possible default headers
|
||||
Object.entries(getHeaders()).forEach(([key, value]) => headers.set(key, value))
|
||||
return headers
|
||||
},
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
})
|
||||
|
||||
@@ -99,7 +99,8 @@ const createGraphOptions = ({
|
||||
}
|
||||
}
|
||||
|
||||
const sortCoursesByDate = courses => courses.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
|
||||
const sortCoursesByDate = courses =>
|
||||
[...courses].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
|
||||
|
||||
const filterCoursesByDate = (courses, date) => courses.filter(c => moment(c.date).isSameOrAfter(moment(date)))
|
||||
|
||||
@@ -172,8 +173,8 @@ const createGoalSeries = (starting, ending, absences) => {
|
||||
}
|
||||
}
|
||||
|
||||
const createStudentCreditLines = (students, singleStudent) => {
|
||||
return students.map(student => {
|
||||
const createStudentCreditLines = (students, singleStudent) =>
|
||||
students.map(student => {
|
||||
const { started, studyrightStart } = student
|
||||
|
||||
const startDate = students.length === 1 ? started : studyrightStart
|
||||
@@ -215,7 +216,6 @@ const createStudentCreditLines = (students, singleStudent) => {
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const CreditAccumulationGraphHighCharts = ({ students, singleStudent, absences, startDate, endDate }) => {
|
||||
const history = useHistory()
|
||||
|
||||
+4
-27
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { shape, arrayOf, string, bool } from 'prop-types'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PopulationCourseStatsFlat from '../../PopulationCourseStats/PopulationCourseStatsFlat'
|
||||
import useCourseFilter from '../../FilterTray/filters/Courses/useCourseFilter'
|
||||
|
||||
const CustomPopulationCourses = ({ courses, pending, selectedStudents, query, error, showFilter }) => {
|
||||
const CustomPopulationCourses = ({ selectedStudents, showFilter = false }) => {
|
||||
const { setCourses, resetCourses } = useCourseFilter()
|
||||
const { data: courses, pending, error } = useSelector(({ populationCourses }) => populationCourses)
|
||||
|
||||
useEffect(() => {
|
||||
if (!pending && !error && courses.coursestatistics) {
|
||||
@@ -21,33 +21,10 @@ const CustomPopulationCourses = ({ courses, pending, selectedStudents, query, er
|
||||
return (
|
||||
<PopulationCourseStatsFlat
|
||||
courses={courses}
|
||||
query={query}
|
||||
pending={pending}
|
||||
selectedStudents={selectedStudents}
|
||||
customPopulation
|
||||
showFilter={showFilter}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
CustomPopulationCourses.propTypes = {
|
||||
pending: bool.isRequired,
|
||||
courses: shape([]).isRequired,
|
||||
selectedStudents: arrayOf(string).isRequired,
|
||||
query: shape({}).isRequired,
|
||||
error: bool.isRequired,
|
||||
showFilter: bool,
|
||||
}
|
||||
|
||||
CustomPopulationCourses.defaultProps = {
|
||||
showFilter: false,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ populationCourses }) => ({
|
||||
courses: populationCourses.data,
|
||||
pending: populationCourses.pending,
|
||||
query: populationCourses.query,
|
||||
error: populationCourses.error,
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps)(CustomPopulationCourses)
|
||||
export default CustomPopulationCourses
|
||||
|
||||
@@ -16,7 +16,6 @@ import InfoBox from '../InfoBox'
|
||||
import FilterTray from '../FilterTray'
|
||||
import useFilters from '../FilterTray/useFilters'
|
||||
import { CustomPopulationFilters } from '../FilterTray/FilterSets'
|
||||
import useFilterTray from '../FilterTray/useFilterTray'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import CustomPopulationSearch from './CustomPopulationSearch'
|
||||
import UnihowDataExport from './UnihowDataExport'
|
||||
@@ -35,7 +34,6 @@ const CustomPopulation = ({
|
||||
const [, setSelectedSearchId] = useState('')
|
||||
const [newestIndex, setNewest] = useState(null)
|
||||
const { setAllStudents, filteredStudents } = useFilters()
|
||||
const [trayOpen] = useFilterTray()
|
||||
const selectedStudents = filteredStudents.map(stu => stu.studentNumber)
|
||||
|
||||
const { progress } = useProgress(loading)
|
||||
@@ -98,12 +96,7 @@ const CustomPopulation = ({
|
||||
content: {
|
||||
content: (
|
||||
<div ref={creditGainRef}>
|
||||
<CreditAccumulationGraphHighCharts
|
||||
students={filteredStudents}
|
||||
render={false}
|
||||
trayOpen={trayOpen}
|
||||
language={language}
|
||||
/>
|
||||
<CreditAccumulationGraphHighCharts students={filteredStudents} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Popup, Button, Icon } from 'semantic-ui-react'
|
||||
import { contextKey as filterTrayContextKey } from './index'
|
||||
import useFilterTray from './useFilterTray'
|
||||
import useAnalytics from './useAnalytics'
|
||||
|
||||
/**
|
||||
* @param {func} applyFilter Function to apply the filter when toggled on
|
||||
* @param {func} clearFilter Function to clear the filter when toggled off.
|
||||
* @param {string} filterName Name to display in analytics.
|
||||
* @param {active} bool Toggle state. Must be controlled in the parent.
|
||||
* @param {funcs} popupContent Content component to show in the tooltip, if given. Tooltip is not shown if omitted.
|
||||
* @param {string} filterPanelContextKey Used to open the filter's panel, if given. If not given, the filter tray will not be opened either.
|
||||
*/
|
||||
const ExternalFilterToggle = ({
|
||||
filterPanelContextKey,
|
||||
applyFilter,
|
||||
clearFilter,
|
||||
filterName,
|
||||
active,
|
||||
popupContent,
|
||||
filterPanelContextKey,
|
||||
}) => {
|
||||
const [, setFilterTrayOpen] = useFilterTray(filterTrayContextKey)
|
||||
const [, setCreditFilterOpen] = useFilterTray(filterPanelContextKey)
|
||||
@@ -55,24 +62,4 @@ const ExternalFilterToggle = ({
|
||||
)
|
||||
}
|
||||
|
||||
ExternalFilterToggle.propTypes = {
|
||||
/** Used to open the filter's panel, if given. If not given, the filter tray will not be opened either. */
|
||||
filterPanelContextKey: PropTypes.string,
|
||||
/** Function to apply the filter when toggled on. */
|
||||
applyFilter: PropTypes.func.isRequired,
|
||||
/** Function to clear the filter when toggled off. */
|
||||
clearFilter: PropTypes.func.isRequired,
|
||||
/** Name to display in analytics. */
|
||||
filterName: PropTypes.string.isRequired,
|
||||
/** Toggle state. Must be controlled in the parent. */
|
||||
active: PropTypes.bool.isRequired,
|
||||
/** Content to show in the tooltip, if given. Tooltip is not shown if omitted. */
|
||||
popupContent: PropTypes.node,
|
||||
}
|
||||
|
||||
ExternalFilterToggle.defaultProps = {
|
||||
filterPanelContextKey: undefined,
|
||||
popupContent: undefined,
|
||||
}
|
||||
|
||||
export default ExternalFilterToggle
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { CourseFilterProvider } from './filters/Courses/useCourseFilter'
|
||||
import { CreditFilterProvider } from './filters/CreditsEarned/useCreditFilter'
|
||||
import { AgeFilterProvider } from './filters/Age/useAgeFilter'
|
||||
@@ -23,8 +22,4 @@ const filterProviders = [
|
||||
const FilterContextProvider = ({ children }) =>
|
||||
filterProviders.reduceRight((children, Provider) => <Provider>{children}</Provider>, children)
|
||||
|
||||
FilterContextProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
export default FilterContextProvider
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import useAnalytics from './useAnalytics'
|
||||
import GraduatedFromProgramme from './filters/GraduatedFromProgramme'
|
||||
import TransferredToProgramme from './filters/TransferredToProgramme'
|
||||
import EnrollmentStatus from './filters/EnrollmentStatus'
|
||||
@@ -7,70 +8,67 @@ import Gender from './filters/Gender'
|
||||
import StartYearAtUni from './filters/StartYearAtUni'
|
||||
import Courses from './filters/Courses'
|
||||
import Programmes from './filters/Programmes'
|
||||
import useAnalytics from './useAnalytics'
|
||||
import Grade from './filters/Grade'
|
||||
import Date from './filters/Date'
|
||||
import Age from './filters/Age'
|
||||
import AdmissionType from './filters/AdmissionType'
|
||||
import Tags from './filters/Tags'
|
||||
|
||||
export const PopulationStatisticsFilters = ({ query }) => {
|
||||
const Filters = ({ name, children }) => {
|
||||
const analytics = useAnalytics()
|
||||
useEffect(() => {
|
||||
analytics.setTarget('Population Statistics')
|
||||
analytics.setTarget(name)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<GraduatedFromProgramme />
|
||||
<TransferredToProgramme />
|
||||
<EnrollmentStatus />
|
||||
<CreditsEarned />
|
||||
<Age />
|
||||
<Gender />
|
||||
<StartYearAtUni />
|
||||
{parseInt(query?.year, 10) >= 2020 ? <AdmissionType /> : null}
|
||||
<Courses />
|
||||
<Tags />
|
||||
<Date />
|
||||
</>
|
||||
)
|
||||
return React.Children.toArray(children)
|
||||
}
|
||||
|
||||
export const CoursePopulationFilters = () => {
|
||||
const analytics = useAnalytics()
|
||||
export const PopulationStatisticsFilters = ({ query }) => (
|
||||
<Filters name="Population Statistics">
|
||||
<GraduatedFromProgramme />
|
||||
<TransferredToProgramme />
|
||||
<EnrollmentStatus />
|
||||
<CreditsEarned />
|
||||
<Age />
|
||||
<Gender />
|
||||
<StartYearAtUni />
|
||||
{parseInt(query?.year, 10) >= 2020 ? <AdmissionType /> : null}
|
||||
<Courses />
|
||||
<Tags />
|
||||
<Date />
|
||||
</Filters>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
analytics.setTarget('Course Population')
|
||||
})
|
||||
export const CoursePopulationFilters = () => (
|
||||
<Filters name="Course Population">
|
||||
<Grade />
|
||||
<Age />
|
||||
<Gender />
|
||||
<StartYearAtUni />
|
||||
<Courses />
|
||||
<Programmes />
|
||||
</Filters>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grade />
|
||||
<Age />
|
||||
<Gender />
|
||||
<StartYearAtUni />
|
||||
<Courses />
|
||||
<Programmes />
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const CustomPopulationFilters = () => (
|
||||
<Filters name="Custom Population">
|
||||
<EnrollmentStatus />
|
||||
<Age />
|
||||
<Gender />
|
||||
<StartYearAtUni />
|
||||
<Courses />
|
||||
<Date />
|
||||
</Filters>
|
||||
)
|
||||
|
||||
export const CustomPopulationFilters = () => {
|
||||
const analytics = useAnalytics()
|
||||
|
||||
useEffect(() => {
|
||||
analytics.setTarget('Custom Population')
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<EnrollmentStatus />
|
||||
<Age />
|
||||
<Gender />
|
||||
<StartYearAtUni />
|
||||
<Courses />
|
||||
<Date />
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const StudyGuidanceGroupFilters = () => (
|
||||
<Filters name="Study Guidance Group">
|
||||
<EnrollmentStatus />
|
||||
<Age />
|
||||
<Gender />
|
||||
<StartYearAtUni />
|
||||
<AdmissionType />
|
||||
<GraduatedFromProgramme />
|
||||
<Tags />
|
||||
<Courses />
|
||||
</Filters>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { createContext, useState, useContext, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { getPopulationSelectedStudentCourses } from '../../../../redux/populationSelectedStudentCourses'
|
||||
|
||||
@@ -23,11 +22,6 @@ const CourseFilterProvider = ({ children, getPopulationSelectedStudentCourses })
|
||||
return <CourseFilterContext.Provider value={[state, setState]}>{children}</CourseFilterContext.Provider>
|
||||
}
|
||||
|
||||
CourseFilterProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
getPopulationSelectedStudentCourses: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
const ConnectedProvider = connect(null, { getPopulationSelectedStudentCourses })(CourseFilterProvider)
|
||||
|
||||
export { ConnectedProvider as CourseFilterProvider }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Button, Icon } from 'semantic-ui-react'
|
||||
|
||||
const ClearFilterButton = ({ disabled, onClick, name }) => (
|
||||
@@ -9,10 +8,4 @@ const ClearFilterButton = ({ disabled, onClick, name }) => (
|
||||
</Button>
|
||||
)
|
||||
|
||||
ClearFilterButton.propTypes = {
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default ClearFilterButton
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Icon, Header } from 'semantic-ui-react'
|
||||
import useFilterTray from '../../useFilterTray'
|
||||
|
||||
@@ -23,12 +22,4 @@ const FilterCard = ({ title, children, contextKey, name, active }) => {
|
||||
)
|
||||
}
|
||||
|
||||
FilterCard.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
contextKey: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
active: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
export default FilterCard
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Input, Label, Popup, Button, Icon } from 'semantic-ui-react'
|
||||
|
||||
const NumericInput = ({ onChange, onKeyDown, onClear, value, label, clearButtonDisabled, className, name }) => (
|
||||
const NumericInput = ({
|
||||
onChange,
|
||||
onKeyDown = () => {},
|
||||
onClear,
|
||||
value,
|
||||
label,
|
||||
clearButtonDisabled,
|
||||
className,
|
||||
name,
|
||||
}) => (
|
||||
<Input
|
||||
labelPosition="left"
|
||||
size="mini"
|
||||
@@ -30,20 +38,4 @@ const NumericInput = ({ onChange, onKeyDown, onClear, value, label, clearButtonD
|
||||
</Input>
|
||||
)
|
||||
|
||||
NumericInput.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onKeyDown: PropTypes.func,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
label: PropTypes.node.isRequired,
|
||||
clearButtonDisabled: PropTypes.bool.isRequired,
|
||||
className: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
NumericInput.defaultProps = {
|
||||
onKeyDown: () => {},
|
||||
className: null,
|
||||
}
|
||||
|
||||
export default NumericInput
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Segment, Header } from 'semantic-ui-react'
|
||||
import './filterTray.css'
|
||||
import useFilters from './useFilters'
|
||||
|
||||
export const contextKey = 'filterTray'
|
||||
|
||||
const FilterTray = ({ children, filterSet, visible }) => {
|
||||
const FilterTray = ({ children, filterSet, visible = true }) => {
|
||||
const { filteredStudents, allStudents } = useFilters()
|
||||
|
||||
if (!visible || !allStudents.length) {
|
||||
@@ -17,7 +16,7 @@ const FilterTray = ({ children, filterSet, visible }) => {
|
||||
<>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'flex-start', width: '100%' }}>
|
||||
<Segment style={{ marginLeft: '0.5rem', width: '12rem', position: 'sticky', top: '10px' }}>
|
||||
<Header size="small" style={{ textAlign: 'center' }}>
|
||||
<Header size="small" style={{ textAlign: 'center' }} data-cy="filtered-students">
|
||||
Filter students
|
||||
</Header>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
@@ -34,14 +33,4 @@ const FilterTray = ({ children, filterSet, visible }) => {
|
||||
)
|
||||
}
|
||||
|
||||
FilterTray.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
filterSet: PropTypes.node.isRequired,
|
||||
visible: PropTypes.bool,
|
||||
}
|
||||
|
||||
FilterTray.defaultProps = {
|
||||
visible: true,
|
||||
}
|
||||
|
||||
export default FilterTray
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { createContext, useState, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import TSA from '../../common/tsa'
|
||||
|
||||
const FilterAnalyticsContext = createContext([[], () => {}])
|
||||
@@ -10,10 +9,6 @@ export const FilterAnalyticsProvider = ({ children }) => {
|
||||
return <FilterAnalyticsContext.Provider value={[state, setState]}>{children}</FilterAnalyticsContext.Provider>
|
||||
}
|
||||
|
||||
FilterAnalyticsProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const [state, setState] = useContext(FilterAnalyticsContext)
|
||||
const category = `Filters (${state})`
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* Context for those expandable filter panels that need to be opened/closed remotely.
|
||||
*/
|
||||
import React, { createContext, useState, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const FilterTrayContext = createContext([[], () => {}])
|
||||
FilterTrayContext.displayName = 'Filter Tray'
|
||||
@@ -13,10 +12,6 @@ export const FilterTrayProvider = ({ children }) => {
|
||||
return <FilterTrayContext.Provider value={[state, setState]}>{children}</FilterTrayContext.Provider>
|
||||
}
|
||||
|
||||
FilterTrayProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
export default key => {
|
||||
const [state, setState] = useContext(FilterTrayContext)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* Context for managing filtering.
|
||||
*/
|
||||
import React, { createContext, useState, useContext, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import lodash from 'lodash'
|
||||
|
||||
const defaultState = {
|
||||
@@ -37,10 +36,6 @@ export const FilterProvider = ({ children }) => {
|
||||
return <FilterContext.Provider value={[state, setState]}>{children}</FilterContext.Provider>
|
||||
}
|
||||
|
||||
FilterProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const [state, setState] = useContext(FilterContext)
|
||||
|
||||
|
||||
+61
-83
@@ -1,9 +1,7 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Table, Form, Input, Tab, Icon } from 'semantic-ui-react'
|
||||
import { func, arrayOf, object, shape, string, bool } from 'prop-types'
|
||||
import { orderBy, debounce } from 'lodash'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { clearCourseStats } from '../../redux/coursestats'
|
||||
import './populationCourseStats.css'
|
||||
import { PopulationCourseContext } from './PopulationCourseContext'
|
||||
@@ -43,7 +41,7 @@ const lodashSortOrderTypes = {
|
||||
DESC: 'desc',
|
||||
}
|
||||
|
||||
function updateCourseStatisticsCriteria(props, language, state) {
|
||||
const updateCourseStatisticsCriteria = (props, language, state) => {
|
||||
const { studentAmountLimit, sortCriteria, codeFilter, nameFilter, reversed } = state
|
||||
const {
|
||||
courses: { coursestatistics },
|
||||
@@ -90,8 +88,21 @@ const initialState = props => ({
|
||||
selectedStudentsLength: props.selectedStudentsLength || 0,
|
||||
})
|
||||
|
||||
function PopulationCourseStats(props) {
|
||||
const PopulationCourseStats = ({ courses, pending, selectedStudents, showFilter = true }) => {
|
||||
const dispatch = useDispatch()
|
||||
const { years } = useSelector(({ semesters }) => semesters.data)
|
||||
const populationCourses = useSelector(({ populationCourses }) => populationCourses)
|
||||
const isAdmin = getUserIsAdmin(useSelector(({ auth }) => auth.token.roles))
|
||||
const { language } = useLanguage()
|
||||
const props = {
|
||||
courses,
|
||||
isAdmin,
|
||||
pending,
|
||||
populationCourses,
|
||||
selectedStudents,
|
||||
showFilter,
|
||||
years,
|
||||
}
|
||||
|
||||
const [filterFields, setFilterFields] = useState({ codeFilter: '', nameFilter: '' })
|
||||
const [courseStatistics, setCourseStatistics] = useState(
|
||||
@@ -105,7 +116,7 @@ function PopulationCourseStats(props) {
|
||||
const filterAnalytics = useAnalytics()
|
||||
|
||||
useEffect(() => {
|
||||
if (state && props.courses) {
|
||||
if (!pending && state && props.courses) {
|
||||
const studentAmountLimit =
|
||||
state.selectedStudentsLength !== props.selectedStudents.length
|
||||
? Math.round(props.selectedStudents.length * 0.3)
|
||||
@@ -119,44 +130,46 @@ function PopulationCourseStats(props) {
|
||||
})
|
||||
setCourseStatistics(updateCourseStatisticsCriteria(props, language, state))
|
||||
}
|
||||
}, [props.courses, props.selectedStudents])
|
||||
}, [props.courses, props.selectedStudents, pending])
|
||||
|
||||
useEffect(() => {
|
||||
const { studentAmountLimit, codeFilter, nameFilter, reversed, sortCriteria } = state
|
||||
const {
|
||||
courses: { coursestatistics },
|
||||
language,
|
||||
} = props
|
||||
const studentAmountFilter = ({ stats }) => {
|
||||
const { students } = stats
|
||||
return studentAmountLimit === 0 || students >= studentAmountLimit
|
||||
if (!pending) {
|
||||
const { studentAmountLimit, codeFilter, nameFilter, reversed, sortCriteria } = state
|
||||
const {
|
||||
courses: { coursestatistics },
|
||||
language,
|
||||
} = props
|
||||
const studentAmountFilter = ({ stats }) => {
|
||||
const { students } = stats
|
||||
return studentAmountLimit === 0 || students >= studentAmountLimit
|
||||
}
|
||||
const courseCodeFilter = ({ course }) => {
|
||||
const { code } = course
|
||||
return code.toLowerCase().includes(codeFilter.toLowerCase())
|
||||
}
|
||||
const courseNameFilter = ({ course }) => {
|
||||
const { name } = course
|
||||
return getTextIn(name, language).toLowerCase().includes(nameFilter.toLowerCase())
|
||||
}
|
||||
|
||||
const filteredCourses =
|
||||
coursestatistics &&
|
||||
coursestatistics
|
||||
.filter(studentAmountFilter)
|
||||
.filter(c => !codeFilter || courseCodeFilter(c))
|
||||
.filter(c => !nameFilter || courseNameFilter(c))
|
||||
|
||||
const lodashSortOrder = reversed ? lodashSortOrderTypes.DESC : lodashSortOrderTypes.ASC
|
||||
|
||||
const sortedStatistics = orderBy(
|
||||
filteredCourses,
|
||||
[course => course.stats[sortCriteria], course => course.course.code],
|
||||
[lodashSortOrder, lodashSortOrderTypes.ASC]
|
||||
)
|
||||
|
||||
setCourseStatistics(sortedStatistics)
|
||||
}
|
||||
const courseCodeFilter = ({ course }) => {
|
||||
const { code } = course
|
||||
return code.toLowerCase().includes(codeFilter.toLowerCase())
|
||||
}
|
||||
const courseNameFilter = ({ course }) => {
|
||||
const { name } = course
|
||||
return getTextIn(name, language).toLowerCase().includes(nameFilter.toLowerCase())
|
||||
}
|
||||
|
||||
const filteredCourses =
|
||||
coursestatistics &&
|
||||
coursestatistics
|
||||
.filter(studentAmountFilter)
|
||||
.filter(c => !codeFilter || courseCodeFilter(c))
|
||||
.filter(c => !nameFilter || courseNameFilter(c))
|
||||
|
||||
const lodashSortOrder = reversed ? lodashSortOrderTypes.DESC : lodashSortOrderTypes.ASC
|
||||
|
||||
const sortedStatistics = orderBy(
|
||||
filteredCourses,
|
||||
[course => course.stats[sortCriteria], course => course.course.code],
|
||||
[lodashSortOrder, lodashSortOrderTypes.ASC]
|
||||
)
|
||||
|
||||
setCourseStatistics(sortedStatistics)
|
||||
}, [state.studentAmountLimit, state.codeFilter, state.nameFilter])
|
||||
}, [state.studentAmountLimit, state.codeFilter, state.nameFilter, pending])
|
||||
|
||||
const onFilterChange = (e, field) => {
|
||||
const {
|
||||
@@ -174,8 +187,8 @@ function PopulationCourseStats(props) {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setFilters(filterFields)
|
||||
}, [filterFields])
|
||||
if (!pending) setFilters(filterFields)
|
||||
}, [filterFields, pending])
|
||||
|
||||
const handleCourseStatisticsCriteriaChange = () => {
|
||||
// eslint-disable-next-line react/no-access-state-in-setstate
|
||||
@@ -232,13 +245,14 @@ function PopulationCourseStats(props) {
|
||||
}
|
||||
|
||||
const onGoToCourseStatisticsClick = courseCode => {
|
||||
const { clearCourseStats: clearCourseStatsfn } = props
|
||||
sendAnalytics('Courses of Population course stats button clicked', courseCode)
|
||||
clearCourseStatsfn()
|
||||
dispatch(clearCourseStats())
|
||||
}
|
||||
|
||||
const onCourseNameCellClick = code => {
|
||||
const courseStatistic = props.populationCourses.data.coursestatistics.find(cs => cs.course.code === code)
|
||||
const courseStatistic =
|
||||
props.populationCourses.data.coursestatistics?.find(cs => cs.course.code === code) ||
|
||||
props.courses.coursestatistics?.find(cs => cs.course.code === code)
|
||||
if (courseStatistic) {
|
||||
const isSelected = courseIsSelected(code)
|
||||
const name = 'Course Filtername'
|
||||
@@ -280,7 +294,6 @@ function PopulationCourseStats(props) {
|
||||
)
|
||||
}
|
||||
|
||||
const { courses, pending } = props
|
||||
const { sortCriteria, reversed } = state
|
||||
const contextValue = {
|
||||
courseStatistics,
|
||||
@@ -324,8 +337,6 @@ function PopulationCourseStats(props) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { showFilter = true } = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showFilter && (
|
||||
@@ -343,37 +354,4 @@ function PopulationCourseStats(props) {
|
||||
)
|
||||
}
|
||||
|
||||
PopulationCourseStats.propTypes = {
|
||||
courses: shape({
|
||||
coursestatistics: arrayOf(object),
|
||||
disciplines: shape({}),
|
||||
}).isRequired,
|
||||
populationCourses: shape({
|
||||
data: shape({ coursestatistics: arrayOf(shape({ course: shape({ code: string, name: shape({}) }) })) }),
|
||||
}).isRequired,
|
||||
clearCourseStats: func.isRequired,
|
||||
pending: bool.isRequired,
|
||||
selectedStudents: arrayOf(string).isRequired,
|
||||
years: shape({}), // eslint-disable-line
|
||||
showFilter: bool,
|
||||
language: string.isRequired,
|
||||
}
|
||||
|
||||
PopulationCourseStats.defaultProps = {
|
||||
showFilter: false,
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { years } = state.semesters.data
|
||||
const isAdmin = getUserIsAdmin(state.auth.token.roles)
|
||||
|
||||
return {
|
||||
years,
|
||||
populationCourses: state.populationCourses,
|
||||
isAdmin,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
clearCourseStats,
|
||||
})(withRouter(PopulationCourseStats))
|
||||
export default PopulationCourseStats
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { connect, useSelector } from 'react-redux'
|
||||
import { Table, Input, Tab, Icon } from 'semantic-ui-react'
|
||||
import { func, arrayOf, object, shape, string, bool } from 'prop-types'
|
||||
import { orderBy, debounce } from 'lodash'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { clearCourseStats } from '../../redux/coursestats'
|
||||
@@ -92,7 +91,7 @@ const initialState = props => ({
|
||||
selectedStudentsLength: props.selectedStudentsLength || 0,
|
||||
})
|
||||
|
||||
function PopulationCourseStats(props) {
|
||||
const PopulationCourseStats = props => {
|
||||
const { language } = useLanguage()
|
||||
|
||||
const [filterFields, setFilterFields] = useState({ codeFilter: '', nameFilter: '' })
|
||||
@@ -268,7 +267,9 @@ function PopulationCourseStats(props) {
|
||||
}
|
||||
|
||||
const onCourseNameCellClick = code => {
|
||||
const courseStatistic = props.populationCourses.data.coursestatistics.find(cs => cs.course.code === code)
|
||||
const courseStatistic =
|
||||
props.populationCourses.data.coursestatistics?.find(cs => cs.course.code === code) ||
|
||||
props.courses.coursestatistics?.find(cs => cs.course.code === code)
|
||||
if (courseStatistic) {
|
||||
const isSelected = courseIsSelected(code)
|
||||
const name = 'Course Filtername'
|
||||
@@ -397,19 +398,6 @@ function PopulationCourseStats(props) {
|
||||
)
|
||||
}
|
||||
|
||||
PopulationCourseStats.propTypes = {
|
||||
courses: shape({
|
||||
coursestatistics: arrayOf(object),
|
||||
}).isRequired,
|
||||
populationCourses: shape({
|
||||
data: shape({ coursestatistics: arrayOf(shape({ course: shape({ code: string, name: shape({}) }) })) }),
|
||||
}).isRequired,
|
||||
clearCourseStats: func.isRequired,
|
||||
pending: bool.isRequired,
|
||||
selectedStudents: arrayOf(string).isRequired,
|
||||
language: string.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ populationCourses }) => ({
|
||||
populationCourses,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { shape, arrayOf, string, bool } from 'prop-types'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Segment, Button } from 'semantic-ui-react'
|
||||
import uuidv4 from 'uuid/v4'
|
||||
import SegmentDimmer from '../SegmentDimmer'
|
||||
@@ -11,15 +10,13 @@ import FilterDegreeCoursesModal from './FilterDegreeCoursesModal'
|
||||
import useCourseFilter from '../FilterTray/filters/Courses/useCourseFilter'
|
||||
import infotooltips from '../../common/InfoToolTips'
|
||||
|
||||
const PopulationCourses = ({
|
||||
populationSelectedStudentCourses,
|
||||
populationCourses,
|
||||
selectedStudents,
|
||||
query,
|
||||
filteredStudents,
|
||||
}) => {
|
||||
const PopulationCourses = ({ selectedStudents, query = {}, filteredStudents }) => {
|
||||
const { setCoursesOnce, resetCourses, runCourseQuery } = useCourseFilter()
|
||||
const [showByStudytrack, setShowByStudytrack] = useState(true)
|
||||
const populationCourses = useSelector(({ populationCourses }) => populationCourses)
|
||||
const populationSelectedStudentCourses = useSelector(
|
||||
({ populationSelectedStudentCourses }) => populationSelectedStudentCourses
|
||||
)
|
||||
|
||||
const selectedPopulationCourses = populationSelectedStudentCourses.data
|
||||
? populationSelectedStudentCourses
|
||||
@@ -89,7 +86,6 @@ const PopulationCourses = ({
|
||||
<PopulationCourseStats
|
||||
key={selectedPopulationCourses.query.uuid}
|
||||
courses={selectedPopulationCourses.data}
|
||||
query={selectedPopulationCourses.query}
|
||||
pending={pending}
|
||||
selectedStudents={selectedStudents}
|
||||
/>
|
||||
@@ -100,21 +96,4 @@ const PopulationCourses = ({
|
||||
)
|
||||
}
|
||||
|
||||
PopulationCourses.defaultPropTypes = {
|
||||
query: {},
|
||||
}
|
||||
|
||||
PopulationCourses.propTypes = {
|
||||
populationSelectedStudentCourses: shape({ query: shape({}), data: shape({}), pending: bool }).isRequired,
|
||||
populationCourses: shape({ query: shape({}), data: shape({}), pending: bool }).isRequired,
|
||||
selectedStudents: arrayOf(string).isRequired,
|
||||
query: shape({}).isRequired,
|
||||
filteredStudents: arrayOf(shape({})).isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ populationSelectedStudentCourses, populationCourses }) => ({
|
||||
populationCourses,
|
||||
populationSelectedStudentCourses,
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps)(PopulationCourses)
|
||||
export default PopulationCourses
|
||||
|
||||
@@ -100,7 +100,10 @@ const PopulationDetails = ({ samples, queryIsSet, isLoading, query, dataExport }
|
||||
key: 1,
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
<span
|
||||
style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}
|
||||
data-cy="credit-statistics"
|
||||
>
|
||||
Credit statistics
|
||||
</span>
|
||||
),
|
||||
|
||||
@@ -37,6 +37,8 @@ const GeneralTab = ({
|
||||
const [popupStates, setPopupStates] = useState({})
|
||||
const sendAnalytics = sendEvent.populationStudents
|
||||
|
||||
if (!populationStatistics) return null
|
||||
|
||||
// TODO: Refactor this away:
|
||||
const selectedStudents = filteredStudents.map(stu => stu.studentNumber)
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { string, arrayOf, object, func, bool, shape, node } from 'prop-types'
|
||||
import { Icon, Tab, Grid, Ref, Item, Dropdown } from 'semantic-ui-react'
|
||||
import { withRouter, Link } from 'react-router-dom'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { Icon, Tab, Grid, Item, Dropdown } from 'semantic-ui-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { orderBy, uniqBy, flatten, sortBy, isNumber } from 'lodash'
|
||||
import scrollToComponent from 'react-scroll-to-component'
|
||||
import { getTextIn, getUserRoles } from '../../common'
|
||||
import { useTabChangeAnalytics } from '../../common/hooks'
|
||||
import { getTextIn } from '../../common'
|
||||
import { useTabChangeAnalytics, usePrevious, useIsAdmin } from '../../common/hooks'
|
||||
|
||||
import { toggleStudentListVisibility } from '../../redux/settings'
|
||||
import { getTagsByStudytrackAction } from '../../redux/tags'
|
||||
import { getStudentTagsByStudytrackAction } from '../../redux/tagstudent'
|
||||
|
||||
@@ -24,79 +22,95 @@ import GeneralTab from './StudentTable/GeneralTab'
|
||||
import sendEvent, { ANALYTICS_CATEGORIES } from '../../common/sendEvent'
|
||||
import infotoolTips from '../../common/InfoToolTips'
|
||||
|
||||
// TODO: Refactoring in process, contains lot of duplicate code.
|
||||
|
||||
const sendAnalytics = sendEvent.populationStudents
|
||||
|
||||
const StudentTableTabs = ({ panes, filterPanes }) => {
|
||||
// only its own component really because I needed to use hooks and didn't want to refactor
|
||||
// this megamillion line component :) /Joona
|
||||
const PopulationStudents = ({
|
||||
language,
|
||||
filteredStudents,
|
||||
studentToTargetCourseDateMap,
|
||||
dataExport,
|
||||
customPopulation = false,
|
||||
coursePopulation = false,
|
||||
coursecode = [],
|
||||
}) => {
|
||||
const [state, setState] = useState({})
|
||||
const studentRef = useRef()
|
||||
const dispatch = useDispatch()
|
||||
const { namesVisible: showNames, studentlistVisible: showList } = useSelector(({ settings }) => settings)
|
||||
const { data: tags } = useSelector(({ tags }) => tags)
|
||||
const { query } = useSelector(({ populations }) => populations)
|
||||
const queryStudyrights = query ? Object.values(query.studyRights) : []
|
||||
const { data: mandatoryCourses } = useSelector(({ populationMandatoryCourses }) => populationMandatoryCourses)
|
||||
const { data: populationCourses } = useSelector(({ populationCourses }) => populationCourses)
|
||||
const prevShowList = usePrevious(showList)
|
||||
const admin = useIsAdmin()
|
||||
const { handleTabChange } = useTabChangeAnalytics(
|
||||
ANALYTICS_CATEGORIES.populationStudents,
|
||||
'Change students table tab'
|
||||
)
|
||||
|
||||
return <Tab onTabChange={handleTabChange} panes={filterPanes(panes)} data-cy="student-table-tabs" />
|
||||
}
|
||||
useEffect(() => {
|
||||
if (tags && tags.length > 0) return
|
||||
const queryStudyright = queryStudyrights[0]
|
||||
dispatch(getTagsByStudytrackAction(queryStudyright))
|
||||
dispatch(getStudentTagsByStudytrackAction(queryStudyright))
|
||||
setState({ ...state, admin })
|
||||
}, [])
|
||||
|
||||
StudentTableTabs.propTypes = {
|
||||
panes: arrayOf(object).isRequired,
|
||||
filterPanes: func.isRequired,
|
||||
}
|
||||
|
||||
class PopulationStudents extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {}
|
||||
|
||||
this.studentsRef = React.createRef()
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const queryStudyright = this.props.queryStudyrights[0]
|
||||
const admin = this.props.userRoles.includes('admin')
|
||||
if (queryStudyright) {
|
||||
this.props.getTagsByStudytrack(queryStudyright)
|
||||
this.props.getStudentTagsStudyTrack(queryStudyright)
|
||||
useEffect(() => {
|
||||
if (!prevShowList && showList) {
|
||||
scrollToComponent(studentRef.current, { align: 'bottom' })
|
||||
}
|
||||
this.setState({ admin })
|
||||
}, [prevShowList])
|
||||
|
||||
const mandatoryCodes = mandatoryCourses.filter(course => course.visible && course.visible.visibility).map(c => c.code)
|
||||
let mandatoryPassed = {}
|
||||
if (populationCourses.coursestatistics) {
|
||||
const mandatorycodesMapCourseCodeToCourseID = new Map()
|
||||
mandatoryCourses
|
||||
.filter(course => course.visible && course.visible.visibility)
|
||||
.forEach(c => mandatorycodesMapCourseCodeToCourseID.set(c.code, c.id))
|
||||
|
||||
const courses = populationCourses.coursestatistics
|
||||
mandatoryPassed = mandatoryCodes.reduce((obj, code) => {
|
||||
const foundCourse = !!courses.find(c => c.course.code === code)
|
||||
|
||||
const searchCode = mandatorycodesMapCourseCodeToCourseID.get(code)
|
||||
|
||||
let foundSubsCourse
|
||||
if (searchCode) {
|
||||
foundSubsCourse = courses.find(c => {
|
||||
if (!c.course.substitutions) return false
|
||||
if (c.course.substitutions.length === null) return false
|
||||
return c.course.substitutions.includes(searchCode)
|
||||
})
|
||||
}
|
||||
|
||||
let passedArray = foundCourse ? Object.keys(courses.find(c => c.course.code === code).students.passed) : null
|
||||
|
||||
if (foundSubsCourse) {
|
||||
passedArray = passedArray
|
||||
? [...passedArray, ...Object.keys(foundSubsCourse.students.passed)]
|
||||
: Object.keys(foundSubsCourse.students.passed)
|
||||
}
|
||||
obj[code] = passedArray
|
||||
return obj
|
||||
}, {})
|
||||
}
|
||||
|
||||
componentDidUpdate = prevProps => {
|
||||
if (!prevProps.showList && this.props.showList) {
|
||||
scrollToComponent(this.studentsRef.current, { align: 'bottom' })
|
||||
}
|
||||
}
|
||||
|
||||
studyrightCodes = (studyrights, value) => {
|
||||
const { queryStudyrights } = this.props
|
||||
return studyrights
|
||||
.filter(sr => {
|
||||
const { studyright_elements: studyrightElements } = sr
|
||||
return studyrightElements.filter(sre => queryStudyrights.includes(sre.code)).length >= queryStudyrights.length
|
||||
})
|
||||
.map(a => a[value])
|
||||
}
|
||||
|
||||
handleRef = node => {
|
||||
this.studentsRef.current = node
|
||||
}
|
||||
|
||||
renderStudentTable() {
|
||||
const { customPopulation, coursePopulation } = this.props
|
||||
const renderStudentTable = () => {
|
||||
const verticalTitle = title => <div className="verticalTitle">{title}</div>
|
||||
|
||||
const hasPassedMandatory = (studentNumber, code) =>
|
||||
this.props.mandatoryPassed[code] && this.props.mandatoryPassed[code].includes(studentNumber)
|
||||
mandatoryPassed[code] && mandatoryPassed[code].includes(studentNumber)
|
||||
|
||||
const totalMandatoryPassed = studentNumber => {
|
||||
this.props.mandatoryCourses.reduce((acc, m) => {
|
||||
mandatoryCourses.reduce((acc, m) => {
|
||||
return hasPassedMandatory(studentNumber, m.code) ? acc + 1 : acc
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const nameColumns = this.props.showNames
|
||||
const nameColumns = showNames
|
||||
? [
|
||||
{
|
||||
key: 'lastname',
|
||||
@@ -164,7 +178,7 @@ class PopulationStudents extends Component {
|
||||
|
||||
const mandatoryCourseLabels = []
|
||||
|
||||
const labelToMandatoryCourses = this.props.mandatoryCourses.reduce((acc, e) => {
|
||||
const labelToMandatoryCourses = mandatoryCourses.reduce((acc, e) => {
|
||||
const label = e.label ? e.label.label : ''
|
||||
acc[label] = acc[label] || []
|
||||
if (acc[label].some(l => l.code === e.code)) return acc
|
||||
@@ -183,14 +197,14 @@ class PopulationStudents extends Component {
|
||||
const mandatoryTitle = m => {
|
||||
return (
|
||||
<>
|
||||
{getTextIn(m.name, this.props.language)}
|
||||
{getTextIn(m.name, language)}
|
||||
<br />
|
||||
{m.code}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const { visibleLabels, visibleCourseCodes } = this.props.mandatoryCourses.reduce(
|
||||
const { visibleLabels, visibleCourseCodes } = mandatoryCourses.reduce(
|
||||
(acc, cur) => {
|
||||
if (cur.visible && cur.visible.visibility) {
|
||||
acc.visibleLabels.add(cur.label_code)
|
||||
@@ -220,7 +234,11 @@ class PopulationStudents extends Component {
|
||||
</div>
|
||||
),
|
||||
parent: true,
|
||||
headerProps: { colSpan: labelToMandatoryCourses[e.label].length, title: e.label, ordernumber: e.orderNumber },
|
||||
headerProps: {
|
||||
colSpan: labelToMandatoryCourses[e.label].length,
|
||||
title: e.label,
|
||||
ordernumber: e.orderNumber,
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
@@ -242,8 +260,8 @@ class PopulationStudents extends Component {
|
||||
.map(m => ({
|
||||
key: `${m.label ? m.label.label : 'fix'}-${m.code}`, // really quick and dirty fix
|
||||
title: verticalTitle(mandatoryTitle(m)),
|
||||
cellProps: { title: `${m.code}, ${getTextIn(m.name, this.props.language)}` },
|
||||
headerProps: { title: `${m.code}, ${getTextIn(m.name, this.props.language)}` },
|
||||
cellProps: { title: `${m.code}, ${getTextIn(m.name, language)}` },
|
||||
headerProps: { title: `${m.code}, ${getTextIn(m.name, language)}` },
|
||||
getRowVal: s => (s.total ? getTotalRowVal(s, m) : hasPassedMandatory(s.studentNumber, m.code)),
|
||||
getRowContent: s => {
|
||||
if (s.total) return getTotalRowVal(s, m)
|
||||
@@ -257,22 +275,20 @@ class PopulationStudents extends Component {
|
||||
),
|
||||
]
|
||||
|
||||
const totals = this.props.filteredStudents.reduce(
|
||||
const totals = filteredStudents.reduce(
|
||||
(acc, s) => {
|
||||
const passedCourses = new Set()
|
||||
this.props.mandatoryCourses.forEach(m => {
|
||||
mandatoryCourses.forEach(m => {
|
||||
if (passedCourses.has(m.code)) return
|
||||
passedCourses.add(m.code)
|
||||
if (hasPassedMandatory(s.studentNumber, m.code)) ++acc[m.code]
|
||||
})
|
||||
return acc
|
||||
},
|
||||
this.props.mandatoryCourses.reduce((acc, e) => ({ ...acc, [e.code]: 0 }), { total: true })
|
||||
mandatoryCourses.reduce((acc, e) => ({ ...acc, [e.code]: 0 }), { total: true })
|
||||
)
|
||||
const mandatoryCourseData = [totals, ...this.props.filteredStudents]
|
||||
const mandatoryCourseData = [totals, filteredStudents]
|
||||
|
||||
// FIXME: here only for refactorment
|
||||
const { showNames, studentToTargetCourseDateMap, coursecode } = this.props
|
||||
const panes = [
|
||||
{
|
||||
menuItem: 'General',
|
||||
@@ -294,7 +310,7 @@ class PopulationStudents extends Component {
|
||||
<Tab.Pane>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ overflowX: 'auto', maxHeight: '80vh' }}>
|
||||
{this.props.mandatoryCourses.length > 0 && (
|
||||
{mandatoryCourses.length > 0 && (
|
||||
<SortableTable
|
||||
getRowKey={s => (s.total ? 'totals' : s.studentNumber)}
|
||||
tableProps={{
|
||||
@@ -308,7 +324,7 @@ class PopulationStudents extends Component {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
collapsingHeaders
|
||||
showNames={this.props.showNames}
|
||||
showNames={showNames}
|
||||
columns={mandatoryCourseColumns}
|
||||
data={mandatoryCourseData}
|
||||
/>
|
||||
@@ -323,7 +339,7 @@ class PopulationStudents extends Component {
|
||||
render: () => (
|
||||
<Tab.Pane>
|
||||
<div style={{ overflowX: 'auto', maxHeight: '80vh' }}>
|
||||
{this.props.tags.length === 0 && (
|
||||
{tags.length === 0 && (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: '10px',
|
||||
@@ -337,7 +353,7 @@ class PopulationStudents extends Component {
|
||||
<h3>
|
||||
No tags defined. You can define them{' '}
|
||||
<Link
|
||||
to={`/study-programme/${this.props.queryStudyrights[0]}?p_m_tab=0&p_tab=6`}
|
||||
to={`/study-programme/${queryStudyrights[0]}?p_m_tab=0&p_tab=6`}
|
||||
onClick={() => {
|
||||
sendAnalytics('No tags defined button clicked', 'Tags tab')
|
||||
}}
|
||||
@@ -348,14 +364,14 @@ class PopulationStudents extends Component {
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
{this.props.tags.length > 0 && (
|
||||
{tags.length > 0 && (
|
||||
<>
|
||||
<TagPopulation
|
||||
tags={this.props.tags}
|
||||
selectedStudents={this.props.filteredStudents.map(stu => stu.studentNumber)}
|
||||
studytrack={this.props.queryStudyrights[0]}
|
||||
tags={tags}
|
||||
selectedStudents={filteredStudents.map(stu => stu.studentNumber)}
|
||||
studytrack={queryStudyrights[0]}
|
||||
/>
|
||||
<TagList studytrack={this.props.queryStudyrights[0]} selectedStudents={this.props.filteredStudents} />
|
||||
<TagList studytrack={queryStudyrights[0]} selectedStudents={filteredStudents} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -377,142 +393,34 @@ class PopulationStudents extends Component {
|
||||
<Grid.Column>
|
||||
<StudentNameVisibilityToggle />
|
||||
</Grid.Column>
|
||||
{this.props.dataExport && (
|
||||
{dataExport && (
|
||||
<Grid.Column textAlign="right">
|
||||
<Dropdown text="Export Data" icon="save" button labeled className="icon" direction="left">
|
||||
<Dropdown.Menu>{this.props.dataExport}</Dropdown.Menu>
|
||||
<Dropdown.Menu>{dataExport}</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</Grid.Column>
|
||||
)}
|
||||
</Grid>
|
||||
<StudentTableTabs panes={panes} filterPanes={filteredPanes} />
|
||||
<Tab onTabChange={handleTabChange} panes={filteredPanes(panes)} data-cy="student-table-tabs" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.filteredStudents.length === 0) {
|
||||
return null
|
||||
}
|
||||
if (filteredStudents.length === 0) return null
|
||||
|
||||
return (
|
||||
<Ref innerRef={this.handleRef}>
|
||||
<>
|
||||
<span style={{ marginRight: '0.5rem' }}>
|
||||
<InfoBox
|
||||
content={
|
||||
this.props.coursePopulation
|
||||
? infotoolTips.CoursePopulation.Students
|
||||
: infotoolTips.PopulationStatistics.Students
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
{this.state.admin ? (
|
||||
<CheckStudentList students={this.props.filteredStudents.map(stu => stu.studentNumber)} />
|
||||
) : null}
|
||||
{this.renderStudentTable()}
|
||||
</>
|
||||
</Ref>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<span style={{ marginRight: '0.5rem' }} ref={studentRef}>
|
||||
<InfoBox
|
||||
content={
|
||||
coursePopulation ? infotoolTips.CoursePopulation.Students : infotoolTips.PopulationStatistics.Students
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
{admin ? <CheckStudentList students={filteredStudents.map(stu => stu.studentNumber)} /> : null}
|
||||
{renderStudentTable()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
PopulationStudents.defaultProps = {
|
||||
studentToTargetCourseDateMap: null,
|
||||
customPopulation: false,
|
||||
coursePopulation: false,
|
||||
dataExport: null,
|
||||
coursecode: [],
|
||||
}
|
||||
|
||||
PopulationStudents.propTypes = {
|
||||
showNames: bool.isRequired,
|
||||
showList: bool.isRequired,
|
||||
language: string.isRequired,
|
||||
queryStudyrights: arrayOf(string).isRequired,
|
||||
mandatoryCourses: arrayOf(
|
||||
shape({
|
||||
name: shape({}).isRequired,
|
||||
code: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
mandatoryPassed: shape({}).isRequired,
|
||||
tags: arrayOf(shape({ tag_id: string, tagname: string, studytrack: string })).isRequired,
|
||||
getTagsByStudytrack: func.isRequired,
|
||||
getStudentTagsStudyTrack: func.isRequired,
|
||||
userRoles: arrayOf(string).isRequired,
|
||||
studentToTargetCourseDateMap: shape({}),
|
||||
coursePopulation: bool,
|
||||
customPopulation: bool,
|
||||
filteredStudents: arrayOf(shape({})).isRequired,
|
||||
dataExport: node,
|
||||
coursecode: arrayOf(string),
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const {
|
||||
settings,
|
||||
populations,
|
||||
populationCourses,
|
||||
populationMandatoryCourses,
|
||||
tags,
|
||||
auth: {
|
||||
token: { roles },
|
||||
},
|
||||
} = state
|
||||
|
||||
const mandatoryCodes = populationMandatoryCourses.data
|
||||
.filter(course => course.visible && course.visible.visibility)
|
||||
.map(c => c.code)
|
||||
|
||||
let mandatoryPassed = {}
|
||||
|
||||
if (populationCourses.data.coursestatistics) {
|
||||
const mandatorycodesMapCourseCodeToCourseID = new Map()
|
||||
populationMandatoryCourses.data
|
||||
.filter(course => course.visible && course.visible.visibility)
|
||||
.forEach(c => mandatorycodesMapCourseCodeToCourseID.set(c.code, c.id))
|
||||
|
||||
const courses = populationCourses.data.coursestatistics
|
||||
mandatoryPassed = mandatoryCodes.reduce((obj, code) => {
|
||||
const foundCourse = !!courses.find(c => c.course.code === code)
|
||||
|
||||
const searchCode = mandatorycodesMapCourseCodeToCourseID.get(code)
|
||||
|
||||
let foundSubsCourse
|
||||
if (searchCode) {
|
||||
foundSubsCourse = courses.find(c => {
|
||||
if (!c.course.substitutions) return false
|
||||
if (c.course.substitutions.length === null) return false
|
||||
return c.course.substitutions.includes(searchCode)
|
||||
})
|
||||
}
|
||||
|
||||
let passedArray = foundCourse ? Object.keys(courses.find(c => c.course.code === code).students.passed) : null
|
||||
|
||||
if (foundSubsCourse) {
|
||||
passedArray = passedArray
|
||||
? [...passedArray, ...Object.keys(foundSubsCourse.students.passed)]
|
||||
: Object.keys(foundSubsCourse.students.passed)
|
||||
}
|
||||
obj[code] = passedArray
|
||||
return obj
|
||||
}, {})
|
||||
}
|
||||
|
||||
return {
|
||||
showNames: settings.namesVisible,
|
||||
showList: settings.studentlistVisible,
|
||||
queryStudyrights: populations.query ? Object.values(populations.query.studyRights) : [],
|
||||
mandatoryCourses: populationMandatoryCourses.data,
|
||||
mandatoryPassed,
|
||||
tags: tags.data,
|
||||
userRoles: getUserRoles(roles),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
toggleStudentListVisibility,
|
||||
getTagsByStudytrack: getTagsByStudytrackAction,
|
||||
getStudentTagsStudyTrack: getStudentTagsByStudytrackAction,
|
||||
})(withRouter(PopulationStudents))
|
||||
export default PopulationStudents
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState } from 'react'
|
||||
import { shape, string, arrayOf } from 'prop-types'
|
||||
import { Menu, Tab, Input, Message } from 'semantic-ui-react'
|
||||
import { flattenDeep } from 'lodash'
|
||||
import Highcharts from 'highcharts/highstock'
|
||||
@@ -7,38 +6,26 @@ import ReactHighcharts from 'react-highcharts'
|
||||
import CreditAccumulationGraphHighCharts from '../../CreditAccumulationGraphHighCharts'
|
||||
import { byDateDesc, reformatDate, getTextIn } from '../../../common'
|
||||
import TSA from '../../../common/tsa'
|
||||
import useLanguage from '../../LanguagePicker/useLanguage'
|
||||
|
||||
const ANALYTICS_CATEGORY = 'Student stats'
|
||||
const sendAnalytics = (action, name, value) => TSA.Matomo.sendEvent(ANALYTICS_CATEGORY, action, name, value)
|
||||
|
||||
const CreditsGraph = ({ graphYearStart, student, absences }) => {
|
||||
const { language } = useLanguage()
|
||||
|
||||
const selectedStart = new Date(graphYearStart ?? student.started)
|
||||
const dates = flattenDeep(student.courses.map(c => c.date)).map(d => new Date(d).getTime())
|
||||
|
||||
const endDate = dates.length > 0 ? Math.max(...dates) : new Date().getTime()
|
||||
|
||||
return (
|
||||
<CreditAccumulationGraphHighCharts
|
||||
singleStudent
|
||||
students={[student]}
|
||||
title="Credit Accumulation"
|
||||
startDate={selectedStart}
|
||||
endDate={endDate}
|
||||
absences={absences}
|
||||
language={language}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
CreditsGraph.propTypes = {
|
||||
student: shape({}).isRequired,
|
||||
absences: arrayOf(shape({})).isRequired,
|
||||
graphYearStart: string.isRequired,
|
||||
}
|
||||
|
||||
const chunkifyArray = (array, size = 1) => {
|
||||
if (!array) return []
|
||||
const firstChunk = array.slice(0, size) // create the first chunk of the given array
|
||||
@@ -197,12 +184,6 @@ const GradeGraph = ({ student, semesters, language }) => {
|
||||
)
|
||||
}
|
||||
|
||||
GradeGraph.propTypes = {
|
||||
student: shape({}).isRequired,
|
||||
semesters: shape({}).isRequired,
|
||||
language: string.isRequired,
|
||||
}
|
||||
|
||||
const StudentGraphs = ({ student, absences, graphYearStart, semesters, language }) => {
|
||||
const panes = [
|
||||
{
|
||||
@@ -225,12 +206,4 @@ const StudentGraphs = ({ student, absences, graphYearStart, semesters, language
|
||||
return <Tab panes={panes} />
|
||||
}
|
||||
|
||||
StudentGraphs.propTypes = {
|
||||
student: shape({}).isRequired,
|
||||
absences: arrayOf(shape({})).isRequired,
|
||||
graphYearStart: string.isRequired,
|
||||
semesters: shape({}).isRequired,
|
||||
language: string.isRequired,
|
||||
}
|
||||
|
||||
export default StudentGraphs
|
||||
|
||||
+144
-129
@@ -1,63 +1,95 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
// temp disable prop types
|
||||
import React, { useEffect, useState, useRef } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Button, Loader, Header, Accordion, Divider, Segment } from 'semantic-ui-react'
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Button, Header, Accordion, Divider, Label, Segment } from 'semantic-ui-react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import scrollToComponent from 'react-scroll-to-component'
|
||||
import { getCustomPopulation, clearPopulations } from '../../redux/populations'
|
||||
import { getCustomPopulationCoursesByStudentnumbers } from '../../redux/populationCourses'
|
||||
import StyledMessage from './StyledMessage'
|
||||
import { getTextIn } from '../../common'
|
||||
import FilterTray from '../FilterTray'
|
||||
import useFilters from '../FilterTray/useFilters'
|
||||
import { CustomPopulationFilters } from '../FilterTray/FilterSets'
|
||||
import useFilterTray from '../FilterTray/useFilterTray'
|
||||
import CreditAccumulationGraphHighCharts from '../CreditAccumulationGraphHighCharts'
|
||||
import PopulationStudents from '../PopulationStudents'
|
||||
import CustomPopulationCourses from '../CustomPopulation/CustomPopulationCourses'
|
||||
import CustomPopulationProgrammeDist from '../CustomPopulation/CustomPopulationProgrammeDist'
|
||||
import InfoBox from '../InfoBox'
|
||||
import infotooltips from '../../common/InfoToolTips'
|
||||
import { getTextIn } from 'common'
|
||||
import { useToggle } from 'common/hooks'
|
||||
import FilterTray from 'components/FilterTray'
|
||||
import useFilters from 'components/FilterTray/useFilters'
|
||||
import PopulationStudents from 'components/PopulationStudents'
|
||||
import CreditAccumulationGraphHighCharts from 'components/CreditAccumulationGraphHighCharts'
|
||||
import { StudyGuidanceGroupFilters } from 'components/FilterTray/FilterSets'
|
||||
import { useGetStudyGuidanceGroupPopulationQuery } from 'redux/studyGuidanceGroups'
|
||||
import { useFilteredAndFormattedElementDetails } from 'redux/elementdetails'
|
||||
import StudyGuidanceGroupPopulationCourses from './StudyGuidanceGroupPopulationCourses'
|
||||
import { startYearToAcademicYear, Wrapper, StyledMessage } from './common'
|
||||
|
||||
const createAcademicYearStartDate = year => new Date(year, 7, 1)
|
||||
|
||||
const takeOnlyCoursesStartingFromGivenAcademicYear = ({ students, year }) => {
|
||||
if (!year) return students
|
||||
const academicYearStartDate = createAcademicYearStartDate(year)
|
||||
return students.map(student => ({
|
||||
...student,
|
||||
courses: student.courses.filter(course => new Date(course.date) > academicYearStartDate),
|
||||
}))
|
||||
}
|
||||
|
||||
const useIsMounted = () => {
|
||||
const isMounted = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true
|
||||
|
||||
return () => {
|
||||
isMounted.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return useCallback(() => isMounted.current, [])
|
||||
}
|
||||
|
||||
const useToggleAndSetNewestIndex = ({ defaultValue, indexOfPanelToScroll, setNewestIndex, isMounted }) => {
|
||||
const [state, toggle] = useToggle(defaultValue)
|
||||
useEffect(() => {
|
||||
if (isMounted()) setNewestIndex(indexOfPanelToScroll)
|
||||
}, [state])
|
||||
return [state, toggle]
|
||||
}
|
||||
|
||||
const SingleStudyGroupContent = ({ population, group, language }) => {
|
||||
const custompop = population?.students || []
|
||||
const isMounted = useIsMounted()
|
||||
const { setAllStudents, filteredStudents } = useFilters()
|
||||
const [trayOpen] = useFilterTray()
|
||||
const selectedStudents = filteredStudents.map(stu => stu.studentNumber)
|
||||
const creditGainRef = useRef()
|
||||
const programmeRef = useRef()
|
||||
const coursesRef = useRef()
|
||||
const studentRef = useRef()
|
||||
const refs = [creditGainRef, programmeRef, coursesRef, studentRef]
|
||||
const [activeIndex, setIndex] = useState([])
|
||||
const [newestIndex, setNewest] = useState(null)
|
||||
const refs = [useRef(), useRef(), useRef(), useRef()]
|
||||
const [activeIndex, setActiveIndex] = useState([])
|
||||
const [newestIndex, setNewestIndex] = useState(null)
|
||||
const [creditsStartingFromAssociatedYear, toggleCreditsStartingFromAssociatedYear] = useToggleAndSetNewestIndex({
|
||||
defaultValue: !!group.tags?.year,
|
||||
indexOfPanelToScroll: 0,
|
||||
setNewestIndex,
|
||||
isMounted,
|
||||
})
|
||||
const [coursesStructuredByProgramme, toggleCoursesStructuredByProgramme] = useToggleAndSetNewestIndex({
|
||||
defaultValue: !!group.tags?.studyProgramme,
|
||||
indexOfPanelToScroll: 1,
|
||||
setNewestIndex,
|
||||
isMounted,
|
||||
})
|
||||
const filteredStudentsWithFilteredCourses = takeOnlyCoursesStartingFromGivenAcademicYear({
|
||||
students: filteredStudents,
|
||||
year: group.tags?.year,
|
||||
})
|
||||
|
||||
const handleClick = index => {
|
||||
const indexes = [...activeIndex].sort()
|
||||
if (indexes.includes(index)) {
|
||||
indexes.splice(
|
||||
indexes.findIndex(ind => ind === index),
|
||||
1
|
||||
)
|
||||
const togglePanel = index => {
|
||||
const currentActiveIndex = new Set(activeIndex)
|
||||
if (currentActiveIndex.has(index)) {
|
||||
currentActiveIndex.delete(index)
|
||||
} else {
|
||||
indexes.push(index)
|
||||
currentActiveIndex.add(index)
|
||||
setNewestIndex(index)
|
||||
}
|
||||
if (activeIndex.length < indexes.length) setNewest(index)
|
||||
else setNewest(null)
|
||||
setIndex(indexes)
|
||||
setActiveIndex([...currentActiveIndex])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setAllStudents(custompop)
|
||||
handleClick(0)
|
||||
}, [custompop])
|
||||
if (newestIndex) scrollToComponent(refs[newestIndex].current, { align: 'bottom' })
|
||||
}, [newestIndex])
|
||||
|
||||
useEffect(() => {
|
||||
if (newestIndex) {
|
||||
scrollToComponent(refs[newestIndex].current, { align: 'bottom' })
|
||||
}
|
||||
}, [activeIndex])
|
||||
setAllStudents(population?.students || [])
|
||||
togglePanel(0)
|
||||
}, [population])
|
||||
|
||||
const panels = [
|
||||
{
|
||||
@@ -65,20 +97,22 @@ const SingleStudyGroupContent = ({ population, group, language }) => {
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
Credit accumulation (for {selectedStudents.length} students)
|
||||
Credit accumulation (for {filteredStudents.length} students)
|
||||
</span>
|
||||
),
|
||||
},
|
||||
onTitleClick: () => handleClick(0),
|
||||
onTitleClick: () => togglePanel(0),
|
||||
content: {
|
||||
content: (
|
||||
<div ref={creditGainRef}>
|
||||
<div ref={refs[0]}>
|
||||
{group.tags?.year && (
|
||||
<Button primary onClick={() => toggleCreditsStartingFromAssociatedYear()}>
|
||||
{creditsStartingFromAssociatedYear ? 'Show all credits' : 'Show starting from associated year'}
|
||||
</Button>
|
||||
)}
|
||||
<CreditAccumulationGraphHighCharts
|
||||
students={filteredStudents}
|
||||
render={false}
|
||||
trayOpen={trayOpen}
|
||||
language={language}
|
||||
startDate={new Date(parseInt(group?.tags?.year, 10), 7, 1).getTime()}
|
||||
students={creditsStartingFromAssociatedYear ? filteredStudentsWithFilteredCourses : filteredStudents}
|
||||
startDate={creditsStartingFromAssociatedYear && createAcademicYearStartDate(group.tags?.year)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
@@ -89,16 +123,20 @@ const SingleStudyGroupContent = ({ population, group, language }) => {
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
Programme distribution
|
||||
Courses of population
|
||||
</span>
|
||||
),
|
||||
},
|
||||
onTitleClick: () => handleClick(1),
|
||||
onTitleClick: () => togglePanel(1),
|
||||
content: {
|
||||
content: (
|
||||
<div ref={programmeRef}>
|
||||
<InfoBox content={infotooltips.PopulationStatistics.ProgrammeDistributionCoursePopulation} />
|
||||
<CustomPopulationProgrammeDist samples={custompop} selectedStudents={selectedStudents} />
|
||||
<div ref={refs[1]}>
|
||||
<StudyGuidanceGroupPopulationCourses
|
||||
selectedStudents={filteredStudents.map(({ studentNumber }) => studentNumber)}
|
||||
showStructured={coursesStructuredByProgramme}
|
||||
toggleShowStructured={toggleCoursesStructuredByProgramme}
|
||||
studyProgramme={group.tags?.studyProgramme}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -108,33 +146,15 @@ const SingleStudyGroupContent = ({ population, group, language }) => {
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
Courses of population
|
||||
Students ({filteredStudents.length})
|
||||
</span>
|
||||
),
|
||||
},
|
||||
onTitleClick: () => handleClick(2),
|
||||
onTitleClick: () => togglePanel(2),
|
||||
content: {
|
||||
content: (
|
||||
<div ref={coursesRef}>
|
||||
<CustomPopulationCourses selectedStudents={selectedStudents} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: {
|
||||
content: (
|
||||
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
|
||||
Students ({selectedStudents.length})
|
||||
</span>
|
||||
),
|
||||
},
|
||||
onTitleClick: () => handleClick(3),
|
||||
content: {
|
||||
content: (
|
||||
<div ref={studentRef}>
|
||||
<PopulationStudents language={language} filteredStudents={filteredStudents} customPopulation />
|
||||
<div ref={refs[2]}>
|
||||
<PopulationStudents customPopulation language={language} filteredStudents={filteredStudents} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -142,78 +162,73 @@ const SingleStudyGroupContent = ({ population, group, language }) => {
|
||||
]
|
||||
|
||||
return (
|
||||
<FilterTray filterSet={<CustomPopulationFilters />}>
|
||||
<Accordion activeIndex={activeIndex} exclusive={false} styled fluid panels={panels} />
|
||||
<FilterTray filterSet={<StudyGuidanceGroupFilters />}>
|
||||
<Segment className="contentSegment">
|
||||
<Accordion activeIndex={activeIndex} exclusive={false} styled fluid panels={panels} />
|
||||
</Segment>
|
||||
</FilterTray>
|
||||
)
|
||||
}
|
||||
|
||||
const SingleStudyGroupViewWrapper = ({ history, groupName, language, children }) => {
|
||||
const dispatch = useDispatch()
|
||||
const SingleStudyGroupViewWrapper = ({ group, language, isLoading, studyProgrammes, children }) => {
|
||||
const history = useHistory()
|
||||
const handleBack = () => {
|
||||
dispatch(clearPopulations())
|
||||
history.push('/studyguidancegroups')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="segmentContainer">
|
||||
<Segment className="contentSegment">
|
||||
<Button icon="arrow circle left" content="Back" onClick={handleBack} />
|
||||
<Divider />
|
||||
<Header size="medium">{getTextIn(groupName, language)}</Header>
|
||||
</Segment>
|
||||
</div>
|
||||
<Wrapper isLoading={isLoading}>
|
||||
<Button icon="arrow circle left" content="Back" onClick={handleBack} />
|
||||
<Divider />
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'baseline' }}>
|
||||
<Header size="medium" style={{ marginRight: 'auto' }}>
|
||||
{group.name && language && getTextIn(group.name, language)}
|
||||
</Header>
|
||||
{group.tags?.studyProgramme && (
|
||||
<Label tag content={studyProgrammes.find(p => p.value === group.tags.studyProgramme)?.text} color="blue" />
|
||||
)}
|
||||
{group.tags?.year && <Label tag content={startYearToAcademicYear(group.tags.year)} color="blue" />}
|
||||
</div>
|
||||
</Wrapper>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SingleStudyGuidanceGroupContainer = ({ groupid }) => {
|
||||
const dispatch = useDispatch()
|
||||
const history = useHistory()
|
||||
const SingleStudyGuidanceGroupContainer = ({ group }) => {
|
||||
const { language } = useSelector(({ settings }) => settings)
|
||||
const { data: groups } = useSelector(({ studyGuidanceGroups }) => studyGuidanceGroups)
|
||||
const group = groups.find(g => g.id === groupid)
|
||||
const groupStudentNumbers = group?.members?.map(({ personStudentNumber }) => personStudentNumber) || []
|
||||
const { data: population, pending } = useSelector(({ populations }) => populations)
|
||||
|
||||
useEffect(() => {
|
||||
const groupStudentNumbers = group?.members?.map(({ personStudentNumber }) => personStudentNumber) || []
|
||||
if (groupStudentNumbers.length === 0 || population?.students?.length > 0) return
|
||||
dispatch(getCustomPopulation({ studentnumberlist: groupStudentNumbers, usingStudyGuidanceGroups: true }))
|
||||
dispatch(
|
||||
getCustomPopulationCoursesByStudentnumbers({
|
||||
studentnumberlist: groupStudentNumbers,
|
||||
usingStudyGuidanceGroups: true,
|
||||
})
|
||||
)
|
||||
}, [])
|
||||
const studyProgrammes = useFilteredAndFormattedElementDetails(language)
|
||||
const { data, isLoading } = useGetStudyGuidanceGroupPopulationQuery(groupStudentNumbers)
|
||||
|
||||
if (!group) {
|
||||
return (
|
||||
<StyledMessage>
|
||||
Couldn't find group with id {groupid}! Please check that id is correct and you have rights to this study
|
||||
guidance group.
|
||||
</StyledMessage>
|
||||
<SingleStudyGroupViewWrapper>
|
||||
<StyledMessage>
|
||||
Couldn't find group with this id! Please check that id is correct and you have rights to this study guidance
|
||||
group.
|
||||
</StyledMessage>
|
||||
</SingleStudyGroupViewWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const isLoading = pending === undefined || pending === true
|
||||
const isLoaded = pending === false && Object.keys(population).length > 0
|
||||
const renderMessage = groupStudentNumbers.length === 0
|
||||
const renderLoader = groupStudentNumbers.length > 0 && isLoading
|
||||
const renderGroup = groupStudentNumbers.length > 0 && isLoaded
|
||||
if (groupStudentNumbers.length === 0) {
|
||||
return (
|
||||
<SingleStudyGroupViewWrapper>
|
||||
<StyledMessage>This study guidance group doesn't contain any students.</StyledMessage>
|
||||
</SingleStudyGroupViewWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SingleStudyGroupViewWrapper history={history} groupName={group.name} language={language}>
|
||||
{renderMessage ? <StyledMessage>This study guidance group doesn't contain any students.</StyledMessage> : null}
|
||||
{renderLoader ? (
|
||||
<Loader active inline="centered">
|
||||
Loading
|
||||
</Loader>
|
||||
) : null}
|
||||
{renderGroup ? <SingleStudyGroupContent population={population} group={group} language={language} /> : null}
|
||||
<SingleStudyGroupViewWrapper
|
||||
group={group}
|
||||
language={language}
|
||||
isLoading={isLoading}
|
||||
studyProgrammes={studyProgrammes}
|
||||
>
|
||||
{!isLoading && <SingleStudyGroupContent population={data} group={group} language={language} />}
|
||||
</SingleStudyGroupViewWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
+93
-104
@@ -1,100 +1,109 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Link, useHistory } from 'react-router-dom'
|
||||
import { Formik } from 'formik'
|
||||
import { Form, Button, Icon } from 'semantic-ui-react'
|
||||
import Datetime from 'react-datetime'
|
||||
import SortableTable from '../SortableTable'
|
||||
import { getTextIn, textAndDescriptionSearch } from '../../common'
|
||||
import StyledMessage from './StyledMessage'
|
||||
import { changeStudyGuidanceGroupTags } from '../../redux/studyGuidanceGroups'
|
||||
import { getElementDetails } from '../../redux/elementdetails'
|
||||
import { useToggle } from '../../common/hooks'
|
||||
import { getTextIn, textAndDescriptionSearch } from 'common'
|
||||
import { useChangeStudyGuidanceGroupTagsMutation } from 'redux/studyGuidanceGroups'
|
||||
import { useFilteredAndFormattedElementDetails } from 'redux/elementdetails'
|
||||
import { useToggle } from 'common/hooks'
|
||||
import SortableTable from 'components/SortableTable'
|
||||
import { startYearToAcademicYear, StyledMessage } from './common'
|
||||
|
||||
const LinkToGroup = ({ group, language }) => (
|
||||
<Link
|
||||
style={{
|
||||
color: 'black',
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '.78571429em .78571429em',
|
||||
}}
|
||||
to={`/studyguidancegroups/${group.id}`}
|
||||
>
|
||||
{getTextIn(group.name, language)}
|
||||
</Link>
|
||||
)
|
||||
const LinkToGroup = ({ group, language }) => {
|
||||
const history = useHistory()
|
||||
const dest = `/studyguidancegroups/${group.id}`
|
||||
return (
|
||||
<Link
|
||||
style={{
|
||||
color: 'black',
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '.78571429em .78571429em',
|
||||
}}
|
||||
to={dest}
|
||||
>
|
||||
{getTextIn(group.name, language)}
|
||||
<Icon color="blue" name="level up alternate" onClick={() => history.push(dest)} />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const prettifyCamelCase = str => {
|
||||
const splitted = str.match(/[A-Za-z][a-z]*/g) || []
|
||||
return splitted.map(w => w.charAt(0).toLowerCase() + w.substring(1)).join(' ')
|
||||
}
|
||||
|
||||
const startYearToAcademicYear = year => {
|
||||
return year === '' || Number.isNaN(year) ? '' : `${year} - ${parseInt(year, 10) + 1}`
|
||||
}
|
||||
const cellWrapper = { display: 'flex', gap: '8px', width: '100%' }
|
||||
const cellContent = { flex: '0 1 50%' }
|
||||
|
||||
const AssociateTagForm = ({ group, tagName, toggleEdit, selectFieldItems }) => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const [changeStudyGuidanceGroupTags, { isLoading }] = useChangeStudyGuidanceGroupTagsMutation()
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<div>
|
||||
<Formik
|
||||
initialValues={{ [tagName]: '' }}
|
||||
onSubmit={values => {
|
||||
dispatch(changeStudyGuidanceGroupTags(group.id, values))
|
||||
toggleEdit()
|
||||
changeStudyGuidanceGroupTags({ groupId: group.id, tags: values })
|
||||
if (group.tags?.[tagName]) toggleEdit()
|
||||
}}
|
||||
validate={values => (!values[tagName] ? { [tagName]: `${tagName} is required` } : {})}
|
||||
>
|
||||
{formik => (
|
||||
<Form onSubmit={formik.handleSubmit} style={{ display: 'flex', gap: '8px' }}>
|
||||
{tagName === 'studyProgramme' ? (
|
||||
<Form.Select
|
||||
name={tagName}
|
||||
search={textAndDescriptionSearch}
|
||||
fluid
|
||||
placeholder={
|
||||
selectFieldItems.find(p => p.value === group.tags?.[tagName])?.text || 'Select study programme'
|
||||
}
|
||||
options={selectFieldItems}
|
||||
closeOnChange
|
||||
value={formik.values[tagName]}
|
||||
onChange={(_, value) => formik.setFieldValue(tagName, value?.value)}
|
||||
/>
|
||||
) : (
|
||||
<Datetime
|
||||
name={tagName}
|
||||
dateFormat="YYYY"
|
||||
timeFormat={false}
|
||||
initialvalue={group.tags?.[tagName]}
|
||||
renderYear={(props, selectableYear) => <td {...props}>{startYearToAcademicYear(selectableYear)}</td>}
|
||||
renderInput={({ value, ...rest }) => {
|
||||
return (
|
||||
<div>
|
||||
<input value={startYearToAcademicYear(value)} placeholder="Select year" {...rest} />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
closeOnSelect
|
||||
value={formik.values[tagName]}
|
||||
onChange={value => formik.setFieldValue(tagName, value?.format('YYYY'))}
|
||||
/>
|
||||
<Form onSubmit={formik.handleSubmit} style={{ ...cellWrapper, alignItems: 'center' }}>
|
||||
<div style={cellContent}>
|
||||
{tagName === 'studyProgramme' ? (
|
||||
<Form.Select
|
||||
name={tagName}
|
||||
search={textAndDescriptionSearch}
|
||||
fluid
|
||||
placeholder={
|
||||
selectFieldItems.find(p => p.value === group.tags?.[tagName])?.text || 'Select study programme'
|
||||
}
|
||||
options={selectFieldItems}
|
||||
closeOnChange
|
||||
value={formik.values[tagName]}
|
||||
onChange={(_, value) => formik.setFieldValue(tagName, value?.value)}
|
||||
/>
|
||||
) : (
|
||||
<Datetime
|
||||
name={tagName}
|
||||
dateFormat="YYYY"
|
||||
timeFormat={false}
|
||||
initialvalue={group.tags?.[tagName]}
|
||||
inputProps={{ readOnly: true }}
|
||||
renderYear={(props, selectableYear) => <td {...props}>{startYearToAcademicYear(selectableYear)}</td>}
|
||||
renderInput={({ value, ...rest }) => {
|
||||
return (
|
||||
<div>
|
||||
<input value={startYearToAcademicYear(value)} placeholder="Select year" {...rest} />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
closeOnSelect
|
||||
value={formik.values[tagName]}
|
||||
onChange={value => formik.setFieldValue(tagName, value?.format('YYYY'))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div style={cellContent}>
|
||||
<Button type="submit" style={{ margin: '0' }} disabled={isLoading}>
|
||||
Add {prettifyCamelCase(tagName)}
|
||||
</Button>
|
||||
</div>
|
||||
{group.tags?.[tagName] && (
|
||||
<div style={cellContent}>
|
||||
<Button icon type="button" style={{ margin: '0' }} onClick={() => toggleEdit()}>
|
||||
Close
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button type="submit" style={{ margin: '0 0 1em 0' }}>
|
||||
Add {prettifyCamelCase(tagName)}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
{group.tags?.[tagName] ? (
|
||||
<Button icon style={{ margin: '0 0 1em 0' }} onClick={() => toggleEdit()}>
|
||||
Close
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -111,40 +120,23 @@ const TagCell = ({ tagName, group, studyProgrammes }) => {
|
||||
throw Error(`Wrong tagname: ${tagName}`)
|
||||
}
|
||||
}
|
||||
const text = getText()
|
||||
|
||||
return group.tags?.[tagName] && !showEdit ? (
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'baseline' }}>
|
||||
<p>{text}</p>
|
||||
<Button type="button" onClick={() => toggleEdit()}>
|
||||
Edit {prettifyCamelCase(tagName)}
|
||||
</Button>
|
||||
<div style={{ ...cellWrapper, alignItems: 'baseline' }}>
|
||||
<p style={{ ...cellContent, textAlign: 'center' }}>{getText()}</p>
|
||||
<div style={cellContent}>
|
||||
<Button type="button" onClick={() => toggleEdit()}>
|
||||
Edit {prettifyCamelCase(tagName)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<AssociateTagForm group={group} tagName={tagName} toggleEdit={toggleEdit} selectFieldItems={studyProgrammes} />
|
||||
)
|
||||
}
|
||||
|
||||
const StudyGuidanceGroupOverview = () => {
|
||||
const dispatch = useDispatch()
|
||||
const StudyGuidanceGroupOverview = ({ groups }) => {
|
||||
const { language } = useSelector(({ settings }) => settings)
|
||||
const { data: groups } = useSelector(({ studyGuidanceGroups }) => studyGuidanceGroups)
|
||||
const { data: elementDetails } = useSelector(({ elementdetails }) => elementdetails)
|
||||
|
||||
useEffect(() => {
|
||||
if (elementDetails && elementDetails.length > 0) return
|
||||
dispatch(getElementDetails())
|
||||
}, [dispatch])
|
||||
|
||||
const studyProgrammesFilteredForDropdown =
|
||||
elementDetails
|
||||
?.filter(elem => elem.code.startsWith('KH') || elem.code.startsWith('MH'))
|
||||
.map(elem => ({
|
||||
key: elem.code,
|
||||
value: elem.code,
|
||||
description: elem.code,
|
||||
text: getTextIn(elem.name, language),
|
||||
})) || []
|
||||
const studyProgrammes = useFilteredAndFormattedElementDetails(language)
|
||||
|
||||
const headers = [
|
||||
{
|
||||
@@ -156,6 +148,7 @@ const StudyGuidanceGroupOverview = () => {
|
||||
style: {
|
||||
padding: '0',
|
||||
},
|
||||
className: 'iconCellNoPointer',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -174,16 +167,12 @@ const StudyGuidanceGroupOverview = () => {
|
||||
key: 'studyProgramme',
|
||||
title: 'Study programme',
|
||||
getRowVal: group => group.tags?.studyProgramme,
|
||||
headerProps: { onClick: null, sorted: null },
|
||||
getRowContent: group => (
|
||||
<TagCell tagName="studyProgramme" studyProgrammes={studyProgrammesFilteredForDropdown} group={group} />
|
||||
),
|
||||
getRowContent: group => <TagCell tagName="studyProgramme" studyProgrammes={studyProgrammes} group={group} />,
|
||||
},
|
||||
{
|
||||
key: 'associatedyear',
|
||||
title: 'Associated starting academic year',
|
||||
getRowVal: group => group.tags?.year,
|
||||
headerProps: { onClick: null, sorted: null },
|
||||
getRowContent: group => <TagCell tagName="year" group={group} />,
|
||||
},
|
||||
]
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Segment, Button } from 'semantic-ui-react'
|
||||
import InfoBox from 'components/InfoBox'
|
||||
import PopulationCourseStatsFlat from 'components/PopulationCourseStats/PopulationCourseStatsFlat'
|
||||
import PopulationCourseStats from 'components/PopulationCourseStats'
|
||||
import useCourseFilter from 'components/FilterTray/filters/Courses/useCourseFilter'
|
||||
import SegmentDimmer from 'components/SegmentDimmer'
|
||||
import { useGetStudyGuidanceGroupPopulationCoursesQuery } from 'redux/studyGuidanceGroups'
|
||||
import { getMandatoryCourses, getMandatoryCourseModules } from 'redux/populationMandatoryCourses'
|
||||
import infotooltips from 'common/InfoToolTips'
|
||||
|
||||
const StudyGuidanceGroupPopulationCourses = ({
|
||||
selectedStudents,
|
||||
showStructured,
|
||||
toggleShowStructured,
|
||||
studyProgramme,
|
||||
}) => {
|
||||
const { setCourses, resetCourses } = useCourseFilter()
|
||||
const dispatch = useDispatch()
|
||||
const mandatoryCourses = useSelector(({ populationMandatoryCourses }) => populationMandatoryCourses.data)
|
||||
const { data: courses, isLoading } = useGetStudyGuidanceGroupPopulationCoursesQuery(selectedStudents)
|
||||
|
||||
useEffect(() => {
|
||||
// ensure mandatory courses are available for course stats structured
|
||||
if (showStructured && studyProgramme && mandatoryCourses.length === 0) {
|
||||
dispatch(getMandatoryCourses(studyProgramme))
|
||||
dispatch(getMandatoryCourseModules(studyProgramme))
|
||||
}
|
||||
}, [showStructured])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) setCourses(courses.coursestatistics)
|
||||
}, [courses])
|
||||
|
||||
useEffect(() => {
|
||||
return resetCourses
|
||||
}, [])
|
||||
|
||||
if (isLoading) return <SegmentDimmer isLoading={isLoading} />
|
||||
return (
|
||||
<Segment basic>
|
||||
<InfoBox content={infotooltips.PopulationStatistics.CoursesOfPopulation} />
|
||||
{studyProgramme && (
|
||||
<Button primary onClick={() => toggleShowStructured()} style={{ marginLeft: '1em' }}>
|
||||
{showStructured ? 'Show courses as flat list' : 'Show by programme structure'}
|
||||
</Button>
|
||||
)}
|
||||
{showStructured ? (
|
||||
<PopulationCourseStats courses={courses} pending={isLoading} selectedStudents={selectedStudents} />
|
||||
) : (
|
||||
<PopulationCourseStatsFlat
|
||||
courses={courses}
|
||||
pending={isLoading}
|
||||
selectedStudents={selectedStudents}
|
||||
showFilter={false}
|
||||
/>
|
||||
)}
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
export default StudyGuidanceGroupPopulationCourses
|
||||
@@ -1,4 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Message } from 'semantic-ui-react'
|
||||
|
||||
export default ({ children }) => <Message style={{ maxWidth: '800px', margin: 'auto' }}>{children}</Message>
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import { Message, Segment } from 'semantic-ui-react'
|
||||
|
||||
export const startYearToAcademicYear = year => {
|
||||
return year === '' || Number.isNaN(year) ? '' : `${year} - ${parseInt(year, 10) + 1}`
|
||||
}
|
||||
|
||||
export const StyledMessage = ({ children }) => (
|
||||
<Message style={{ maxWidth: '800px', margin: 'auto' }}>{children}</Message>
|
||||
)
|
||||
|
||||
export const Wrapper = ({ isLoading, children }) => (
|
||||
<div className="segmentContainer">
|
||||
<Segment loading={isLoading} className="contentSegment">
|
||||
{children}
|
||||
</Segment>
|
||||
</div>
|
||||
)
|
||||
@@ -1,26 +1,16 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useRouteMatch } from 'react-router-dom'
|
||||
import { Header, Loader, Segment } from 'semantic-ui-react'
|
||||
import { useTitle } from '../../common/hooks'
|
||||
import { getStudyGuidanceGroups } from '../../redux/studyGuidanceGroups'
|
||||
import React from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Header } from 'semantic-ui-react'
|
||||
import { useGetAllStudyGuidanceGroupsQuery } from 'redux/studyGuidanceGroups'
|
||||
import { useTitle } from 'common/hooks'
|
||||
import StudyGuidanceGroupOverview from './StudyGuidanceGroupOverview'
|
||||
import SingleStudyGuidanceGroup from './SingleStudyGuidanceGroup'
|
||||
import { Wrapper } from './common'
|
||||
|
||||
const StudyGuidanceGroups = () => {
|
||||
useTitle('Study guidance groups')
|
||||
const dispatch = useDispatch()
|
||||
const { pending, data: groups } = useSelector(({ studyGuidanceGroups }) => studyGuidanceGroups)
|
||||
const match = useRouteMatch('/studyguidancegroups/:groupid')
|
||||
const groupid = match?.params?.groupid
|
||||
|
||||
useEffect(() => {
|
||||
if (groups && groups.length > 0) return
|
||||
dispatch(getStudyGuidanceGroups())
|
||||
}, [dispatch])
|
||||
|
||||
const isLoading = pending === undefined || pending === true
|
||||
const isLoaded = pending === false
|
||||
const { groupid } = useParams()
|
||||
const { data, isLoading } = useGetAllStudyGuidanceGroupsQuery()
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -28,16 +18,9 @@ const StudyGuidanceGroups = () => {
|
||||
<Header className="segmentTitle" size="large">
|
||||
Study guidance groups
|
||||
</Header>
|
||||
<Segment className="contentSegment">
|
||||
{isLoading ? (
|
||||
<Loader active inline="centered">
|
||||
Loading
|
||||
</Loader>
|
||||
) : null}
|
||||
{isLoaded && !groupid ? <StudyGuidanceGroupOverview /> : null}
|
||||
</Segment>
|
||||
</div>
|
||||
<div>{isLoaded && groupid ? <SingleStudyGuidanceGroup groupid={groupid} /> : null}</div>
|
||||
<Wrapper isLoading={isLoading}>{!isLoading && !groupid && <StudyGuidanceGroupOverview groups={data} />}</Wrapper>
|
||||
{!isLoading && groupid && <SingleStudyGuidanceGroup group={data.find(group => group.id === groupid)} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const login = () => ({
|
||||
|
||||
export const logout = () => ({ type: 'LOGOUT_ATTEMPT' })
|
||||
|
||||
const reducer = (state = { pending: false, error: false, token: null }, action) => {
|
||||
const reducer = (state = { pending: false, error: false, token: null, encodedToken: null }, action) => {
|
||||
switch (action.type) {
|
||||
case 'LOGIN_ATTEMPT':
|
||||
return {
|
||||
@@ -31,6 +31,7 @@ const reducer = (state = { pending: false, error: false, token: null }, action)
|
||||
pending: false,
|
||||
error: false,
|
||||
token: decodeToken(action.token),
|
||||
encodedToken: action.token,
|
||||
}
|
||||
|
||||
case 'LOGOUT_ATTEMPT':
|
||||
@@ -46,6 +47,7 @@ const reducer = (state = { pending: false, error: false, token: null }, action)
|
||||
pending: false,
|
||||
error: true,
|
||||
token: null,
|
||||
encodedToken: null,
|
||||
}
|
||||
|
||||
case 'LOGOUT_SUCCESS':
|
||||
@@ -54,6 +56,7 @@ const reducer = (state = { pending: false, error: false, token: null }, action)
|
||||
pending: false,
|
||||
error: false,
|
||||
token: null,
|
||||
encodedToken: null,
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { RTKApi } from 'apiConnection'
|
||||
import { getTextIn } from 'common'
|
||||
import { callController } from '../apiConnection'
|
||||
|
||||
// original elementdetail stuff
|
||||
export const getElementDetails = () => {
|
||||
const route = '/elementdetails/all'
|
||||
const prefix = 'GET_ELEMENTDETAILS_'
|
||||
@@ -31,4 +34,38 @@ const reducer = (state = { data: [] }, action) => {
|
||||
}
|
||||
}
|
||||
|
||||
// RTK query based stuff
|
||||
|
||||
const elementDetailsApi = RTKApi.injectEndpoints({
|
||||
endpoints: builder => ({
|
||||
getAllElementDetails: builder.query({
|
||||
query: () => '/elementdetails/all',
|
||||
providesTags: result => [
|
||||
...result?.map(({ id }) => ({ type: 'StudyGuidanceGroups', id })),
|
||||
{ type: 'StudyGuidanceGroups', id: 'LIST' },
|
||||
],
|
||||
}),
|
||||
}),
|
||||
overrideExisting: false,
|
||||
})
|
||||
|
||||
export const { useGetAllElementDetailsQuery } = elementDetailsApi
|
||||
|
||||
// Returns only newest studyprogrammes and formats them to be used in semantic ui dropdowns
|
||||
export const useFilteredAndFormattedElementDetails = language => {
|
||||
const { data, isLoading } = useGetAllElementDetailsQuery()
|
||||
const filteredAndFormatted = isLoading
|
||||
? []
|
||||
: data
|
||||
.filter(elem => elem.code.startsWith('KH') || elem.code.startsWith('MH'))
|
||||
.map(elem => ({
|
||||
key: elem.code,
|
||||
value: elem.code,
|
||||
description: elem.code,
|
||||
text: getTextIn(elem.name, language),
|
||||
}))
|
||||
|
||||
return filteredAndFormatted
|
||||
}
|
||||
|
||||
export default reducer
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import { handleRequest, handleAuth } from '../apiConnection'
|
||||
import { setupListeners } from '@reduxjs/toolkit/query/react'
|
||||
import { handleRequest, handleAuth, RTKApi } from 'apiConnection'
|
||||
import actionHistory from './actionHistory'
|
||||
import users from './users'
|
||||
import populations from './populations'
|
||||
@@ -40,9 +41,8 @@ import singleCourseStats from './singleCourseStats'
|
||||
import userAccessEmail from './userAccessEmail'
|
||||
import customPopulationSearch from './customPopulationSearch'
|
||||
import coolDataScience from './coolDataScience'
|
||||
import studyGuidanceGroups from './studyGuidanceGroups'
|
||||
|
||||
export default configureStore({
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
actionHistory,
|
||||
users,
|
||||
@@ -84,7 +84,7 @@ export default configureStore({
|
||||
userAccessEmail,
|
||||
customPopulationSearch,
|
||||
coolDataScience,
|
||||
studyGuidanceGroups,
|
||||
[RTKApi.reducerPath]: RTKApi.reducer,
|
||||
},
|
||||
// oodikone is currently too heavy for other middlewares than thunk, but
|
||||
// feel free to take use them at some point if possible
|
||||
@@ -92,5 +92,8 @@ export default configureStore({
|
||||
...getDefaultMiddleware({ immutableCheck: false, serializableCheck: false }),
|
||||
handleRequest,
|
||||
handleAuth,
|
||||
RTKApi.middleware,
|
||||
],
|
||||
})
|
||||
setupListeners(store.dispatch)
|
||||
export default store
|
||||
|
||||
@@ -1,37 +1,43 @@
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { callController, actionTypes } from '../apiConnection'
|
||||
import { initialState, matcherReducers, defaultReducer } from './common'
|
||||
import { RTKApi } from 'apiConnection'
|
||||
|
||||
const baseUrl = '/studyguidancegroups'
|
||||
const getPrefix = 'GET_STUDY_GUIDANCE_GROUPS_'
|
||||
const getTypes = {
|
||||
...actionTypes(getPrefix),
|
||||
}
|
||||
const changeTagsPrefix = 'CHANGE_STUDY_GUIDANCE_GROUP_TAGS_'
|
||||
const changeTagsTypes = {
|
||||
...actionTypes(changeTagsPrefix),
|
||||
}
|
||||
const studyGuidanceGroupsApi = RTKApi.injectEndpoints({
|
||||
endpoints: builder => ({
|
||||
getAllStudyGuidanceGroups: builder.query({
|
||||
query: () => '/studyguidancegroups',
|
||||
providesTags: result => [
|
||||
...result?.map(({ id }) => ({ type: 'StudyGuidanceGroups', id })),
|
||||
{ type: 'StudyGuidanceGroups', id: 'LIST' },
|
||||
],
|
||||
}),
|
||||
changeStudyGuidanceGroupTags: builder.mutation({
|
||||
query: ({ groupId, tags }) => ({
|
||||
url: `/studyguidancegroups/${groupId}/tags`,
|
||||
method: 'PUT',
|
||||
body: tags,
|
||||
}),
|
||||
invalidatesTags: ({ studyGuidanceGroupId }) => [{ type: 'StudyGuidanceGroups', id: studyGuidanceGroupId }],
|
||||
}),
|
||||
getStudyGuidanceGroupPopulation: builder.query({
|
||||
query: studentnumberlist => ({
|
||||
url: '/v3/populationstatisticsbystudentnumbers',
|
||||
method: 'POST',
|
||||
body: { studentnumberlist, usingStudyGuidanceGroups: true },
|
||||
}),
|
||||
}),
|
||||
getStudyGuidanceGroupPopulationCourses: builder.query({
|
||||
query: studentnumberlist => ({
|
||||
url: '/v2/populationstatistics/coursesbystudentnumberlist',
|
||||
method: 'POST',
|
||||
body: { studentnumberlist, usingStudyGuidanceGroups: true },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
overrideExisting: false,
|
||||
})
|
||||
|
||||
export const getStudyGuidanceGroups = () => callController(baseUrl, getPrefix)
|
||||
|
||||
export const changeStudyGuidanceGroupTags = (groupId, tags) =>
|
||||
callController(`${baseUrl}/${groupId}/tags`, changeTagsPrefix, tags, 'put')
|
||||
|
||||
const reducer = createReducer(
|
||||
initialState,
|
||||
{
|
||||
[getTypes.success]: (state, action) => {
|
||||
state.data = action.response
|
||||
},
|
||||
[changeTagsTypes.success]: (state, action) => {
|
||||
state.data.find(group => group.id === action.response.studyGuidanceGroupId).tags = (({
|
||||
studyGuidanceGroupId,
|
||||
...rest
|
||||
}) => rest)(action.response)
|
||||
},
|
||||
},
|
||||
matcherReducers,
|
||||
defaultReducer
|
||||
)
|
||||
|
||||
export default reducer
|
||||
export const {
|
||||
useGetAllStudyGuidanceGroupsQuery,
|
||||
useChangeStudyGuidanceGroupTagsMutation,
|
||||
useGetStudyGuidanceGroupPopulationQuery,
|
||||
useGetStudyGuidanceGroupPopulationCoursesQuery,
|
||||
} = studyGuidanceGroupsApi
|
||||
|
||||
Generated
+3
-3
@@ -6596,9 +6596,9 @@
|
||||
}
|
||||
},
|
||||
"validator": {
|
||||
"version": "13.6.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.6.0.tgz",
|
||||
"integrity": "sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg=="
|
||||
"version": "13.7.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
|
||||
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw=="
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2",
|
||||
|
||||
@@ -358,20 +358,27 @@ const updateAttainments = async (attainments, personIdToStudentNumber, attainmen
|
||||
} else {
|
||||
coursesToBeCreated.set('AY'.concat(course.code), {
|
||||
...course.dataValues,
|
||||
name: course.name,
|
||||
id: course.id.concat('-ay'),
|
||||
code: 'AY'.concat(course.code),
|
||||
substitutions: [course.id],
|
||||
})
|
||||
|
||||
const subs = [...course.substitutions, course.id.concat('-ay')]
|
||||
coursesToBeUpdated.set(course.code, {
|
||||
...course.dataValues,
|
||||
substitutions: subs,
|
||||
})
|
||||
|
||||
courseCodeToAyCodelessId.set('AY'.concat(course.code), course.id.concat('-ay'))
|
||||
const courseProvider = await CourseProvider.findOne({
|
||||
where: {
|
||||
coursecode: course ? course.id : 'AY'.concat(course.code),
|
||||
coursecode: `${course.id}-ay`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!courseProvider) {
|
||||
const mapCourseProvider = courseProviderMapper('AY'.concat(course.code))
|
||||
courseProvidersToBeCreated.push(mapCourseProvider('hy-org-48645785'))
|
||||
const mapCourseProvider = courseProviderMapper(course.id.concat('-ay'))
|
||||
courseProvidersToBeCreated.push(mapCourseProvider({ organisationId: 'hy-org-48645785' }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user