mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e29afedad | ||
|
|
8610bf1084 | ||
|
|
fe49a63408 | ||
|
|
e3fb31ef8f | ||
|
|
f865796771 | ||
|
|
2532b5367e | ||
|
|
eeca5c52fc | ||
|
|
1e940fde73 | ||
|
|
38a2f0a9a8 | ||
|
|
991203e08e | ||
|
|
0c8698ab04 | ||
|
|
57e04434b0 | ||
|
|
f1fcf9661d | ||
|
|
61ab3d9849 | ||
|
|
52db993e1f | ||
|
|
66d4989fee | ||
|
|
0439efddbe | ||
|
|
453511fe5d | ||
|
|
bcaa0086e5 |
@@ -7,11 +7,15 @@ const deleteTag = name => {
|
||||
cy.contains(name).should('not.exist')
|
||||
}
|
||||
|
||||
const getEmptyYears = () => {
|
||||
const getEmptyYears = isAcademicYear => {
|
||||
const thisYear = new Date().getFullYear()
|
||||
const years = []
|
||||
for (let year = thisYear; year > 2021; year--) {
|
||||
years.push(year)
|
||||
if (isAcademicYear) {
|
||||
years.push(`${year} - ${year + 1}`)
|
||||
} else {
|
||||
years.push(year)
|
||||
}
|
||||
}
|
||||
return years
|
||||
}
|
||||
@@ -19,9 +23,11 @@ const getEmptyYears = () => {
|
||||
describe('Studyprogramme overview', () => {
|
||||
describe('when opening programme page with basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme/KH50_005')
|
||||
cy.init('/study-programme')
|
||||
})
|
||||
|
||||
/* Basic information overview -tests*/
|
||||
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Basic information -tab loads', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
@@ -32,31 +38,24 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
})
|
||||
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Studytracks and student populations -tab loads', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
cy.get('[data-cy=Section-StudytrackOverview]')
|
||||
cy.get('[data-cy=Section-StudytrackProgress]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
})
|
||||
|
||||
it('Basic information contains correct students', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, All, Started, Graduated, Cancelled, Transferred Away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0]),
|
||||
[2021, 0, 0, 0, 0, 1, 0],
|
||||
[2020, 12, 10, 36, 16, 3, 2],
|
||||
[2019, 87, 86, 17, 14, 0, 1],
|
||||
[2018, 161, 162, 1, 3, 0, 0],
|
||||
[2017, 171, 173, 0, 0, 0, 0],
|
||||
// [Year, Started, Graduated, Cancelled, Transferred Away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
[2021, 0, 0, 0, 1, 0],
|
||||
[2020, 10, 36, 16, 3, 2],
|
||||
[2019, 86, 17, 14, 0, 1],
|
||||
[2018, 162, 1, 3, 0, 0],
|
||||
[2017, 173, 0, 0, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudentsOfTheStudyprogramme')
|
||||
})
|
||||
|
||||
it('Basic information contains correct credits', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, Total, Major credits, Non-major credits, Transferred credits]
|
||||
@@ -72,6 +71,7 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('Basic information contains correct thesis writers and graduates', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, Graduated, Wrote thesis]
|
||||
@@ -86,6 +86,232 @@ describe('Studyprogramme overview', () => {
|
||||
cy.checkTableStats(tableContents, 'GraduatedAndThesisWritersOfTheProgramme')
|
||||
})
|
||||
|
||||
it('Special studyrights can be excluded and basic data changes accordingly', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen maisteriohjelma').click()
|
||||
cy.get('[data-cy=StudentToggle]').click()
|
||||
const years = getEmptyYears()
|
||||
const studentTableContents = [
|
||||
// [Year, Started, Graduated]
|
||||
...years.map(year => [year, 0, 0]),
|
||||
[2021, 0, 0],
|
||||
[2020, 22, 0],
|
||||
[2019, 12, 0],
|
||||
[2018, 0, 0],
|
||||
[2017, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
|
||||
|
||||
const creditTableContents = [
|
||||
// [Year, Total, Major students credits, Transferred credits]
|
||||
...years.map(year => [year, 0, 0, 0]),
|
||||
[2021, 0, 0, 0],
|
||||
[2020, 117.5, 0, 7.5],
|
||||
[2019, 67, 0, 0],
|
||||
[2018, 0, 0, 0],
|
||||
[2017, 0, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
|
||||
const graduatedTableContents = [
|
||||
// [Year, Graduated, Wrote thesis]
|
||||
...years.map(year => [year, 0, 0]),
|
||||
[2021, 0, 0],
|
||||
[2020, 0, 0],
|
||||
[2019, 0, 0],
|
||||
[2018, 0, 0],
|
||||
[2017, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(graduatedTableContents, 'GraduatedAndThesisWritersOfTheProgramme')
|
||||
})
|
||||
|
||||
it('Year can be changed to academic year, and data changes accordingly', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen maisteriohjelma').click()
|
||||
cy.get('[data-cy=YearToggle]').click()
|
||||
const isAcademicYear = true
|
||||
const years = getEmptyYears(isAcademicYear)
|
||||
const studentTableContents = [
|
||||
// [Year, Started, Graduated, Cancelled, Transferred away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0, 0],
|
||||
['2020 - 2021', 5, 0, 1, 0, 0],
|
||||
['2019 - 2020', 27, 0, 1, 0, 0],
|
||||
['2018 - 2019', 4, 0, 0, 0, 0],
|
||||
['2017 - 2018', 0, 0, 0, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
|
||||
|
||||
const creditTableContents = [
|
||||
// [Year, Total, Major students credits, Non-major students credits, Transferred credits]
|
||||
...years.map(year => [year, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0],
|
||||
['2020 - 2021', 0, 0, 0, 0],
|
||||
['2019 - 2020', 338.5, 0, 331, 7.5],
|
||||
['2018 - 2019', 173, 0, 173, 0],
|
||||
['2017 - 2018', 42, 0, 42, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
})
|
||||
|
||||
it('Basic information graphs render', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
|
||||
cy.get('[data-cy=Graph-StudentsOfTheStudyprogramme')
|
||||
.should('contain', 'Started')
|
||||
.should('contain', 'Graduated')
|
||||
.should('contain', 'Transferred away')
|
||||
|
||||
cy.get('[data-cy=Graph-CreditsProducedByTheStudyprogramme')
|
||||
.should('contain', 'Major students credits')
|
||||
.should('contain', 'Non-major students credits')
|
||||
.should('contain', 'Transferred credits')
|
||||
.should('contain', 788)
|
||||
.should('contain', 1820)
|
||||
|
||||
cy.get('[data-cy=Graph-GraduatedAndThesisWritersOfTheProgramme')
|
||||
.should('contain', 'Graduated students')
|
||||
.should('contain', 'Wrote thesis')
|
||||
.should('contain', 1)
|
||||
.should('contain', 5)
|
||||
.should('contain', 17)
|
||||
.should('contain', 25)
|
||||
.should('contain', 36)
|
||||
.should('contain', 35)
|
||||
|
||||
cy.get('[data-cy=Graph-ProgrammesAfterGraduation')
|
||||
.should('contain', 'Tietojenkäsittelytieteen maisteriohjelma')
|
||||
.should('contain', 'Datatieteen maisteriohjelma')
|
||||
.should('contain', 'Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
.should('contain', 'Kielellisen diversiteetin ja digitaalisten ihmistieteiden maisteriohjelma')
|
||||
.should('contain', 1)
|
||||
.should('contain', 4)
|
||||
.should('contain', 13)
|
||||
})
|
||||
|
||||
/* Studytrack overview -tests*/
|
||||
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Studytracks and student populations -tab loads', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
cy.get('[data-cy=Section-StudytrackOverview]')
|
||||
cy.get('[data-cy=Section-StudytrackProgress]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
})
|
||||
|
||||
it('Students of the studyprogramme are shown correctly', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
const tableContents = [
|
||||
// [Year, All, Started studying, Currently enrolled, Absent, Cancelled, Graduated, Men, Women, Finnish]
|
||||
[
|
||||
'2020 - 2021',
|
||||
12,
|
||||
'100.0 %',
|
||||
10,
|
||||
'83.3 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
10,
|
||||
'83.3 %',
|
||||
2,
|
||||
'16.7 %',
|
||||
12,
|
||||
'100.0 %',
|
||||
],
|
||||
[
|
||||
'2019 - 2020',
|
||||
86,
|
||||
'100.0 %',
|
||||
86,
|
||||
'100.0 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
1,
|
||||
'1.2 %',
|
||||
1,
|
||||
'1.2 %',
|
||||
65,
|
||||
'75.6 %',
|
||||
21,
|
||||
'24.4 %',
|
||||
86,
|
||||
'100.0 %',
|
||||
],
|
||||
[
|
||||
'2018 - 2019',
|
||||
162,
|
||||
'100.0 %',
|
||||
161,
|
||||
'99.4 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
11,
|
||||
'6.8 %',
|
||||
11,
|
||||
'6.8 %',
|
||||
118,
|
||||
'72.8 %',
|
||||
44,
|
||||
'27.2 %',
|
||||
161,
|
||||
'99.4 %',
|
||||
],
|
||||
[
|
||||
'2017 - 2018',
|
||||
171,
|
||||
'100.0 %',
|
||||
171,
|
||||
'100.0 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
21,
|
||||
'12.3 %',
|
||||
42,
|
||||
'24.6 %',
|
||||
135,
|
||||
'78.9 %',
|
||||
36,
|
||||
'21.1 %',
|
||||
166,
|
||||
'97.1 %',
|
||||
],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackOverview')
|
||||
})
|
||||
|
||||
it('Student progress data is shown correctly', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
const tableContents = [
|
||||
['2020 - 2021', 12, 5, 2, 2, 0, 2, 1],
|
||||
['2019 - 2020', 86, 13, 15, 32, 11, 2, 13],
|
||||
['2018 - 2019', 162, 7, 16, 35, 33, 27, 44],
|
||||
['2017 - 2018', 171, 10, 22, 18, 22, 24, 75],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackProgress')
|
||||
})
|
||||
|
||||
/* Tag-tests*/
|
||||
|
||||
it('can create and delete tags for population', () => {
|
||||
const name = `tag-${new Date().getTime()}`
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
|
||||
@@ -106,7 +106,7 @@ Cypress.Commands.add('selectStudyProgramme', name => {
|
||||
})
|
||||
|
||||
Cypress.Commands.add('checkTableStats', (correctStats, tableName) => {
|
||||
cy.get(`[data-cy=${tableName}]>tbody`).within(() => {
|
||||
cy.get(`[data-cy=Table-${tableName}]>tbody`).within(() => {
|
||||
correctStats.forEach((values, trIndex) => {
|
||||
cy.get('tr')
|
||||
.eq(trIndex)
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
const router = require('express').Router()
|
||||
const MandatoryCourses = require('../services/mandatoryCourses')
|
||||
const {
|
||||
productivityStatsForStudytrack,
|
||||
throughputStatsForStudytrack,
|
||||
optionData,
|
||||
} = require('../services/studyprogramme')
|
||||
const { findProgrammeTheses, createThesisCourse, deleteThesisCourse } = require('../services/thesis')
|
||||
const {
|
||||
getProductivity,
|
||||
setProductivity,
|
||||
getThroughput,
|
||||
setThroughput,
|
||||
patchProductivity,
|
||||
patchThroughput,
|
||||
} = require('../services/analyticsService')
|
||||
const { getAllProgrammes, getAllElementDetails } = require('../services/studyrights')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const programmeStatsSince = new Date('2017-07-31')
|
||||
|
||||
router.get('/elementdetails/all', async (req, res) => {
|
||||
const elementdetails = await getAllElementDetails()
|
||||
@@ -29,22 +13,6 @@ router.get('/studyprogrammes', async (req, res) => {
|
||||
res.json(studyrights)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/optiondata', async (req, res) => {
|
||||
const code = req.params.id
|
||||
|
||||
let level
|
||||
if (code.includes('MH')) {
|
||||
level = 'MSC'
|
||||
} else if (code.includes('KH')) {
|
||||
level = 'BSC'
|
||||
} else {
|
||||
return res.json([])
|
||||
}
|
||||
|
||||
const data = await optionData(new Date('2017-07-31'), new Date(), code, level)
|
||||
res.json(data)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/mandatory_courses', async (req, res) => {
|
||||
if (req.params.id) {
|
||||
const codes = await MandatoryCourses.byStudyprogramme(req.params.id)
|
||||
@@ -54,109 +22,6 @@ router.get('/v2/studyprogrammes/:id/mandatory_courses', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/productivity', async (req, res) => {
|
||||
const code = req.params.id
|
||||
if (code) {
|
||||
let data = null
|
||||
try {
|
||||
data = await getProductivity(code)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} productivity`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const stats = await productivityStatsForStudytrack(code, programmeStatsSince)
|
||||
data = await setProductivity(stats)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
|
||||
}
|
||||
}
|
||||
return res.json(data)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/productivity/recalculate', async (req, res) => {
|
||||
const code = req.query.code
|
||||
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
|
||||
res.status(200).end()
|
||||
|
||||
try {
|
||||
if (code.includes('MH') || code.includes('KH')) {
|
||||
const data = await productivityStatsForStudytrack(code, new Date('2017-07-31'))
|
||||
await setProductivity(data)
|
||||
} else {
|
||||
const data = await productivityStatsForStudytrack(code, new Date('2000-07-31'))
|
||||
await setProductivity(data)
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
await patchProductivity({
|
||||
[code]: { status: 'RECALCULATION ERRORED' },
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
|
||||
return
|
||||
}
|
||||
|
||||
logger.error(`Failed to update productivity stats for code: ${code}, reason: ${e.message}`)
|
||||
}
|
||||
logger.info(`Produdtiviy stats recalculation for studyprogramme ${code} done`)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/throughput', async (req, res) => {
|
||||
const code = req.params.id
|
||||
if (code) {
|
||||
const thesisPromise = findProgrammeTheses(req.params.id)
|
||||
|
||||
let data = null
|
||||
try {
|
||||
data = await getThroughput(code)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} throughput`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const result = await throughputStatsForStudytrack(req.params.id, programmeStatsSince.getFullYear())
|
||||
data = await setThroughput(result)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
|
||||
}
|
||||
}
|
||||
return res.json({ ...data, thesis: await thesisPromise })
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/throughput/recalculate', async (req, res) => {
|
||||
const code = req.query.code
|
||||
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
|
||||
res.status(200).end()
|
||||
|
||||
try {
|
||||
if (code.includes('MH') || code.includes('KH')) {
|
||||
const data = await throughputStatsForStudytrack(code, 2017)
|
||||
await setThroughput(data)
|
||||
} else {
|
||||
const data = await throughputStatsForStudytrack(code, 2000)
|
||||
await setThroughput(data)
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATION ERRORED' } })
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
|
||||
return
|
||||
}
|
||||
logger.error(`Failed to update throughput stats for code: ${code}, reason: ${e.message}`)
|
||||
}
|
||||
logger.info(`Throughput stats recalculation for studyprogramme ${code} done`)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/thesis', async (req, res) => {
|
||||
const { id } = req.params
|
||||
if (id) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
const { redisClient } = require('../services/redis')
|
||||
const moment = require('moment')
|
||||
|
||||
const createRedisKeyForProductivity = id => `PRODUCTIVITY_${id}`
|
||||
const createRedisKeyForThroughput = id => `THROUGHPUT_${id}`
|
||||
const createRedisKeyForBasicStats = (id, yearType, specialGroups) => `BASIC_STATS_${id}_${yearType}_${specialGroups}`
|
||||
const createRedisKeyForCreditStats = (id, yearType, specialGroups) => `CREDIT_STATS_${id}_${yearType}_${specialGroups}`
|
||||
const createRedisKeyForGraduationStats = (id, yearType, specialGroups) =>
|
||||
@@ -10,89 +8,6 @@ const createRedisKeyForGraduationStats = (id, yearType, specialGroups) =>
|
||||
const createRedisKeyForStudytrackStats = (id, graduated, specialGroups) =>
|
||||
`STUDYTRACK_STATS_${id}_${graduated}_${specialGroups}`
|
||||
|
||||
const getProductivity = async id => {
|
||||
const redisKey = createRedisKeyForProductivity(id)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return {
|
||||
[id]: JSON.parse(dataFromRedis),
|
||||
}
|
||||
}
|
||||
|
||||
const setProductivity = async data => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForProductivity(id)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return {
|
||||
[id]: dataToRedis,
|
||||
}
|
||||
}
|
||||
|
||||
const patchProductivity = async data => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForProductivity(id)
|
||||
const dataFromRedis = JSON.parse(await redisClient.getAsync(redisKey))
|
||||
const patchedData = {
|
||||
...dataFromRedis,
|
||||
...data,
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(patchedData))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return {
|
||||
[id]: patchedData,
|
||||
}
|
||||
}
|
||||
|
||||
const getThroughput = async id => {
|
||||
const redisKey = createRedisKeyForThroughput(id)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return {
|
||||
[id]: JSON.parse(dataFromRedis),
|
||||
}
|
||||
}
|
||||
|
||||
const setThroughput = async data => {
|
||||
const { id, data: dataToSet } = data
|
||||
const redisKey = createRedisKeyForThroughput(id)
|
||||
const dataToRedis = {
|
||||
data: dataToSet.years,
|
||||
totals: dataToSet.totals,
|
||||
stTotals: dataToSet.stTotals,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return {
|
||||
[id]: dataToRedis,
|
||||
}
|
||||
}
|
||||
|
||||
const patchThroughput = async data => {
|
||||
const [id, dataToPatch] = Object.entries(data)[0]
|
||||
const redisKey = createRedisKeyForThroughput(id)
|
||||
const { status } = dataToPatch
|
||||
const dataFromRedis = JSON.parse(await redisClient.getAsync(redisKey))
|
||||
const patchedData = {
|
||||
...dataFromRedis,
|
||||
status,
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(patchedData))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return {
|
||||
[id]: patchedData,
|
||||
}
|
||||
}
|
||||
|
||||
const getBasicStats = async (id, yearType, specialGroups) => {
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
@@ -174,12 +89,6 @@ const setStudytrackStats = async (data, graduated, specialGroups) => {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getProductivity,
|
||||
setProductivity,
|
||||
patchProductivity,
|
||||
getThroughput,
|
||||
setThroughput,
|
||||
patchThroughput,
|
||||
getBasicStats,
|
||||
setBasicStats,
|
||||
getCreditStats,
|
||||
|
||||
@@ -16,26 +16,26 @@ const {
|
||||
} = require('./newStudyprogramme')
|
||||
const { getYearStartAndEndDates } = require('../util/semester')
|
||||
|
||||
const getAllStats = async ({ studyprogramme, years, isAcademicYear, includeAllSpecials }) => {
|
||||
const { graphStats, tableStats } = getStatsBasis(years)
|
||||
// const getAllStats = async ({ studyprogramme, years, isAcademicYear, includeAllSpecials }) => {
|
||||
// const { graphStats, tableStats } = getStatsBasis(years)
|
||||
|
||||
await Promise.all(
|
||||
years.map(async year => {
|
||||
const { startDate, endDate } = getYearStartAndEndDates(year, isAcademicYear)
|
||||
const studentnumbersOfTheYear = await getCorrectStudentnumbers({
|
||||
codes: [studyprogramme],
|
||||
startDate,
|
||||
endDate,
|
||||
includeAllSpecials,
|
||||
})
|
||||
const all = await allStudyrights(studyprogramme, null, studentnumbersOfTheYear)
|
||||
// await Promise.all(
|
||||
// years.map(async year => {
|
||||
// const { startDate, endDate } = getYearStartAndEndDates(year, isAcademicYear)
|
||||
// const studentnumbersOfTheYear = await getCorrectStudentnumbers({
|
||||
// codes: [studyprogramme],
|
||||
// startDate,
|
||||
// endDate,
|
||||
// includeAllSpecials,
|
||||
// })
|
||||
// const all = await allStudyrights(studyprogramme, null, studentnumbersOfTheYear)
|
||||
|
||||
graphStats[indexOf(years, year)] = all.length
|
||||
tableStats[year] += all.length
|
||||
})
|
||||
)
|
||||
return { graphStats, tableStats }
|
||||
}
|
||||
// graphStats[indexOf(years, year)] = all.length
|
||||
// tableStats[year] += all.length
|
||||
// })
|
||||
// )
|
||||
// return { graphStats, tableStats }
|
||||
// }
|
||||
|
||||
const getStartedStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
|
||||
const { graphStats, tableStats } = getStatsBasis(years)
|
||||
@@ -133,7 +133,6 @@ const getBasicStatsForStudytrack = async ({ studyprogramme, yearType, specialGro
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
|
||||
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials }
|
||||
const all = await getAllStats(queryParameters)
|
||||
const started = await getStartedStats(queryParameters)
|
||||
const graduated = await getGraduatedStats(queryParameters)
|
||||
const cancelled = await getCancelledStats(queryParameters)
|
||||
@@ -146,14 +145,13 @@ const getBasicStatsForStudytrack = async ({ studyprogramme, yearType, specialGro
|
||||
includeAllSpecials
|
||||
? [
|
||||
year,
|
||||
all.tableStats[year],
|
||||
started.tableStats[year],
|
||||
graduated.tableStats[year],
|
||||
cancelled.tableStats[year],
|
||||
transferredAway.tableStats[year],
|
||||
transferredTo.tableStats[year],
|
||||
]
|
||||
: [year, all.tableStats[year], started.tableStats[year], graduated.tableStats[year]]
|
||||
: [year, started.tableStats[year], graduated.tableStats[year]]
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -69,6 +69,8 @@ const getTransferredCreditStats = async ({ studyprogramme, since, years, isAcade
|
||||
return { graphStats, tableStats }
|
||||
}
|
||||
|
||||
// Fetches all credits for the studytrack and combines them into the statistics for the table
|
||||
// and graph in the studyprogramme overview
|
||||
const getCreditStatsForStudytrack = async ({ studyprogramme, yearType, specialGroups }) => {
|
||||
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
|
||||
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
|
||||
|
||||
@@ -258,8 +258,8 @@ const getCreditThresholds = studyprogramme => {
|
||||
|
||||
const tableTitles = {
|
||||
basics: {
|
||||
SPECIAL_EXCLUDED: ['', 'All', 'Started studying', 'Graduated'],
|
||||
SPECIAL_INCLUDED: ['', 'All', 'Started studying', 'Graduated', 'Cancelled', 'Transferred away', 'Transferred to'],
|
||||
SPECIAL_EXCLUDED: ['', 'Started studying', 'Graduated'],
|
||||
SPECIAL_INCLUDED: ['', 'Started studying', 'Graduated', 'Cancelled', 'Transferred away', 'Transferred to'],
|
||||
},
|
||||
credits: {
|
||||
SPECIAL_EXCLUDED: ['', 'Total', 'Major students credits', 'Transferred credits'],
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import PopulationStatistics from './populationStatistics'
|
||||
import PopulationOverview from './populationOverview'
|
||||
import CoolDataScience from './coolDataScience'
|
||||
import CourseStatistics from './courseStatistics'
|
||||
import CoursePopulation from './coursePopulation'
|
||||
@@ -7,7 +6,6 @@ import Studyprogramme from './studyprogramme'
|
||||
|
||||
export default {
|
||||
PopulationStatistics,
|
||||
PopulationOverview,
|
||||
CoolDataScience,
|
||||
CourseStatistics,
|
||||
CoursePopulation,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
export default {
|
||||
PopulationProgress: `Tämä koontisivu sisältää koulutusohjelman kaikkien opiskelijoiden perustiedot lukuvuosittain. Luvut perustuvat opinto-oikeuksiin kyseisessä ohjelmassa.
|
||||
|
||||
Population progress:
|
||||
Vuosittain ohjelmassa aloittaneet, ohjelman keskeyttäneet / peruneet sekä ohjelmaan siirtyneet opiskelijat.
|
||||
Huomattavaa on, että sama opiskelija voi olla kahdessa luokassa, jos hän on esimerkiksi aloittanut opinnot jo, mutta keskeyttänyt ne lukuvuoden aikana.
|
||||
Tarkemmat tiedot saat esille klikkaamalla nuolta kunkin lukuvuoden kohdalla.`,
|
||||
YearlyProductivity: `
|
||||
Yearly productivity:
|
||||
Koulutusohjelman kunakin kalenterivuonna tuottamat opintopisteet, valmistuneiden määrän, ohjelman pääaineopiskelijoiden
|
||||
opintopisteet sekä muiden kuin pääaineopiskelijoiden opintopisteet. Mukana on myös kuhunkin koulutusohjelmaan hyväksiluetut opintopisteet.
|
||||
|
||||
Vanhojen koulutusohjelmien lakkauttaminen ja opiskelijoiden siirtäminen uusiin ohjelmiin voi aiheuttaa tavallista suuremmat opintopistemäärät lukuvuodelle 2020-2021.`,
|
||||
Year: `Lukuvuosi`,
|
||||
Students: `Opiskelijat, jotka ovat koulutusohjelmassa aloittaneet, opinto-oikeutensa peruneet sekä koulutusohjelmaan tai siitä pois siirtyneet.`,
|
||||
Started: `Koulutusohjelmassa kunakin lukuvuonna aloittaneet opiskelijat. EI sisällä ohjelmaan siirtyneitä opiskelijoita.`,
|
||||
Cancelled: `Koulutusohjelmaan oikeuttavan opinto-oikeutensa kunakin lukuvuonna peruttaneet`,
|
||||
Graduated: `Tiettynä lukuvuonna aloittaneista opiskelijoista on valmistunut`,
|
||||
Transferred: `Koulutusohjelmaan kyseisenä lukuvuonna siirtyneet opiskelijat.
|
||||
|
||||
Vanhojen koulutusohjelmien lakkauttaminen aiheuttaa tavallista suuremmat siirtoluvut lukuvuodelle 2020-2021.`,
|
||||
Credits: `Kunakin vuonna aloittaneista opiskelijoista ne, jotka ovat ylittäneet tietyn opintopisterajapyykin`,
|
||||
Thesis: `Loppututkielman kirjoittaneiden määrä`,
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Table, Header, Grid, Segment } from 'semantic-ui-react'
|
||||
import { shape, any, arrayOf, bool, oneOfType, array } from 'prop-types'
|
||||
|
||||
const DIRECTIONS = {
|
||||
ASC: 'ascending', // true
|
||||
DESC: 'descending', // false
|
||||
}
|
||||
|
||||
function sortBy(column, data) {
|
||||
const sortByString = column === 'name' || column === 'code'
|
||||
return data.concat().sort((a, b) => {
|
||||
const keyA = column === 'name' ? a[column].fi : a[column]
|
||||
const keyB = column === 'name' ? b[column].fi : b[column]
|
||||
return sortByString ? keyB.localeCompare(keyA) : keyB - keyA
|
||||
})
|
||||
}
|
||||
|
||||
const BachelorsTable = ({ bachelors, loading }) => {
|
||||
const [bachelorsData, setBachelorsData] = useState({})
|
||||
const [sortColumn, setSortColumn] = useState(null)
|
||||
const [direction, setDirection] = useState(false)
|
||||
|
||||
useEffect(() => setBachelorsData(bachelors), [bachelors])
|
||||
|
||||
useEffect(() => {
|
||||
if (!bachelorsData || !bachelorsData.data) return
|
||||
const data = sortBy(sortColumn, bachelorsData.data)
|
||||
if (direction) data.reverse()
|
||||
setBachelorsData({ ...bachelorsData, data })
|
||||
}, [direction, sortColumn])
|
||||
|
||||
const getSorted = column => {
|
||||
if (column !== sortColumn) return null
|
||||
return direction ? DIRECTIONS.ASC : DIRECTIONS.DESC
|
||||
}
|
||||
|
||||
const setSorted = column => {
|
||||
if (column === sortColumn) setDirection(!direction)
|
||||
else {
|
||||
setSortColumn(column)
|
||||
setDirection(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!loading && (!bachelorsData.data || !bachelorsData.data.length)) return null
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<Grid columns={2}>
|
||||
<Grid.Row>
|
||||
<Grid.Column>Study programme options</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Header>
|
||||
{bachelorsData && bachelorsData.years && `Statistics from ${bachelorsData.years.length} years`}
|
||||
<Segment basic loading={loading} style={{ overflowX: 'auto' }}>
|
||||
<Table celled structured compact striped selectable sortable>
|
||||
{bachelorsData && bachelorsData.data ? (
|
||||
<>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell sorted={getSorted('name')} onClick={() => setSorted('name')}>
|
||||
Name
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell sorted={getSorted('code')} onClick={() => setSorted('code')}>
|
||||
Code
|
||||
</Table.HeaderCell>
|
||||
{bachelorsData.years.map(year => (
|
||||
<Table.HeaderCell sorted={getSorted(year)} onClick={() => setSorted(year)} key={year}>
|
||||
{year}
|
||||
</Table.HeaderCell>
|
||||
))}
|
||||
<Table.HeaderCell sorted={getSorted('total')} onClick={() => setSorted('total')}>
|
||||
Total
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{bachelorsData.data.map(item => (
|
||||
<Table.Row key={item.code}>
|
||||
<Table.Cell>{item.name.fi}</Table.Cell>
|
||||
<Table.Cell>{item.code}</Table.Cell>
|
||||
{bachelorsData.years.map(year => (
|
||||
<Table.Cell key={year}>{item[year]}</Table.Cell>
|
||||
))}
|
||||
<Table.Cell>{item.total}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</>
|
||||
) : null}
|
||||
</Table>
|
||||
</Segment>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
BachelorsTable.propTypes = {
|
||||
bachelors: oneOfType([
|
||||
shape({
|
||||
data: arrayOf(any),
|
||||
years: arrayOf(any),
|
||||
}),
|
||||
array,
|
||||
]),
|
||||
loading: bool.isRequired,
|
||||
}
|
||||
|
||||
BachelorsTable.defaultProps = {
|
||||
bachelors: null,
|
||||
}
|
||||
|
||||
export default BachelorsTable
|
||||
+2
-2
@@ -4,7 +4,7 @@ const ReactHighcharts = require('react-highcharts')
|
||||
|
||||
const colors = ['#003E65', '#1392c2', '#036415']
|
||||
|
||||
const BarChart = ({ data }) => {
|
||||
const BarChart = ({ cypress, data }) => {
|
||||
const dataWithColors = data?.graphStats?.map((series, index) => ({ ...series, color: colors[index] }))
|
||||
|
||||
const defaultConfig = {
|
||||
@@ -51,7 +51,7 @@ const BarChart = ({ data }) => {
|
||||
|
||||
if (!data) return <></>
|
||||
return (
|
||||
<div className="graph-container">
|
||||
<div className="graph-container" data-cy={`Graph-${cypress}`}>
|
||||
<ReactHighcharts config={defaultConfig} />
|
||||
</div>
|
||||
)
|
||||
+1
-1
@@ -6,7 +6,7 @@ const DataTable = ({ data, cypress, titles, wideTable }) => {
|
||||
|
||||
return (
|
||||
<div className={`table-container${wideTable ? '-wide' : ''}`}>
|
||||
<Table data-cy={cypress} celled>
|
||||
<Table data-cy={`Table-${cypress}`} celled>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{titles?.map(title => (
|
||||
+2
-2
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
|
||||
const ReactHighcharts = require('react-highcharts')
|
||||
|
||||
const GaugeChart = ({ data, year, graduationAmount, studyprogramme }) => {
|
||||
const GaugeChart = ({ cypress, data, year, graduationAmount, studyprogramme }) => {
|
||||
if (!data || !graduationAmount || !data.length) return null
|
||||
|
||||
const thresholdValues = studyprogramme.includes('KH') ? [36, 41] : [24, 27]
|
||||
@@ -76,7 +76,7 @@ const GaugeChart = ({ data, year, graduationAmount, studyprogramme }) => {
|
||||
],
|
||||
}
|
||||
|
||||
return <ReactHighcharts config={defaultConfig} />
|
||||
return <ReactHighcharts data-cy={`Graph-${cypress}`} config={defaultConfig} />
|
||||
}
|
||||
|
||||
export default GaugeChart
|
||||
+2
-2
@@ -7,7 +7,7 @@ require('highcharts-exporting')(ReactHighcharts.Highcharts)
|
||||
|
||||
const colors = ['#003E65', '#1392c2', '#E68825', '#333737', '#036415']
|
||||
|
||||
const LineGraph = ({ data }) => {
|
||||
const LineGraph = ({ cypress, data }) => {
|
||||
const dataWithColors = data?.graphStats.map((series, index) => ({
|
||||
...series,
|
||||
color: colors[index],
|
||||
@@ -49,7 +49,7 @@ const LineGraph = ({ data }) => {
|
||||
|
||||
if (!data) return <></>
|
||||
return (
|
||||
<div className="graph-container">
|
||||
<div className="graph-container" data-cy={`Graph-${cypress}`}>
|
||||
<ReactHighcharts config={defaultConfig} />
|
||||
</div>
|
||||
)
|
||||
+2
-2
@@ -4,7 +4,7 @@ const ReactHighcharts = require('react-highcharts')
|
||||
|
||||
const colors = ['#003E65', '#1392c2', '#036415', '#bdc2c7', '#E68825', '#333737', '#54997b', '#195f8a']
|
||||
|
||||
const StackedBarChart = ({ data, labels, wideTable }) => {
|
||||
const StackedBarChart = ({ cypress, data, labels, wideTable }) => {
|
||||
const dataWithColors = data?.map((series, index) => ({ ...series, color: colors[index] }))
|
||||
|
||||
const defaultConfig = {
|
||||
@@ -55,7 +55,7 @@ const StackedBarChart = ({ data, labels, wideTable }) => {
|
||||
|
||||
if (!data) return <></>
|
||||
return (
|
||||
<div className={`graph-container${wideTable ? '-narrow' : ''}`}>
|
||||
<div className={`graph-container${wideTable ? '-narrow' : ''}`} data-cy={`Graph-${cypress}`}>
|
||||
<ReactHighcharts config={defaultConfig} />
|
||||
</div>
|
||||
)
|
||||
+21
-10
@@ -13,10 +13,10 @@ import InfoBox from '../../Info/InfoBox'
|
||||
import InfotoolTips from '../../../common/InfoToolTips'
|
||||
import '../studyprogramme.css'
|
||||
|
||||
const getRadioButton = (toolTip, firstLabel, secondLabel, value, setValue) => (
|
||||
const getRadioButton = (cypress, toolTip, firstLabel, secondLabel, value, setValue) => (
|
||||
<div className="radio-toggle">
|
||||
<label className="toggle-label">{firstLabel}</label>
|
||||
<Radio toggle checked={value} onChange={() => setValue(!value)} />
|
||||
<Radio data-cy={cypress} toggle checked={value} onChange={() => setValue(!value)} />
|
||||
{toolTip ? (
|
||||
<WithHelpTooltip tooltip={{ short: toolTip }}>
|
||||
<label className="toggle-label">{secondLabel}</label>
|
||||
@@ -27,9 +27,7 @@ const getRadioButton = (toolTip, firstLabel, secondLabel, value, setValue) => (
|
||||
</div>
|
||||
)
|
||||
|
||||
const Overview = ({ studyprogramme }) => {
|
||||
const [academicYear, setAcademicYear] = useState(false)
|
||||
const [specialGroups, setSpecialGroups] = useState(false)
|
||||
const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYear, setAcademicYear }) => {
|
||||
const [showMeanTime, setShowMeanTime] = useState(true)
|
||||
const toolTips = InfotoolTips.Studyprogramme
|
||||
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
|
||||
@@ -71,8 +69,16 @@ const Overview = ({ studyprogramme }) => {
|
||||
return (
|
||||
<div className="studyprogramme-overview">
|
||||
<div className="toggle-container">
|
||||
{getRadioButton(toolTips.YearToggle, 'Calendar year ', 'Academic year', academicYear, setAcademicYear)}
|
||||
{getRadioButton(
|
||||
'YearToggle',
|
||||
toolTips.YearToggle,
|
||||
'Calendar year ',
|
||||
'Academic year',
|
||||
academicYear,
|
||||
setAcademicYear
|
||||
)}
|
||||
{getRadioButton(
|
||||
'StudentToggle',
|
||||
toolTips.StudentToggle,
|
||||
'All studyrights included',
|
||||
'Special studyrights excluded',
|
||||
@@ -94,7 +100,7 @@ const Overview = ({ studyprogramme }) => {
|
||||
<>
|
||||
{getDivider('Students of the studyprogramme', 'StudentsOfTheStudyprogramme')}
|
||||
<div className="section-container">
|
||||
<LineGraph data={basics?.data} />
|
||||
<LineGraph cypress="StudentsOfTheStudyprogramme" data={basics?.data} />
|
||||
<DataTable
|
||||
cypress="StudentsOfTheStudyprogramme"
|
||||
data={basics?.data?.tableStats}
|
||||
@@ -107,7 +113,11 @@ const Overview = ({ studyprogramme }) => {
|
||||
<>
|
||||
{getDivider('Credits produced by the studyprogramme', 'CreditsProducedByTheStudyprogramme')}
|
||||
<div className="section-container">
|
||||
<StackedBarChart data={credits?.data?.graphStats} labels={credits?.data?.years} />
|
||||
<StackedBarChart
|
||||
cypress="CreditsProducedByTheStudyprogramme"
|
||||
data={credits?.data?.graphStats}
|
||||
labels={credits?.data?.years}
|
||||
/>
|
||||
<DataTable
|
||||
cypress="CreditsProducedByTheStudyprogramme"
|
||||
data={credits?.data?.tableStats}
|
||||
@@ -120,7 +130,7 @@ const Overview = ({ studyprogramme }) => {
|
||||
<>
|
||||
{getDivider('Graduated and thesis writers of the programme', 'GraduatedAndThesisWritersOfTheProgramme')}
|
||||
<div className="section-container">
|
||||
<BarChart data={graduations?.data} />
|
||||
<BarChart cypress="GraduatedAndThesisWritersOfTheProgramme" data={graduations?.data} />
|
||||
<DataTable
|
||||
cypress="GraduatedAndThesisWritersOfTheProgramme"
|
||||
data={graduations?.data?.tableStats}
|
||||
@@ -128,7 +138,7 @@ const Overview = ({ studyprogramme }) => {
|
||||
/>
|
||||
</div>
|
||||
{getDivider('Average graduation times', 'AverageGraduationTimes')}
|
||||
{getRadioButton(null, 'Mean time', 'Median time', showMeanTime, setShowMeanTime)}
|
||||
{getRadioButton('GraduationTimeToggle', null, 'Mean time', 'Median time', showMeanTime, setShowMeanTime)}
|
||||
<div className="section-container-centered">
|
||||
{graduations?.data?.years.map(year => (
|
||||
<GaugeChart
|
||||
@@ -150,6 +160,7 @@ const Overview = ({ studyprogramme }) => {
|
||||
{getDivider('Primary master programme studies after this programme', 'ProgrammesAfterGraduation')}
|
||||
<div className="section-container">
|
||||
<StackedBarChart
|
||||
cypress="ProgrammesAfterGraduation"
|
||||
wideTable
|
||||
data={graduations?.data?.programmesAfterGraphStats}
|
||||
labels={graduations?.data?.years}
|
||||
@@ -1,62 +0,0 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import ProductivityTable from '../ProductivityTable'
|
||||
import ThroughputTable from '../ThroughputTable'
|
||||
import BachelorsTable from '../BachelorsTable'
|
||||
import { getProductivity } from '../../../redux/productivity'
|
||||
import { getThroughput } from '../../../redux/throughput'
|
||||
import { getBachelors } from '../../../redux/studyProgrammeBachelors'
|
||||
import { isNewHYStudyProgramme } from '../../../common'
|
||||
|
||||
const Overview = props => {
|
||||
const {
|
||||
productivity,
|
||||
throughput,
|
||||
bachelors,
|
||||
studyprogramme,
|
||||
dispatchGetProductivity,
|
||||
dispatchGetThroughput,
|
||||
dispatchGetBachelors,
|
||||
history,
|
||||
} = props
|
||||
|
||||
useEffect(() => {
|
||||
dispatchGetProductivity(studyprogramme)
|
||||
dispatchGetThroughput(studyprogramme)
|
||||
dispatchGetBachelors(studyprogramme)
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
<ThroughputTable
|
||||
throughput={throughput.data[studyprogramme]}
|
||||
thesis={throughput.data.thesis}
|
||||
loading={throughput.pending}
|
||||
error={throughput.error}
|
||||
studyprogramme={studyprogramme}
|
||||
history={history}
|
||||
newProgramme={isNewHYStudyProgramme(studyprogramme)}
|
||||
/>
|
||||
<ProductivityTable
|
||||
productivity={productivity.data[studyprogramme]}
|
||||
thesis={throughput.data.thesis}
|
||||
loading={productivity.pending}
|
||||
error={productivity.error}
|
||||
showCredits={isNewHYStudyProgramme(studyprogramme)}
|
||||
newProgramme={isNewHYStudyProgramme(studyprogramme)}
|
||||
/>
|
||||
<BachelorsTable bachelors={bachelors.data} loading={throughput.pending} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ studyProgrammeProductivity, studyProgrammeThroughput, studyProgrammeBachelors }) => ({
|
||||
productivity: studyProgrammeProductivity,
|
||||
throughput: studyProgrammeThroughput,
|
||||
bachelors: studyProgrammeBachelors,
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
dispatchGetProductivity: getProductivity,
|
||||
dispatchGetThroughput: getThroughput,
|
||||
dispatchGetBachelors: getBachelors,
|
||||
})(Overview)
|
||||
@@ -1,162 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import moment from 'moment'
|
||||
import { Table, Header, Grid, Label, Segment, Dropdown } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import { shape, number, arrayOf, bool, string, oneOfType } from 'prop-types'
|
||||
import { getProductivity } from '../../../redux/productivity'
|
||||
import InfoBox from '../../Info/InfoBox'
|
||||
import infotooltips from '../../../common/InfoToolTips'
|
||||
import './productivityTable.css'
|
||||
|
||||
const ProductivityTable = ({ productivity, thesis, loading, error, showCredits, newProgramme }) => {
|
||||
const [selectedYear, setYear] = useState(newProgramme ? 2017 : null)
|
||||
|
||||
const years = productivity
|
||||
? productivity.data
|
||||
.map(stats => ({ key: stats.year, text: stats.year, value: stats.year }))
|
||||
.sort((year1, year2) => Number(year2.value) - Number(year1.value))
|
||||
: []
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedYear && years.length > 5) {
|
||||
setYear(years[5].value)
|
||||
} else if (!selectedYear && years.length > 1) {
|
||||
setYear(years[years.length - 1].value)
|
||||
}
|
||||
}, [years])
|
||||
|
||||
if (error) return <h1>Oh no so error {error}</h1>
|
||||
let thesisTypes = []
|
||||
if (thesis) {
|
||||
thesisTypes = thesis.map(t => t.thesisType)
|
||||
}
|
||||
|
||||
const headerList = [
|
||||
'Year',
|
||||
'Credits',
|
||||
thesisTypes.includes('MASTER') && 'Masters Thesis',
|
||||
thesisTypes.includes('BACHELOR') && 'Bachelors Thesis',
|
||||
'Graduated',
|
||||
'Credits for studyprogramme majors',
|
||||
'Credits for non major students',
|
||||
'Hyväksiluettu (not included in Credits column)',
|
||||
].filter(_ => _)
|
||||
|
||||
const handleChange = (event, { value }) => {
|
||||
event.preventDefault()
|
||||
setYear(value)
|
||||
}
|
||||
|
||||
const creditCell = content =>
|
||||
showCredits ? (
|
||||
<Table.Cell>{content}</Table.Cell>
|
||||
) : (
|
||||
<Table.Cell
|
||||
className="productivity-table__not-available"
|
||||
title="Credits productivity statistics not available for old programmes"
|
||||
>
|
||||
Not available
|
||||
</Table.Cell>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ marginTop: '2em' }}>
|
||||
<InfoBox content={infotooltips.PopulationOverview.YearlyProductivity} />
|
||||
</div>
|
||||
<Header>
|
||||
<Grid columns={2}>
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
Yearly productivity
|
||||
{productivity && (
|
||||
<Header.Subheader data-cy="productivityUpdateStatus">
|
||||
{`Last updated ${
|
||||
productivity.lastUpdated
|
||||
? moment(productivity.lastUpdated).format('HH:mm:ss MM-DD-YYYY')
|
||||
: 'unknown'
|
||||
}`}
|
||||
{productivity.status === 'RECALCULATING' && (
|
||||
<Label content="Recalculating! Refresh page in a few minutes" color="red" />
|
||||
)}
|
||||
</Header.Subheader>
|
||||
)}
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Header>
|
||||
<div>Statistics from selected year onwards</div>
|
||||
{years.length > 4 ? <Dropdown options={years} onChange={handleChange} selection value={selectedYear} /> : null}
|
||||
<Segment basic loading={loading} style={{ overflowX: 'auto' }}>
|
||||
<Table structured celled compact striped selectable className="fixed-header">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{headerList.map(header => (
|
||||
<Table.HeaderCell key={header}>{header}</Table.HeaderCell>
|
||||
))}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{productivity && productivity.data
|
||||
? productivity.data
|
||||
.filter(stats => Number(stats.year) >= Number(selectedYear))
|
||||
.sort((year1, year2) => year2.year - year1.year)
|
||||
.map(year => (
|
||||
<Table.Row key={year.year}>
|
||||
<Table.Cell>{year.year}</Table.Cell>
|
||||
{creditCell(year.credits.toFixed(2))}
|
||||
{thesisTypes.includes('BACHELOR') && <Table.Cell>{year.bThesis}</Table.Cell>}
|
||||
{thesisTypes.includes('MASTER') && <Table.Cell>{year.mThesis}</Table.Cell>}
|
||||
<Table.Cell>{year.graduated}</Table.Cell>
|
||||
{creditCell(year.creditsForMajors.toFixed(2))}
|
||||
{creditCell((year.credits - year.creditsForMajors).toFixed(2))}
|
||||
{creditCell(year.transferredCredits.toFixed(2))}
|
||||
</Table.Row>
|
||||
))
|
||||
: null}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Segment>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ProductivityTable.propTypes = {
|
||||
productivity: shape({
|
||||
lastUpdated: string,
|
||||
status: string,
|
||||
data: arrayOf(
|
||||
shape({
|
||||
year: oneOfType([number, string]),
|
||||
credits: number,
|
||||
mThesis: number,
|
||||
bThesis: number,
|
||||
graduated: number,
|
||||
creditsForMajors: number,
|
||||
transferredCredits: number,
|
||||
})
|
||||
),
|
||||
}),
|
||||
thesis: arrayOf(
|
||||
shape({
|
||||
programmeCode: string,
|
||||
courseCode: string,
|
||||
thesisType: string,
|
||||
createdAt: string,
|
||||
updatedAt: string,
|
||||
})
|
||||
),
|
||||
loading: bool.isRequired,
|
||||
error: bool.isRequired,
|
||||
showCredits: bool.isRequired,
|
||||
newProgramme: bool.isRequired,
|
||||
}
|
||||
|
||||
ProductivityTable.defaultProps = {
|
||||
productivity: null,
|
||||
thesis: undefined,
|
||||
}
|
||||
|
||||
export default connect(null, {
|
||||
dispatchGetProductivity: getProductivity,
|
||||
})(ProductivityTable)
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
.productivity-table__not-available {
|
||||
color: rgba(40, 40, 40, 0.3);
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { string, func, shape, bool } from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { uniqBy } from 'lodash'
|
||||
import { Dropdown, Tab, Message, Button } from 'semantic-ui-react'
|
||||
import ThroughputTable from '../ThroughputTable'
|
||||
import { getProductivity } from '../../../redux/productivity'
|
||||
import { getThroughput } from '../../../redux/throughput'
|
||||
import { isNewHYStudyProgramme, textAndDescriptionSearch, getTextIn } from '../../../common'
|
||||
import ProtoC from '../../CoolDataScience/ProtoC'
|
||||
|
||||
const Overview = ({
|
||||
language,
|
||||
throughput,
|
||||
studyprogramme,
|
||||
dispatchGetProductivity,
|
||||
dispatchGetThroughput,
|
||||
history,
|
||||
studyprogrammes,
|
||||
}) => {
|
||||
const [selectedTrack, setTrack] = useState('')
|
||||
const [selectedYear, setYear] = useState('')
|
||||
const [throughputData, setData] = useState(null)
|
||||
const [studytrackOptions, setStudytrackOptions] = useState([])
|
||||
const [yearOptions, setYearOptions] = useState([])
|
||||
const [activeIndex, setIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
dispatchGetProductivity(studyprogramme)
|
||||
dispatchGetThroughput(studyprogramme)
|
||||
}, [])
|
||||
|
||||
// set dropdown options
|
||||
useEffect(() => {
|
||||
if (throughput.data[studyprogramme]) {
|
||||
const years = throughput.data[studyprogramme].data.map(data => data.year)
|
||||
const filteredStudytracks = years.reduce((acc, curr) => {
|
||||
if (studyprogrammes[studyprogramme].enrollmentStartYears[curr.slice(0, 4)])
|
||||
acc.push(...Object.values(studyprogrammes[studyprogramme].enrollmentStartYears[curr.slice(0, 4)].studyTracks))
|
||||
return uniqBy(acc, 'code')
|
||||
}, [])
|
||||
|
||||
const yearDropdownOptions = years.map(year => ({
|
||||
key: year,
|
||||
text: year,
|
||||
value: year,
|
||||
}))
|
||||
|
||||
const dropdownOptions = filteredStudytracks.map(st => ({
|
||||
key: st.code,
|
||||
text: getTextIn(st.name, language),
|
||||
description: st.code,
|
||||
value: st.code,
|
||||
}))
|
||||
if (yearDropdownOptions.length > 0) {
|
||||
setYear(yearDropdownOptions[0].value)
|
||||
}
|
||||
setYearOptions(yearDropdownOptions)
|
||||
setStudytrackOptions(dropdownOptions)
|
||||
}
|
||||
}, [throughput, studyprogrammes])
|
||||
|
||||
// mankel throughput data after selecting studytrack
|
||||
useEffect(() => {
|
||||
if (throughput.data[studyprogramme] && selectedTrack) {
|
||||
if (throughput.data[studyprogramme].data[0].studytrackdata) {
|
||||
const selectedData = throughput.data[studyprogramme].data.reduce((acc, curr) => {
|
||||
if (curr.studytrackdata[selectedTrack]) acc.push(curr.studytrackdata[selectedTrack])
|
||||
return acc
|
||||
}, [])
|
||||
const newData = {
|
||||
data: selectedData,
|
||||
lastUpdated: throughput.data[studyprogramme].lastUpdated,
|
||||
status: throughput.data[studyprogramme].status,
|
||||
totals: throughput.data[studyprogramme].stTotals[selectedTrack],
|
||||
}
|
||||
setData(newData)
|
||||
}
|
||||
}
|
||||
}, [selectedTrack, activeIndex])
|
||||
|
||||
// mankel throughput data after selecting a year
|
||||
useEffect(() => {
|
||||
if (throughput.data[studyprogramme] && selectedYear) {
|
||||
if (throughput.data[studyprogramme].data[0].studytrackdata) {
|
||||
const selectedYearData = throughput.data[studyprogramme].data.find(data => data.year === selectedYear)
|
||||
const filteredStudytracks = Object.values(studyprogrammes[studyprogramme].enrollmentStartYears).reduce(
|
||||
(acc, curr) => {
|
||||
acc.push(...Object.values(curr.studyTracks))
|
||||
return uniqBy(acc, 'code')
|
||||
},
|
||||
[]
|
||||
)
|
||||
const studytrackYearData = Object.keys(selectedYearData.studytrackdata).reduce((acc, curr) => {
|
||||
const studytrack = filteredStudytracks.find(st => st.code === curr)
|
||||
if (studytrack) {
|
||||
const newStudyTrackObject = {
|
||||
...selectedYearData.studytrackdata[curr],
|
||||
// oh pls no, pls fix asap
|
||||
// need to fix logic in throughputtable component so that we can name this better
|
||||
year: `${getTextIn(studytrack.name, language)}, ${curr}`,
|
||||
}
|
||||
acc.push(newStudyTrackObject)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
const newData = {
|
||||
data: studytrackYearData,
|
||||
lastUpdated: throughput.data[studyprogramme].lastUpdated,
|
||||
status: throughput.data[studyprogramme].status,
|
||||
totals: {
|
||||
...selectedYearData,
|
||||
credits: selectedYearData.creditValues,
|
||||
genders: {
|
||||
Male: Number(selectedYearData.genders.Male) || 0,
|
||||
Female: Number(selectedYearData.genders.Female) || 0,
|
||||
Other: Number(selectedYearData.genders.Other) || 0,
|
||||
},
|
||||
students:
|
||||
(Number(selectedYearData.genders.Male) || 0) +
|
||||
(Number(selectedYearData.genders.Female) || 0) +
|
||||
(Number(selectedYearData.genders.Other) || 0),
|
||||
},
|
||||
}
|
||||
setData(newData)
|
||||
}
|
||||
}
|
||||
}, [selectedYear, activeIndex])
|
||||
|
||||
const handleStudytrackChange = (event, { value }) => {
|
||||
event.preventDefault()
|
||||
setTrack(value)
|
||||
}
|
||||
|
||||
const handleYearChange = (event, { value }) => {
|
||||
event.preventDefault()
|
||||
setYear(value)
|
||||
}
|
||||
|
||||
const renderSelection = year => {
|
||||
if (year && yearOptions.length < 10)
|
||||
return (
|
||||
<>
|
||||
<h3>Year selection</h3>
|
||||
<Button.Group>
|
||||
{yearOptions.map(option => (
|
||||
<Button
|
||||
key={option.key}
|
||||
content={option.text}
|
||||
style={{ borderRadius: '1px' }}
|
||||
onClick={e => handleYearChange(e, option)}
|
||||
active={option.value === selectedYear}
|
||||
color={option.value === selectedYear ? 'blue' : 'black'}
|
||||
basic={option.value !== selectedYear}
|
||||
/>
|
||||
))}
|
||||
</Button.Group>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<h3>{year ? 'Year' : 'Studytrack'} selection</h3>
|
||||
<Dropdown
|
||||
options={year ? yearOptions : studytrackOptions}
|
||||
onChange={year ? handleYearChange : handleStudytrackChange}
|
||||
defaultValue={year ? yearOptions[0].value : studytrackOptions[0].value}
|
||||
selection
|
||||
placeholder={`Select ${year ? 'year' : ' studytrack'}`}
|
||||
search={textAndDescriptionSearch}
|
||||
fluid
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const renderTable = year => {
|
||||
if (!throughputData || throughputData.data.length < 1) {
|
||||
return (
|
||||
<>
|
||||
{renderSelection(year)}
|
||||
<h2>No {year ? 'year' : 'studytrack'} selected or no data available for selection</h2>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{renderSelection(year)}
|
||||
<ThroughputTable
|
||||
throughput={throughputData}
|
||||
thesis={throughput.data.thesis}
|
||||
loading={throughput.pending}
|
||||
error={throughput.error}
|
||||
studyprogramme={studyprogramme}
|
||||
studytrack={selectedTrack}
|
||||
history={history}
|
||||
newProgramme={isNewHYStudyProgramme(studyprogramme)}
|
||||
isStudytrackView={year}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
const handleTabChange = (event, { activeIndex }) => {
|
||||
setData(null)
|
||||
if (activeIndex === 1) {
|
||||
setTrack(studytrackOptions[0].value)
|
||||
setYear('')
|
||||
} else if (activeIndex === 0) {
|
||||
setYear(yearOptions[0].value)
|
||||
setTrack('')
|
||||
}
|
||||
setIndex(activeIndex)
|
||||
}
|
||||
|
||||
const panes = [
|
||||
{
|
||||
menuItem: 'Year specific',
|
||||
render: () => renderTable(true),
|
||||
},
|
||||
{
|
||||
menuItem: 'Studytrack specific',
|
||||
render: () => renderTable(false),
|
||||
},
|
||||
]
|
||||
|
||||
if (studyprogramme.includes('KH'))
|
||||
panes.push({
|
||||
menuItem: 'Studytrack Graph',
|
||||
render: () => <ProtoC programme={studyprogramme} />,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Message
|
||||
content="Here you can see statistics on studytrack level. In year specific tab you can compare statistics for all studytracks within selected year.
|
||||
In studytrack specific tab you can compare different years from one selected studytrack."
|
||||
/>
|
||||
<Tab panes={panes} activeIndex={activeIndex} onTabChange={handleTabChange} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Overview.propTypes = {
|
||||
studyprogramme: string.isRequired,
|
||||
dispatchGetProductivity: func.isRequired,
|
||||
dispatchGetThroughput: func.isRequired,
|
||||
history: shape({}).isRequired,
|
||||
studyprogrammes: shape({}).isRequired,
|
||||
language: string.isRequired,
|
||||
throughput: shape({
|
||||
error: bool,
|
||||
pending: bool,
|
||||
data: shape({}),
|
||||
}).isRequired // eslint-disable-line
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ studyProgrammeThroughput, populationProgrammes, settings }) => ({
|
||||
throughput: studyProgrammeThroughput,
|
||||
studyprogrammes: populationProgrammes.data.programmes || {},
|
||||
language: settings.language,
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
dispatchGetProductivity: getProductivity,
|
||||
dispatchGetThroughput: getThroughput,
|
||||
})(Overview)
|
||||
+1
-1
@@ -18,7 +18,7 @@ const DataTable = ({ data, titles, track }) => {
|
||||
|
||||
return (
|
||||
<div className="table-container">
|
||||
<Table compact celled>
|
||||
<Table data-cy="Table-StudytrackProgress" compact celled>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{titles?.map(title => (
|
||||
+1
-1
@@ -152,7 +152,7 @@ const StudytrackDataTable = ({
|
||||
return (
|
||||
<div className="datatable">
|
||||
{getRadioButton(null, 'Show percentages', 'Hide percentages', hidePercentages, setHidePercentages)}
|
||||
<Table celled>
|
||||
<Table data-cy="Table-StudytrackOverview" celled>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{titles.map((title, index) => (
|
||||
+2
-3
@@ -23,11 +23,9 @@ const getRadioButton = (toolTip, firstLabel, secondLabel, value, setValue) => (
|
||||
</div>
|
||||
)
|
||||
|
||||
const StudytrackOverview = ({ studyprogramme }) => {
|
||||
const StudytrackOverview = ({ studyprogramme, specialGroups, setSpecialGroups, graduated, setGraduated }) => {
|
||||
const toolTips = InfotoolTips.Studyprogramme
|
||||
const [showMeanTime, setShowMeanTime] = useState(true)
|
||||
const [specialGroups, setSpecialGroups] = useState(false)
|
||||
const [graduated, setGraduated] = useState(false)
|
||||
const [track, setTrack] = useState(studyprogramme)
|
||||
const special = specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
|
||||
const grad = graduated ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
|
||||
@@ -99,6 +97,7 @@ const StudytrackOverview = ({ studyprogramme }) => {
|
||||
<div className="section-container">
|
||||
<BarChart data={stats?.data} track={track || studyprogramme} />
|
||||
<BasicDataTable
|
||||
cypress="StudytrackProgress"
|
||||
data={stats?.data?.creditTableStats}
|
||||
track={track || studyprogramme}
|
||||
titles={stats?.data?.creditTableTitles}
|
||||
@@ -1,56 +0,0 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Segment } from 'semantic-ui-react'
|
||||
import { getCourseSearchResults } from '../../../selectors/courses'
|
||||
import useLanguage from '../../LanguagePicker/useLanguage'
|
||||
import SortableTable from '../../SortableTable'
|
||||
|
||||
const SearchResult = ({ courses, getCourseActions }) => {
|
||||
const { language } = useLanguage()
|
||||
|
||||
if (!courses || courses.length === 0) return <Segment content="No results" />
|
||||
|
||||
return (
|
||||
<SortableTable
|
||||
getRowKey={c => c.code}
|
||||
tableProps={{ celled: true }}
|
||||
columns={[
|
||||
{
|
||||
key: 'code',
|
||||
title: 'Code',
|
||||
getRowVal: c => c.code,
|
||||
headerProps: { width: 2, textAlign: 'center' },
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
title: 'Name',
|
||||
getRowVal: c => c.name[language],
|
||||
headerProps: { width: 12, textAlign: 'left' },
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: 'Action',
|
||||
getRowContent: getCourseActions,
|
||||
headerProps: { width: 2, textAlign: 'center' },
|
||||
},
|
||||
]}
|
||||
data={courses}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
SearchResult.defaultProps = {
|
||||
courses: [],
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { thesisCourses } = state
|
||||
const { data } = thesisCourses || null
|
||||
const thesisCourseCodes = data.map(c => c.courseCode)
|
||||
const searchResults = getCourseSearchResults(state).courses
|
||||
const filteredResults = searchResults.filter(c => !thesisCourseCodes.includes(c.code))
|
||||
return {
|
||||
courses: filteredResults,
|
||||
}
|
||||
}
|
||||
export default connect(mapStateToProps)(SearchResult)
|
||||
@@ -1,67 +0,0 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Dimmer, Loader, Segment, Button } from 'semantic-ui-react'
|
||||
import { getThesisCourses, deleteThesisCourse } from '../../../redux/thesisCourses'
|
||||
import SortableTable from '../../SortableTable'
|
||||
|
||||
const ThesisCourseList = ({ studyprogramme, getThesisCourses, pending, data, deleteThesisCourse }) => {
|
||||
useEffect(() => {
|
||||
getThesisCourses(studyprogramme)
|
||||
}, [studyprogramme])
|
||||
|
||||
const handleDelete = course => () => {
|
||||
deleteThesisCourse(studyprogramme, course)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dimmer inverted active={pending}>
|
||||
<Loader />
|
||||
</Dimmer>
|
||||
{data.length === 0 ? (
|
||||
<Segment content="No thesis courses" />
|
||||
) : (
|
||||
<SortableTable
|
||||
tableProps={{ celled: true }}
|
||||
data={data}
|
||||
getRowKey={c => c.courseCode}
|
||||
columns={[
|
||||
{
|
||||
key: 'code',
|
||||
title: 'Code',
|
||||
headerProps: { width: 2 },
|
||||
getRowVal: c => c.courseCode,
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
title: 'Type',
|
||||
headerProps: { width: 13 },
|
||||
getRowVal: c => c.thesisType,
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: 'Remove',
|
||||
headerProps: { width: 1, textAlign: 'center', onClick: null, sorted: null },
|
||||
cellProps: { textAlign: 'center' },
|
||||
getRowContent: c => <Button basic icon="trash" onClick={handleDelete(c.courseCode)} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ThesisCourseList.defaultProps = {
|
||||
data: [],
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ thesisCourses }) => {
|
||||
const { pending, data } = thesisCourses
|
||||
return {
|
||||
pending,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { getThesisCourses, deleteThesisCourse })(ThesisCourseList)
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Button, Message } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import CourseSearchForm from '../../CourseSearchForm'
|
||||
import ThesisCourseList from './ThesisCourseList'
|
||||
import SearchResult from './SearchResult'
|
||||
import Expandable from '../../Expandable'
|
||||
import { createNewThesisCourse, getThesisCourses } from '../../../redux/thesisCourses'
|
||||
|
||||
const ThesisCourses = ({ studyprogramme, createThesis, getTheses }) => {
|
||||
const handleClick = (course, thesis) => () => {
|
||||
createThesis(studyprogramme, course, thesis).then(() => {
|
||||
getTheses(studyprogramme)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Message content="The set of courses used for calculating thesis productivity statistics" />
|
||||
<Expandable fluid title="Add thesis course">
|
||||
<CourseSearchForm />
|
||||
<SearchResult
|
||||
getCourseActions={({ code }) => (
|
||||
<Button.Group basic fluid width="3">
|
||||
<Button content="Bachelors" onClick={handleClick(code, 'BACHELOR')} />
|
||||
<Button.Or />
|
||||
<Button content="Masters" onClick={handleClick(code, 'MASTER')} />
|
||||
</Button.Group>
|
||||
)}
|
||||
/>
|
||||
</Expandable>
|
||||
<ThesisCourseList studyprogramme={studyprogramme} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(null, {
|
||||
createThesis: createNewThesisCourse,
|
||||
getTheses: getThesisCourses,
|
||||
})(ThesisCourses)
|
||||
@@ -1,390 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import moment from 'moment'
|
||||
import { Header, Table, Icon, Label, Segment, Dropdown, Button, Modal, Popup } from 'semantic-ui-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { flatten, uniq, range } from 'lodash'
|
||||
import { useGetAuthorizedUserQuery } from 'redux/auth'
|
||||
import { isDev } from '../../../conf'
|
||||
import InfoBox from '../../Info/InfoBox'
|
||||
import infotooltips from '../../../common/InfoToolTips'
|
||||
|
||||
const getMonths = year => {
|
||||
const end = moment()
|
||||
const lastDayOfMonth = moment(end).endOf('month')
|
||||
const start = `${year}-08-01`
|
||||
return Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(start))).asMonths())
|
||||
}
|
||||
|
||||
const PopulationStatisticsLink = ({ studytrack, studyprogramme, year: yearLabel, children }) => {
|
||||
const year = Number(yearLabel.slice(0, 4))
|
||||
const months = Math.ceil(moment.duration(moment().diff(`${year}-08-01`)).asMonths())
|
||||
const href = studytrack
|
||||
? `/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"studyTrack"%3A"${studytrack}"%7D&year=${year}`
|
||||
: `/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=${year}`
|
||||
return (
|
||||
<Link title={`Population statistics of class ${yearLabel}`} to={href}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const TotalPopulationLink = ({ confirm, years, studyprogramme, studytrack, children }) => {
|
||||
const confirmWrapper = e => {
|
||||
if (confirm) {
|
||||
// eslint-disable-next-line no-alert
|
||||
const c = window.confirm(
|
||||
`Are you sure you want to see a combined population of ${years.length} different populations?`
|
||||
)
|
||||
if (!c) e.preventDefault()
|
||||
}
|
||||
}
|
||||
const yearsString = years.map(year => year.value).join('&years=')
|
||||
const months = getMonths(Math.min(...years.map(year => Number(year.value))))
|
||||
const href = studytrack
|
||||
? `/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"studyTrack"%3A"${studytrack}"%7D&year=${years[0].value}&years=${yearsString}`
|
||||
: `/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=${years[0].value}&years=${yearsString}`
|
||||
return (
|
||||
<Link onClick={confirmWrapper} title="Population statistics of all years" to={href}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const ThroughputTable = ({
|
||||
throughput,
|
||||
thesis,
|
||||
loading,
|
||||
error,
|
||||
studyprogramme,
|
||||
studytrack,
|
||||
history,
|
||||
newProgramme,
|
||||
isStudytrackView,
|
||||
}) => {
|
||||
const [lowerYear, setLower] = useState(null)
|
||||
const [upperYear, setUpper] = useState(null)
|
||||
const [yearDifference, setDifference] = useState(null)
|
||||
const { isAdmin } = useGetAuthorizedUserQuery()
|
||||
|
||||
const data = throughput && throughput.data ? throughput.data.filter(year => year.credits.length > 0) : []
|
||||
|
||||
const years =
|
||||
data && !isStudytrackView
|
||||
? data
|
||||
.map(stats => ({
|
||||
key: stats.year.substring(0, 4),
|
||||
text: stats.year,
|
||||
value: stats.year.substring(0, 4),
|
||||
}))
|
||||
.sort((year1, year2) => Number(year2.value) - Number(year1.value))
|
||||
: []
|
||||
|
||||
if (error) return <h1>Oh no so error {error}</h1>
|
||||
|
||||
// toggle only on dev env and with admin user
|
||||
const GRADUATED_FEATURE_TOGGLED_ON = isAdmin && isDev
|
||||
|
||||
const genders = data.length > 0 ? uniq(flatten(data.map(year => Object.keys(year.genders)))) : []
|
||||
genders.sort()
|
||||
|
||||
const renderGenders = genders.length > 0
|
||||
|
||||
const calculateTotalNationalities = () =>
|
||||
data.length > 0 ? Object.values(throughput.totals.nationalities).reduce((res, curr) => res + curr, 0) : 0
|
||||
|
||||
const renderRatioOfFinns = calculateTotalNationalities() > 0
|
||||
let thesisTypes = []
|
||||
if (thesis) {
|
||||
thesisTypes = thesis.map(t => t.thesisType)
|
||||
}
|
||||
|
||||
const handleLowerBoundChange = (event, { value }) => {
|
||||
event.preventDefault()
|
||||
if (Number(value) > Number(upperYear)) {
|
||||
setUpper(value)
|
||||
setLower(value)
|
||||
setDifference(1)
|
||||
} else if (Number(upperYear) - Number(value) > 5) {
|
||||
setUpper(String(Number(value) + 5))
|
||||
setLower(value)
|
||||
setDifference(6)
|
||||
} else {
|
||||
setLower(value)
|
||||
setDifference(Number(upperYear) - Number(value) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpperBoundChange = (event, { value }) => {
|
||||
event.preventDefault()
|
||||
if (Number(value) < Number(lowerYear)) {
|
||||
setUpper(value)
|
||||
setLower(value)
|
||||
setDifference(1)
|
||||
} else if (Number(value) - Number(lowerYear) > 5) {
|
||||
setLower(String(Number(value) - 5))
|
||||
setUpper(value)
|
||||
setDifference(6)
|
||||
} else {
|
||||
setUpper(value)
|
||||
setDifference(Number(value) - Number(lowerYear) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const pushQueryToUrl = event => {
|
||||
event.preventDefault()
|
||||
const yearRange = range(Number(lowerYear), Number(upperYear) + 1)
|
||||
const months = getMonths(Number(lowerYear))
|
||||
const yearsString = yearRange.join('&years=')
|
||||
|
||||
if (yearRange.length > 1) {
|
||||
history.push(
|
||||
`/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=${lowerYear}&years=${yearsString}`
|
||||
)
|
||||
} else {
|
||||
history.push(
|
||||
`/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=${lowerYear}&years[]=${yearsString}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const headerCellPopUp = (title, cell) => {
|
||||
return (
|
||||
<Popup trigger={cell} wide="very" position="left center">
|
||||
<Popup.Content>{infotooltips.PopulationOverview[title]}</Popup.Content>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
const renderStudentsHeader = () => {
|
||||
let colSpan = 4
|
||||
let rowSpan = 1
|
||||
|
||||
if (renderGenders) colSpan += genders.length
|
||||
if (renderRatioOfFinns) colSpan += 1
|
||||
if (!renderGenders && !renderRatioOfFinns) rowSpan += 1
|
||||
|
||||
return headerCellPopUp(
|
||||
'Students',
|
||||
<Table.HeaderCell colSpan={colSpan} rowSpan={rowSpan}>
|
||||
Students
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
}
|
||||
|
||||
const ratioOfFinnsIn = year => {
|
||||
const total = Object.values(year.nationalities).reduce((res, curr) => res + curr, 0)
|
||||
return (
|
||||
<Table.Cell>
|
||||
{`${year.nationalities.Finland || 0} (${Math.floor((year.nationalities.Finland / total) * 100) || 0}%)`}
|
||||
</Table.Cell>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div style={{ marginTop: '2em' }}>
|
||||
<InfoBox content={infotooltips.PopulationOverview.PopulationProgress} />
|
||||
</div>
|
||||
<Header>
|
||||
{studytrack ? `Population progress for selected studytrack` : 'Population progress'}
|
||||
{throughput && (
|
||||
<Header.Subheader data-cy="throughputUpdateStatus">
|
||||
{`Last updated ${
|
||||
throughput.lastUpdated ? moment(throughput.lastUpdated).format('HH:mm:ss MM-DD-YYYY') : 'unknown'
|
||||
}`}
|
||||
{throughput.status === 'RECALCULATING' && (
|
||||
<Label content="Recalculating! Refresh page in a few minutes" color="red" />
|
||||
)}
|
||||
</Header.Subheader>
|
||||
)}
|
||||
</Header>
|
||||
<Segment basic loading={loading} style={{ overflowX: 'auto' }}>
|
||||
<Table celled structured compact striped selectable className="fixed-header">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell rowSpan="2">{isStudytrackView ? 'Studytrack' : 'Year'}</Table.HeaderCell>
|
||||
{renderStudentsHeader()}
|
||||
|
||||
{headerCellPopUp(
|
||||
'Graduated',
|
||||
<Table.HeaderCell colSpan={GRADUATED_FEATURE_TOGGLED_ON ? '3' : '1'}>Graduated</Table.HeaderCell>
|
||||
)}
|
||||
{headerCellPopUp('Credits', <Table.HeaderCell colSpan="5">Credits</Table.HeaderCell>)}
|
||||
{(thesisTypes.includes('BACHELOR') || thesisTypes.includes('MASTER')) &&
|
||||
headerCellPopUp('Thesis', <Table.HeaderCell colSpan={thesisTypes.length}>Thesis</Table.HeaderCell>)}
|
||||
</Table.Row>
|
||||
|
||||
<Table.Row>
|
||||
{renderGenders || renderRatioOfFinns ? <Table.HeaderCell content="All" /> : null}
|
||||
{genders.map(gender => (
|
||||
<Table.HeaderCell key={gender} content={gender} />
|
||||
))}
|
||||
{renderRatioOfFinns ? <Table.HeaderCell content="Finnish" /> : null}
|
||||
{headerCellPopUp(
|
||||
'Started',
|
||||
<Table.HeaderCell rowSpan="2" colSpan="1">
|
||||
Started
|
||||
</Table.HeaderCell>
|
||||
)}
|
||||
{headerCellPopUp(
|
||||
'Transferred',
|
||||
<Table.HeaderCell rowSpan="2" colSpan="1">
|
||||
Transferred to
|
||||
</Table.HeaderCell>
|
||||
)}
|
||||
{headerCellPopUp(
|
||||
'Cancelled',
|
||||
<Table.HeaderCell rowSpan="2" colSpan="1">
|
||||
Cancelled
|
||||
</Table.HeaderCell>
|
||||
)}
|
||||
<Table.HeaderCell>Graduated overall</Table.HeaderCell>
|
||||
{GRADUATED_FEATURE_TOGGLED_ON && (
|
||||
<>
|
||||
<Table.HeaderCell>Graduated in time</Table.HeaderCell>
|
||||
<Table.HeaderCell>Graduation median time</Table.HeaderCell>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Table.HeaderCell content="≥ 30" />
|
||||
<Table.HeaderCell content="≥ 60" />
|
||||
<Table.HeaderCell content="≥ 90" />
|
||||
<Table.HeaderCell content="≥ 120" />
|
||||
<Table.HeaderCell content="≥ 150" />
|
||||
{thesisTypes.includes('MASTER') && <Table.HeaderCell content="Master" />}
|
||||
{thesisTypes.includes('BACHELOR') && <Table.HeaderCell content="Bachelor" />}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{data
|
||||
.sort((year1, year2) => Number(year2.year.slice(0, 4)) - Number(year1.year.slice(0, 4)))
|
||||
.map(year => (
|
||||
<Table.Row key={year.year}>
|
||||
<Table.Cell>
|
||||
{year.year}
|
||||
{!isStudytrackView && (
|
||||
<PopulationStatisticsLink
|
||||
studytrack={studytrack}
|
||||
studyprogramme={studyprogramme}
|
||||
year={year.year}
|
||||
>
|
||||
<Icon name="level up alternate" />
|
||||
</PopulationStatisticsLink>
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{year.credits.length}</Table.Cell>
|
||||
{genders.map(gender => (
|
||||
<Table.Cell key={`${year.year} gender:${gender}`}>
|
||||
{`${year.genders[gender] || 0} (${
|
||||
Math.floor((year.genders[gender] / year.credits.length) * 100) || 0
|
||||
}%)`}
|
||||
</Table.Cell>
|
||||
))}
|
||||
{renderRatioOfFinns && ratioOfFinnsIn(year)}
|
||||
<Table.Cell>{year.started}</Table.Cell>
|
||||
<Table.Cell>{year.transferred}</Table.Cell>
|
||||
<Table.Cell>{year.cancelled}</Table.Cell>
|
||||
<Table.Cell>{year.graduated}</Table.Cell>
|
||||
|
||||
{GRADUATED_FEATURE_TOGGLED_ON && (
|
||||
<>
|
||||
<Table.Cell>{year.inTargetTime}</Table.Cell>
|
||||
<Table.Cell>{year.medianGraduationTime ? `${year.medianGraduationTime} months` : '∞'}</Table.Cell>
|
||||
</>
|
||||
)}
|
||||
|
||||
{Object.keys(year.creditValues).map(creditKey => (
|
||||
<Table.Cell key={`${year.year} credit:${creditKey}`}>{year.creditValues[creditKey]}</Table.Cell>
|
||||
))}
|
||||
{thesisTypes.includes('MASTER') ? <Table.Cell>{year.thesisM}</Table.Cell> : null}
|
||||
{thesisTypes.includes('BACHELOR') ? <Table.Cell>{year.thesisB}</Table.Cell> : null}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
{throughput && throughput.totals ? (
|
||||
<Table.Footer>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell style={{ fontWeight: 'bold' }}>
|
||||
All{' '}
|
||||
{newProgramme && !isStudytrackView && years.length > 0 && years.length < 5 ? (
|
||||
<TotalPopulationLink confirm studyprogramme={studyprogramme} studytrack={studytrack} years={years}>
|
||||
<Icon name="level up alternate" />
|
||||
</TotalPopulationLink>
|
||||
) : null}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell>{throughput.totals.students}</Table.HeaderCell>
|
||||
{Object.keys(throughput.totals.genders)
|
||||
.sort()
|
||||
.map(genderKey => (
|
||||
<Table.HeaderCell key={`${genderKey}total`}>
|
||||
{`${throughput.totals.genders[genderKey]} (${Math.floor(
|
||||
(throughput.totals.genders[genderKey] / throughput.totals.students) * 100
|
||||
)}%)`}
|
||||
</Table.HeaderCell>
|
||||
))}
|
||||
{renderRatioOfFinns ? (
|
||||
<Table.HeaderCell>
|
||||
{`${throughput.totals.nationalities.Finland || 0} (${
|
||||
Math.floor((throughput.totals.nationalities.Finland / calculateTotalNationalities()) * 100) || 0
|
||||
}%)`}
|
||||
</Table.HeaderCell>
|
||||
) : null}
|
||||
<Table.HeaderCell>{throughput.totals.started}</Table.HeaderCell>
|
||||
<Table.HeaderCell>{throughput.totals.transferred}</Table.HeaderCell>
|
||||
<Table.HeaderCell>{throughput.totals.cancelled}</Table.HeaderCell>
|
||||
<Table.HeaderCell>{throughput.totals.graduated}</Table.HeaderCell>
|
||||
{GRADUATED_FEATURE_TOGGLED_ON && (
|
||||
<>
|
||||
<Table.HeaderCell>{throughput.totals.inTargetTime}</Table.HeaderCell>
|
||||
<Table.HeaderCell>
|
||||
{throughput.totals.medianGraduationTime
|
||||
? `${throughput.totals.medianGraduationTime} months`
|
||||
: '∞'}
|
||||
</Table.HeaderCell>
|
||||
</>
|
||||
)}
|
||||
{Object.keys(throughput.totals.credits).map(creditKey => (
|
||||
<Table.HeaderCell key={`${creditKey}total`}>{throughput.totals.credits[creditKey]}</Table.HeaderCell>
|
||||
))}
|
||||
{thesisTypes.includes('MASTER') ? (
|
||||
<Table.HeaderCell>{throughput.totals.thesisM}</Table.HeaderCell>
|
||||
) : null}
|
||||
{thesisTypes.includes('BACHELOR') ? (
|
||||
<Table.HeaderCell>{throughput.totals.thesisB}</Table.HeaderCell>
|
||||
) : null}
|
||||
</Table.Row>
|
||||
</Table.Footer>
|
||||
) : null}
|
||||
</Table>
|
||||
</Segment>
|
||||
{years.length > 4 ? (
|
||||
<>
|
||||
Statistics from:
|
||||
<Dropdown selection options={years} onChange={handleLowerBoundChange} value={lowerYear} />
|
||||
to:
|
||||
<Dropdown selection options={years} onChange={handleUpperBoundChange} value={upperYear} />
|
||||
<Modal
|
||||
trigger={<Button disabled={!lowerYear || !upperYear}>Show combined population</Button>}
|
||||
header={`Are you sure you want to see a combined population of ${yearDifference} different populations?`}
|
||||
actions={[
|
||||
'Cancel',
|
||||
{
|
||||
key: 'fetch',
|
||||
content: 'Show population',
|
||||
positive: true,
|
||||
onClick: event => pushQueryToUrl(event),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThroughputTable
|
||||
@@ -1,35 +1,38 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useHistory, withRouter } from 'react-router-dom'
|
||||
import { connect, useDispatch, useSelector } from 'react-redux'
|
||||
import { Header, Segment, Tab } from 'semantic-ui-react'
|
||||
import { isEqual } from 'lodash'
|
||||
import { useGetAuthorizedUserQuery } from 'redux/auth'
|
||||
import DegreeCoursesTable from './DegreeCourses'
|
||||
import StudyProgrammeSelector from './StudyProgrammeSelector'
|
||||
import NewOverview from './NewOverview'
|
||||
import NewStudytrackOverview from './NewStudytrackOverview'
|
||||
import NewUpdateView from './NewUpdateView'
|
||||
import BasicOverview from './BasicOverview'
|
||||
import StudytrackOverview from './StudytrackOverview'
|
||||
import UpdateView from './UpdateView'
|
||||
import '../PopulationQueryCard/populationQueryCard.css'
|
||||
import { getTextIn } from '../../common'
|
||||
import { useTabs, useTitle } from '../../common/hooks'
|
||||
import TSA from '../../common/tsa'
|
||||
import Tags from './Tags'
|
||||
|
||||
import { getThroughput } from '../../redux/throughput'
|
||||
import { getProductivity } from '../../redux/productivity'
|
||||
import { getProgrammes } from '../../redux/populationProgrammes'
|
||||
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
|
||||
const StudyProgramme = props => {
|
||||
const dispatch = useDispatch()
|
||||
const history = useHistory()
|
||||
const programmes = useSelector(state => state.populationProgrammes?.data?.programmes)
|
||||
const { language } = useLanguage()
|
||||
const { isAdmin } = useGetAuthorizedUserQuery()
|
||||
const [tab, setTab] = useTabs('p_tab', 0, props.history)
|
||||
const [tab, setTab] = useTabs('p_tab', 0, history)
|
||||
const [academicYear, setAcademicYear] = useState(false)
|
||||
const [specialGroups, setSpecialGroups] = useState(false)
|
||||
const [graduated, setGraduated] = useState(false)
|
||||
|
||||
useTitle('Study programmes')
|
||||
|
||||
useEffect(() => {
|
||||
props.getProgrammesDispatch()
|
||||
dispatch(getProgrammes())
|
||||
}, [])
|
||||
|
||||
const getPanes = () => {
|
||||
@@ -38,16 +41,34 @@ const StudyProgramme = props => {
|
||||
const panes = []
|
||||
panes.push({
|
||||
menuItem: 'Basic information (NEW)',
|
||||
render: () => <NewOverview studyprogramme={studyProgrammeId} history={props.history} />,
|
||||
render: () => (
|
||||
<BasicOverview
|
||||
studyprogramme={studyProgrammeId}
|
||||
history={history}
|
||||
specialGroups={specialGroups}
|
||||
setSpecialGroups={setSpecialGroups}
|
||||
academicYear={academicYear}
|
||||
setAcademicYear={setAcademicYear}
|
||||
/>
|
||||
),
|
||||
})
|
||||
panes.push({
|
||||
menuItem: 'Studytracks and student populations (NEW)',
|
||||
render: () => <NewStudytrackOverview studyprogramme={studyProgrammeId} history={props.history} />,
|
||||
render: () => (
|
||||
<StudytrackOverview
|
||||
studyprogramme={studyProgrammeId}
|
||||
history={history}
|
||||
specialGroups={specialGroups}
|
||||
setSpecialGroups={setSpecialGroups}
|
||||
graduated={graduated}
|
||||
setGraduated={setGraduated}
|
||||
/>
|
||||
),
|
||||
})
|
||||
if (isAdmin) {
|
||||
panes.push({
|
||||
menuItem: 'Update statistics',
|
||||
render: () => <NewUpdateView studyprogramme={studyProgrammeId} />,
|
||||
render: () => <UpdateView studyprogramme={studyProgrammeId} />,
|
||||
})
|
||||
}
|
||||
panes.push({
|
||||
@@ -63,12 +84,12 @@ const StudyProgramme = props => {
|
||||
|
||||
const handleSelect = useCallback(
|
||||
programme => {
|
||||
props.history.push(`/study-programme/${programme}`, { selected: programme })
|
||||
history.push(`/study-programme/${programme}`, { selected: programme })
|
||||
},
|
||||
[props.history]
|
||||
[history]
|
||||
)
|
||||
|
||||
const { match, programmes } = props
|
||||
const { match } = props
|
||||
const { studyProgrammeId } = match.params
|
||||
const programmeName = programmes?.[studyProgrammeId] && getTextIn(programmes?.[studyProgrammeId].name, language)
|
||||
const panes = getPanes()
|
||||
@@ -112,22 +133,4 @@ const StudyProgramme = props => {
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ populationProgrammes }) => {
|
||||
const programmes = populationProgrammes.data ? populationProgrammes.data.programmes : {}
|
||||
return {
|
||||
programmes,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
getThroughputDispatch: getThroughput,
|
||||
getProductivityDispatch: getProductivity,
|
||||
getProgrammesDispatch: getProgrammes,
|
||||
},
|
||||
null,
|
||||
{
|
||||
areStatePropsEqual: isEqual,
|
||||
}
|
||||
)(withRouter(StudyProgramme))
|
||||
export default connect()(withRouter(StudyProgramme))
|
||||
|
||||
@@ -9,9 +9,6 @@ import populationSelectedStudentCourses from './populationSelectedStudentCourses
|
||||
import populationProgrammes from './populationProgrammes'
|
||||
import populationProgrammesUnfiltered from './populationProgrammesUnfiltered'
|
||||
import populationMandatoryCourses from './populationMandatoryCourses'
|
||||
import studyProgrammeProductivity from './productivity'
|
||||
import studyProgrammeThroughput from './throughput'
|
||||
import studyProgrammeBachelors from './studyProgrammeBachelors'
|
||||
import faculties from './faculties'
|
||||
import units from './units'
|
||||
import elementdetails from './elementdetails'
|
||||
@@ -53,9 +50,6 @@ const store = configureStore({
|
||||
populationProgrammes,
|
||||
populationProgrammesUnfiltered,
|
||||
populationMandatoryCourses,
|
||||
studyProgrammeProductivity,
|
||||
studyProgrammeThroughput,
|
||||
studyProgrammeBachelors,
|
||||
faculties,
|
||||
units,
|
||||
students,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { callController } from '../apiConnection/index'
|
||||
import listreducer from './common/listreducer'
|
||||
|
||||
const prefix = 'STUDYPROGRAMME_PRODUCTIVITY_'
|
||||
|
||||
export const getProductivity = studyprogrammeId => {
|
||||
const route = `v2/studyprogrammes/${studyprogrammeId}/productivity`
|
||||
return callController(route, prefix, [], 'get')
|
||||
}
|
||||
|
||||
const reducer = listreducer(prefix, null, false)
|
||||
|
||||
export default reducer
|
||||
@@ -1,13 +0,0 @@
|
||||
import { callController } from '../apiConnection/index'
|
||||
import listreducer from './common/listreducer'
|
||||
|
||||
const prefix = 'GET_STUDYPROGRAMME_BACHELORS_'
|
||||
|
||||
export const getBachelors = studyprogrammeId => {
|
||||
const route = `v2/studyprogrammes/${studyprogrammeId}/optiondata`
|
||||
return callController(route, prefix, [], 'get')
|
||||
}
|
||||
|
||||
const reducer = listreducer(prefix, null, true)
|
||||
|
||||
export default reducer
|
||||
@@ -1,13 +0,0 @@
|
||||
import { callController } from '../apiConnection/index'
|
||||
import listreducer from './common/listreducer'
|
||||
|
||||
const prefix = 'GET_STUDYPROGRAMME_THROUGHPUT_'
|
||||
|
||||
export const getThroughput = studyprogrammeId => {
|
||||
const route = `v2/studyprogrammes/${studyprogrammeId}/throughput`
|
||||
return callController(route, prefix, [], 'get')
|
||||
}
|
||||
|
||||
const reducer = listreducer(prefix, null, false)
|
||||
|
||||
export default reducer
|
||||
Reference in New Issue
Block a user