mirror of
https://github.com/UniversityOfHelsinkiCS/ohtuilmo.git
synced 2026-10-12 19:03:41 +03:00
Merge pull request #351 from Ohtuilmo/350-timelog-tagien-visualisointi-ohjaajille-ja-adminille
feature: timelog tagien visualisointi ohjaajille ja adminille
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
const addWeeksToDate = (date, weeks) => {
|
||||
const newDate = new Date(date)
|
||||
newDate.setDate(newDate.getDate() + weeks * 7)
|
||||
return newDate
|
||||
}
|
||||
|
||||
const addDaysToDate = (date, days) => {
|
||||
const newDate = new Date(date)
|
||||
newDate.setDate(newDate.getDate() + days)
|
||||
return newDate
|
||||
}
|
||||
|
||||
const formatDate = (date) => date.toISOString().slice(0, 10)
|
||||
|
||||
describe('Staff tag page', () => {
|
||||
before(() => {
|
||||
cy.deleteAllGroups()
|
||||
cy.loginAsAdmin()
|
||||
cy.visit('/')
|
||||
cy.createTag('Coding')
|
||||
cy.createTag('Meeting')
|
||||
cy.createGroup({
|
||||
name: 'Brand New Group',
|
||||
topicId: 1,
|
||||
configurationId: 1,
|
||||
instructorId: null,
|
||||
studentIds: ['012345698', '918273645'],
|
||||
}).then((createdGroup) => {
|
||||
cy.loginAsRegisteredUser()
|
||||
cy.visit('/')
|
||||
|
||||
const dateToday = new Date()
|
||||
const dateYesterday = addDaysToDate(dateToday, -1)
|
||||
|
||||
cy.createSprint({
|
||||
sprint: 0,
|
||||
start_date: formatDate(addWeeksToDate(dateYesterday, -1)),
|
||||
end_date: formatDate(dateYesterday),
|
||||
})
|
||||
cy.createSprint({
|
||||
sprint: 1,
|
||||
start_date: formatDate(dateToday),
|
||||
end_date: formatDate(addWeeksToDate(dateToday, 1)),
|
||||
})
|
||||
cy.addTimelogEntry({
|
||||
studentNumber: '012345698',
|
||||
sprint: 0,
|
||||
date: formatDate(dateYesterday),
|
||||
minutes: 120,
|
||||
description: 'Coding with team A',
|
||||
tags: ['Coding'],
|
||||
groupId: createdGroup.id,
|
||||
})
|
||||
cy.addTimelogEntry({
|
||||
studentNumber: '012345698',
|
||||
sprint: 1,
|
||||
date: formatDate(dateToday),
|
||||
minutes: 180,
|
||||
description: 'Customer meeting A',
|
||||
tags: ['Meeting'],
|
||||
groupId: createdGroup.id,
|
||||
})
|
||||
|
||||
cy.loginAsRegisteredIndicatedUser()
|
||||
cy.addTimelogEntryAlt({
|
||||
studentNumber: '918273645',
|
||||
sprint: 0,
|
||||
date: formatDate(dateYesterday),
|
||||
minutes: 240,
|
||||
description: 'Coding with team B',
|
||||
tags: ['Coding'],
|
||||
groupId: createdGroup.id,
|
||||
})
|
||||
cy.addTimelogEntryAlt({
|
||||
studentNumber: '918273645',
|
||||
sprint: 0,
|
||||
date: formatDate(dateYesterday),
|
||||
minutes: 120,
|
||||
description: 'Customer meeting B1',
|
||||
tags: ['Meeting'],
|
||||
groupId: createdGroup.id,
|
||||
})
|
||||
cy.addTimelogEntryAlt({
|
||||
studentNumber: '918273645',
|
||||
sprint: 1,
|
||||
date: formatDate(dateToday),
|
||||
minutes: 60,
|
||||
description: 'Customer meeting B2',
|
||||
tags: ['Meeting'],
|
||||
groupId: createdGroup.id,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loginAsAdmin()
|
||||
cy.visit('/')
|
||||
cy.get('#hamburger-menu-button')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.contains('Tag statistics').click()
|
||||
})
|
||||
// Ignore ResizeObserver loop limit error which happens randomly
|
||||
cy.on(
|
||||
'uncaught:exception',
|
||||
(err) => !err.message.includes('ResizeObserver loop'),
|
||||
)
|
||||
})
|
||||
|
||||
it('opens staff tag page', () => {
|
||||
cy.contains('Tags').should('exist')
|
||||
})
|
||||
|
||||
it('displays all available tags to staff', () => {
|
||||
cy.contains('Coding').should('exist')
|
||||
cy.contains('Meeting').should('exist')
|
||||
})
|
||||
|
||||
it('displays correct tag data for group', () => {
|
||||
cy.get('[data-cy="configuration-selector"]')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.contains('Konfiguraatio 1').click()
|
||||
})
|
||||
cy.get('[data-cy="group-selector"]')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.contains('Brand New Group').click()
|
||||
})
|
||||
cy.contains('Tag usage for Brand New Group').should('exist')
|
||||
cy.get('#bar-Coding').trigger('mouseover')
|
||||
cy.get('.recharts-tooltip-wrapper')
|
||||
.should('be.visible')
|
||||
.and('contain.text', '6 h')
|
||||
|
||||
cy.get('#bar-Meeting').trigger('mouseover')
|
||||
cy.get('.recharts-tooltip-wrapper')
|
||||
.should('be.visible')
|
||||
.and('contain.text', '6 h')
|
||||
|
||||
cy.get('#tag-usage-line-chart .recharts-cartesian-grid-vertical').trigger(
|
||||
'mouseover',
|
||||
'left',
|
||||
{ force: true },
|
||||
)
|
||||
cy.get('.recharts-tooltip-wrapper')
|
||||
.should('be.visible')
|
||||
.and('contain.text', 'Coding : 6 h')
|
||||
.and('contain.text', 'Meeting : 2 h')
|
||||
|
||||
cy.get('#tag-usage-line-chart .recharts-cartesian-grid-vertical').trigger(
|
||||
'mouseover',
|
||||
'right',
|
||||
{ force: true },
|
||||
)
|
||||
cy.get('.recharts-tooltip-wrapper')
|
||||
.should('be.visible')
|
||||
.and('contain.text', 'Coding : 0 h')
|
||||
.and('contain.text', 'Meeting : 4 h')
|
||||
})
|
||||
|
||||
it('displays correct tag data for individual student', () => {
|
||||
cy.get('[data-cy="configuration-selector"]')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.contains('Konfiguraatio 1').click()
|
||||
})
|
||||
cy.get('[data-cy="group-selector"]')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.contains('Brand New Group').click()
|
||||
})
|
||||
cy.get('[data-cy="student-selector"]')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.contains('Volodymyr').click()
|
||||
})
|
||||
cy.contains('Tag usage for Volodymyr').should('exist')
|
||||
cy.get('#bar-Coding').trigger('mouseover')
|
||||
cy.get('.recharts-tooltip-wrapper')
|
||||
.should('be.visible')
|
||||
.and('contain.text', '4 h')
|
||||
|
||||
cy.get('#bar-Meeting').trigger('mouseover')
|
||||
cy.get('.recharts-tooltip-wrapper')
|
||||
.should('be.visible')
|
||||
.and('contain.text', '3 h')
|
||||
|
||||
cy.get('#tag-usage-line-chart .recharts-cartesian-grid-vertical').trigger(
|
||||
'mouseover',
|
||||
'left',
|
||||
{ force: true },
|
||||
)
|
||||
cy.get('.recharts-tooltip-wrapper')
|
||||
.should('be.visible')
|
||||
.and('contain.text', 'Coding : 4 h')
|
||||
.and('contain.text', 'Meeting : 2 h')
|
||||
|
||||
cy.get('#tag-usage-line-chart .recharts-cartesian-grid-vertical').trigger(
|
||||
'mouseover',
|
||||
'right',
|
||||
{ force: true },
|
||||
)
|
||||
cy.get('.recharts-tooltip-wrapper')
|
||||
.should('be.visible')
|
||||
.and('contain.text', 'Coding : 0 h')
|
||||
.and('contain.text', 'Meeting : 1 h')
|
||||
})
|
||||
|
||||
|
||||
after(() => {
|
||||
cy.deleteAllGroups()
|
||||
cy.deleteAllTags()
|
||||
})
|
||||
|
||||
})
|
||||
@@ -849,6 +849,30 @@ Cypress.Commands.add('addTimelogEntry', (timeLogEntryData) => {
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add('addTimelogEntryAlt', (timeLogEntryData) => {
|
||||
withLoggedRegisteredUserTokenAlt((token) => {
|
||||
const authHeaders = {
|
||||
Authorization: 'Bearer ' + token
|
||||
}
|
||||
const { studentNumber, sprint, date, minutes, description, tags, groupId } = timeLogEntryData
|
||||
|
||||
cy.request({
|
||||
url: '/api/timelogs',
|
||||
method: 'POST',
|
||||
headers: authHeaders,
|
||||
body: {
|
||||
studentNumber,
|
||||
sprint,
|
||||
date,
|
||||
minutes,
|
||||
description,
|
||||
tags,
|
||||
groupId
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* DELETE ALL TIMELOGS FOR TESTING */
|
||||
|
||||
Cypress.Commands.add('deleteAllTimelogs', () => {
|
||||
|
||||
@@ -36,6 +36,7 @@ import SprintsDashboard from './components/SprintsPage/SprintsDashboard'
|
||||
import TagsDashboard from './components/TagManagementPage/TagsDashboard'
|
||||
import AdminSprintsPage from "./components/AdminSprintsPage/AdminSprintsPage"
|
||||
import StudentTagPage from './components/TagPage/StudentTagPage'
|
||||
import StaffTagPage from './components/TagPage/StaffTagPage'
|
||||
|
||||
// Actions
|
||||
import appActions from './reducers/actions/appActions'
|
||||
@@ -296,6 +297,16 @@ const App = (props) => {
|
||||
path="/student-tags"
|
||||
render={renderWithLoadingCheck(<StudentTagPage />)}
|
||||
/>
|
||||
<InstructorRoute
|
||||
exact
|
||||
path="/instructor-tags"
|
||||
render={renderWithLoadingCheck(<StaffTagPage />)}
|
||||
/>
|
||||
<AdminRoute
|
||||
exact
|
||||
path="/administration/tags-statistics"
|
||||
render={renderWithLoadingCheck(<StaffTagPage />)}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import LoadingSpinner from '../common/LoadingSpinner'
|
||||
import { Typography } from '@material-ui/core'
|
||||
import CheckboxMultiSelect from '../common/CheckboxMultiSelect'
|
||||
import { StudentSelectionForm } from '../common/StudentSelectionForm'
|
||||
import TagUsageLineChart from './TagUsageLineChart'
|
||||
import TagUsageBarChart from './TagUsageBarChart'
|
||||
|
||||
import configurationService from '../../services/configuration'
|
||||
import groupManagementService from '../../services/groupManagement'
|
||||
import sprintService from '../../services/sprints'
|
||||
import userService from '../../services/user'
|
||||
import * as notificationActions from '../../reducers/actions/notificationActions'
|
||||
import tagsActions from '../../reducers/actions/tagActions'
|
||||
import tagService from '../../services/tags'
|
||||
import './TagPage.css'
|
||||
|
||||
const colourSet = [
|
||||
'#9258c8',
|
||||
'#3a75c4',
|
||||
'#8c0032',
|
||||
'#15bef0',
|
||||
'#00b08c',
|
||||
'#fca311',
|
||||
'#009e60',
|
||||
'#e5053a',
|
||||
'#5bbf21',
|
||||
'#00a39a',
|
||||
'#e63375',
|
||||
'#256ec7',
|
||||
'#e95c55',
|
||||
'#a3af07',
|
||||
]
|
||||
|
||||
const StaffTagPage = (props) => {
|
||||
const {
|
||||
user,
|
||||
availableTags,
|
||||
fetchAvailableTags,
|
||||
} = props
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [allSprints, setAllSprints] = useState([])
|
||||
const [selectedTags, setSelectedTags] = useState([])
|
||||
|
||||
const [allConfigurations, setAllConfigurations] = useState([])
|
||||
const [selectedConfigurationId, setSelectedConfigurationId] = useState(0)
|
||||
const [allGroups, setAllGroups] = useState([])
|
||||
const [selectedGroupId, setSelectedGroupId] = useState(0)
|
||||
const [selectedGroup, setSelectedGroup] = useState(null)
|
||||
const [allStudents, setAllStudents] = useState([])
|
||||
const [selectedStudentNumber, setSelectedStudentNumber] = useState(0)
|
||||
|
||||
const [groupTagData, setGroupTagData] = useState({})
|
||||
const [studentTagData, setStudentTagData] = useState({})
|
||||
|
||||
const tagColors = availableTags.reduce((acc, tag, index) => {
|
||||
acc[tag] = colourSet[index % colourSet.length]
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAllConfigurations = async () => {
|
||||
try {
|
||||
const allConfigurations = await configurationService.getAll()
|
||||
setAllConfigurations(allConfigurations.configurations)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error fetching groups:',
|
||||
error.message,
|
||||
' / ',
|
||||
error.response.data.error
|
||||
)
|
||||
setError(error.response.data.error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchAllGroups = async () => {
|
||||
try {
|
||||
const allGroups = await groupManagementService.get()
|
||||
setAllGroups(allGroups)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error fetching groups:',
|
||||
error.message,
|
||||
' / ',
|
||||
error.response.data.error
|
||||
)
|
||||
setError(error.response.data.error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchAllStudents = async () => {
|
||||
try {
|
||||
const allStudents = await userService.get()
|
||||
setAllStudents(allStudents)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error fetching students:',
|
||||
error.message,
|
||||
' / ',
|
||||
error.response.data.error
|
||||
)
|
||||
setError(error.response.data.error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true)
|
||||
await fetchAllConfigurations()
|
||||
await fetchAllGroups()
|
||||
await fetchAllStudents()
|
||||
await fetchAvailableTags()
|
||||
setIsLoading(false)
|
||||
}
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (allConfigurations.length > 0 && allGroups.length > 0) {
|
||||
const allGroupsInSortedOrder = [...allGroups] // clones the array
|
||||
allGroupsInSortedOrder.sort((a, b) => a.id - b.id)
|
||||
const newestGroupByInstructor = allGroupsInSortedOrder.findLast(
|
||||
group => group.instructorId === user.studentNumber
|
||||
)
|
||||
const configurationByInstructor = allConfigurations.find(
|
||||
configuration =>
|
||||
newestGroupByInstructor?.configurationId === configuration.id
|
||||
)
|
||||
setSelectedConfigurationId(configurationByInstructor?.id ?? 0)
|
||||
setSelectedGroupId(newestGroupByInstructor?.id ?? 0)
|
||||
setSelectedGroup(newestGroupByInstructor?.id ? newestGroupByInstructor : null)
|
||||
}
|
||||
}, [allConfigurations, allGroups])
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
setGroupTagData({})
|
||||
setStudentTagData({})
|
||||
setIsLoading(false)
|
||||
}, [selectedConfigurationId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedGroupId) return
|
||||
|
||||
const fetchSprints = async () => {
|
||||
try {
|
||||
const sprintData = await sprintService.getSprintsByGroup(selectedGroupId)
|
||||
setAllSprints(sprintData)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error fetching sprints:',
|
||||
error.message,
|
||||
' / ',
|
||||
error.response.data.error,
|
||||
)
|
||||
notificationActions.setError(error.response.data.error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchGroupTagData = async () => {
|
||||
const fetchedGroupTagData = {}
|
||||
|
||||
for (const studentId of selectedGroup.studentIds) {
|
||||
const singleStudentTagData = await tagService.getTagsByStudent(studentId)
|
||||
|
||||
for (const tag in singleStudentTagData) {
|
||||
if (!fetchedGroupTagData[tag]) {
|
||||
fetchedGroupTagData[tag] = []
|
||||
}
|
||||
|
||||
for (const { sprint_id, minutes } of singleStudentTagData[tag]) {
|
||||
const sprint = fetchedGroupTagData[tag].find(s => s.sprint_id === sprint_id)
|
||||
|
||||
if (sprint) {
|
||||
sprint.minutes += minutes
|
||||
} else {
|
||||
fetchedGroupTagData[tag].push({ sprint_id, minutes })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setGroupTagData(fetchedGroupTagData)
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
if (selectedGroupId) {
|
||||
await fetchSprints()
|
||||
await fetchGroupTagData()
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}, [selectedGroupId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedStudentNumber) return
|
||||
const fetchData = async () => {
|
||||
await tagService.getTagsByStudent(selectedStudentNumber).then((data) => {
|
||||
setStudentTagData(data)
|
||||
})
|
||||
}
|
||||
fetchData()
|
||||
}, [selectedStudentNumber])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTags(availableTags)
|
||||
}, [availableTags])
|
||||
|
||||
const handleConfigurationChange = (configuration_id) => {
|
||||
const configurationById = allConfigurations.find(conf => conf.id === configuration_id)
|
||||
setSelectedConfigurationId(configurationById.id ?? 0)
|
||||
}
|
||||
|
||||
const handleGroupChange = (group_id) => {
|
||||
const groupById = allGroups.find(grp => grp.id === group_id)
|
||||
setSelectedGroupId(groupById?.id ?? 0)
|
||||
setSelectedGroup(groupById?.id ? groupById : null)
|
||||
}
|
||||
|
||||
const handleStudentChange = async (studentNumber) => {
|
||||
setSelectedStudentNumber(studentNumber)
|
||||
}
|
||||
|
||||
const selectedStudent = allStudents.find(
|
||||
student => student.student_number === selectedStudentNumber
|
||||
)
|
||||
|
||||
if (isLoading) return <LoadingSpinner />
|
||||
|
||||
|
||||
return (
|
||||
<div className="tagpage-container">
|
||||
<div className="tagpage-selection-container">
|
||||
<StudentSelectionForm
|
||||
configurations={allConfigurations}
|
||||
selectedConfigurationId={selectedConfigurationId}
|
||||
handleConfigurationChange={handleConfigurationChange}
|
||||
groups={allGroups}
|
||||
selectedGroupId={selectedGroupId}
|
||||
handleGroupChange={handleGroupChange}
|
||||
students={allStudents}
|
||||
selectedStudentNumber={selectedStudentNumber}
|
||||
handleStudentNumberChange={handleStudentChange}
|
||||
/>
|
||||
<Typography variant="h4">Tags</Typography>
|
||||
<CheckboxMultiSelect
|
||||
allItems={availableTags}
|
||||
selectedItems={selectedTags}
|
||||
setSelectedItems={setSelectedTags}
|
||||
/>
|
||||
</div>
|
||||
<div className="tagpage-charts-container">
|
||||
{selectedTags.length === 0 ? (
|
||||
<Typography variant="body1">No tags selected.</Typography>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="h5">
|
||||
{!selectedGroupId
|
||||
? 'Select a group'
|
||||
: selectedStudentNumber === 0
|
||||
? `Tag usage for ${selectedGroup?.name ?? ''}`
|
||||
: `Tag usage for ${selectedStudent?.first_names ?? ''} ${selectedStudent?.last_name ?? ''}`
|
||||
}
|
||||
</Typography>
|
||||
<TagUsageBarChart
|
||||
allTags={availableTags}
|
||||
selectedTags={selectedTags}
|
||||
tagData={selectedStudentNumber === 0 ? groupTagData : studentTagData}
|
||||
tagColors={tagColors}
|
||||
/>
|
||||
|
||||
<TagUsageLineChart
|
||||
allSprints={allSprints}
|
||||
allTags={availableTags}
|
||||
selectedTags={selectedTags}
|
||||
tagData={selectedStudentNumber === 0 ? groupTagData : studentTagData}
|
||||
tagColors={tagColors}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
state: state,
|
||||
user: {
|
||||
studentNumber: state.login.user.user.student_number,
|
||||
admin: state.login.user.user.admin,
|
||||
instructor: state.login.user.user.instructor,
|
||||
name: `${state.login.user.user.first_name} ${state.login.user.user.last_name}`,
|
||||
},
|
||||
availableTags: state.tags.availableTags,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setError: notificationActions.setError,
|
||||
setSuccess: notificationActions.setSuccess,
|
||||
fetchAvailableTags: tagsActions.fetchAvailableTags,
|
||||
}
|
||||
|
||||
export default withRouter(
|
||||
connect(mapStateToProps, mapDispatchToProps)(StaffTagPage),
|
||||
)
|
||||
@@ -12,6 +12,7 @@ import TagUsageLineChart from './TagUsageLineChart'
|
||||
import TagUsageBarChart from './TagUsageBarChart'
|
||||
|
||||
import sprintService from '../../services/sprints'
|
||||
import tagService from '../../services/tags'
|
||||
import * as notificationActions from '../../reducers/actions/notificationActions'
|
||||
import tagsActions from '../../reducers/actions/tagActions'
|
||||
import './TagPage.css'
|
||||
@@ -38,14 +39,13 @@ const StudentTagPage = (props) => {
|
||||
user,
|
||||
group,
|
||||
availableTags,
|
||||
studentTags,
|
||||
fetchAvailableTags,
|
||||
fetchTagsByStudent,
|
||||
} = props
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [allSprints, setAllSprints] = useState([])
|
||||
const [selectedTags, setSelectedTags] = useState([])
|
||||
const [studentTags, setStudentTags] = useState({})
|
||||
|
||||
const tagColors = availableTags.reduce((acc, tag, index) => {
|
||||
acc[tag] = colourSet[index % colourSet.length]
|
||||
@@ -57,7 +57,6 @@ const StudentTagPage = (props) => {
|
||||
try {
|
||||
const fetchedData = await sprintService.getSprints()
|
||||
setAllSprints(fetchedData)
|
||||
console.log('all sprints:', fetchedData)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error fetching sprints:',
|
||||
@@ -72,13 +71,15 @@ const StudentTagPage = (props) => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true)
|
||||
group?.id && (await fetchSprints())
|
||||
await fetchTagsByStudent(user.studentNumber)
|
||||
await tagService.getTagsByStudent(user.studentNumber).then((data) => {
|
||||
setStudentTags(data)
|
||||
})
|
||||
await fetchAvailableTags()
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [fetchAvailableTags, fetchTagsByStudent, user.studentNumber, group?.id])
|
||||
}, [fetchAvailableTags, user.studentNumber, group?.id])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTags(availableTags)
|
||||
@@ -134,14 +135,12 @@ const mapStateToProps = (state) => ({
|
||||
},
|
||||
group: state.registrationDetails.myGroup,
|
||||
availableTags: state.tags.availableTags,
|
||||
studentTags: state.tags.studentTags,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setError: notificationActions.setError,
|
||||
setSuccess: notificationActions.setSuccess,
|
||||
fetchAvailableTags: tagsActions.fetchAvailableTags,
|
||||
fetchTagsByStudent: tagsActions.fetchTagsByStudent,
|
||||
}
|
||||
|
||||
export default withRouter(
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
.tagpage-selection-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 480px;
|
||||
max-width: 600px;
|
||||
margin-bottom: 26px;
|
||||
gap: 38px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import { TimeLogsSelectForm } from './TimeLogsSelectForm'
|
||||
import { StudentSelectionForm } from '../common/StudentSelectionForm'
|
||||
import { TimeLogRow } from './TimeLogRow'
|
||||
import TimeLogChart from './TimeLogChart'
|
||||
import LoadingSpinner from '../common/LoadingSpinner'
|
||||
@@ -278,7 +278,7 @@ const InstructorTimeLogsPage = (props) => {
|
||||
<div className='timelogs-responsive-grid'>
|
||||
<div id='timelogs-container-1'>
|
||||
<div>
|
||||
<TimeLogsSelectForm
|
||||
<StudentSelectionForm
|
||||
configurations={allConfigurations}
|
||||
selectedConfigurationId={selectedConfigurationId}
|
||||
handleConfigurationChange={handleConfigurationChange}
|
||||
|
||||
@@ -74,7 +74,11 @@ const instructorItems = (history) => {
|
||||
{
|
||||
text: 'Time Logs (Instructor)',
|
||||
handler: () => history.push('/instructor-timelogs'),
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Tags',
|
||||
handler: () => history.push('/instructor-tags'),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -149,6 +153,11 @@ const adminItems = (history) => {
|
||||
text: 'Tag management',
|
||||
handler: () => history.push('/administration/tags'),
|
||||
},
|
||||
{
|
||||
className: 'tags-statistics-menu-item',
|
||||
text: 'Tag statistics',
|
||||
handler: () => history.push('/administration/tags-statistics'),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
+40
-27
@@ -1,14 +1,19 @@
|
||||
import React from 'react'
|
||||
import './TimeLogsPage.css'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import Select from '@material-ui/core/Select'
|
||||
|
||||
export const TimeLogsSelectForm = ({
|
||||
configurations, selectedConfigurationId, handleConfigurationChange,
|
||||
groups, selectedGroupId, handleGroupChange,
|
||||
students, selectedStudentNumber, handleStudentNumberChange }) => {
|
||||
|
||||
export const StudentSelectionForm = ({
|
||||
configurations,
|
||||
selectedConfigurationId,
|
||||
handleConfigurationChange,
|
||||
groups,
|
||||
selectedGroupId,
|
||||
handleGroupChange,
|
||||
students,
|
||||
selectedStudentNumber,
|
||||
handleStudentNumberChange,
|
||||
}) => {
|
||||
const SelectorWrapper = ({ label, children }) => (
|
||||
<div style={{ padding: 20 }}>
|
||||
<Typography variant="caption">{label}</Typography>
|
||||
@@ -21,7 +26,7 @@ export const TimeLogsSelectForm = ({
|
||||
selectedConfigurationId,
|
||||
handleConfigurationChange,
|
||||
handleGroupChange,
|
||||
handleStudentNumberChange
|
||||
handleStudentNumberChange,
|
||||
}) => {
|
||||
return (
|
||||
<Select
|
||||
@@ -44,17 +49,14 @@ export const TimeLogsSelectForm = ({
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
const GroupIsInConfiguration = ( group, configurationId ) => {
|
||||
const GroupIsInConfiguration = (group, configurationId) => {
|
||||
return group.configurationId === configurationId
|
||||
}
|
||||
|
||||
const GroupSelect = ({
|
||||
groups,
|
||||
selectedGroupId,
|
||||
handleGroupChange
|
||||
}) => {
|
||||
const GroupSelect = ({ groups, selectedGroupId, handleGroupChange }) => {
|
||||
return (
|
||||
<Select
|
||||
data-cy="group-selector"
|
||||
@@ -65,8 +67,13 @@ export const TimeLogsSelectForm = ({
|
||||
}}
|
||||
MenuProps={{ style: { zIndex: 1600 } }}
|
||||
>
|
||||
<MenuItem key="0" className="group-menu-item" value="0" disabled>Pick a group</MenuItem>
|
||||
{groups.filter(group => GroupIsInConfiguration(group, selectedConfigurationId))
|
||||
<MenuItem key="0" className="group-menu-item" value="0" disabled>
|
||||
Pick a group
|
||||
</MenuItem>
|
||||
{groups
|
||||
.filter((group) =>
|
||||
GroupIsInConfiguration(group, selectedConfigurationId),
|
||||
)
|
||||
.map((group) => (
|
||||
<MenuItem
|
||||
key={group.id}
|
||||
@@ -77,17 +84,20 @@ export const TimeLogsSelectForm = ({
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
const StudentIsInGroup = ( student, group_id ) => {
|
||||
return groups.find(g => g.id === group_id)?.studentIds.includes(student.student_number)
|
||||
const StudentIsInGroup = (student, group_id) => {
|
||||
return groups
|
||||
.find((g) => g.id === group_id)
|
||||
?.studentIds.includes(student.student_number)
|
||||
}
|
||||
|
||||
const StudentSelect = ({
|
||||
selectedGroupId,
|
||||
students,
|
||||
selectedStudentNumber,
|
||||
handleStudentNumberChange
|
||||
handleStudentNumberChange,
|
||||
}) => {
|
||||
return (
|
||||
<Select
|
||||
@@ -99,8 +109,11 @@ export const TimeLogsSelectForm = ({
|
||||
disabled={!selectedGroupId}
|
||||
MenuProps={{ style: { zIndex: 1600 } }}
|
||||
>
|
||||
<MenuItem key={0} className="student-menu-item" value={0}>Pick a student</MenuItem>
|
||||
{students.filter(student => StudentIsInGroup(student, selectedGroupId))
|
||||
<MenuItem key={0} className="student-menu-item" value={0}>
|
||||
Pick a student
|
||||
</MenuItem>
|
||||
{students
|
||||
.filter((student) => StudentIsInGroup(student, selectedGroupId))
|
||||
.map((student) => (
|
||||
<MenuItem
|
||||
key={student.student_number}
|
||||
@@ -115,7 +128,7 @@ export const TimeLogsSelectForm = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="timelog-select-container">
|
||||
<div>
|
||||
<div className="selector-container">
|
||||
<SelectorWrapper label="Select configuration">
|
||||
<ConfigurationSelect
|
||||
@@ -126,7 +139,7 @@ export const TimeLogsSelectForm = ({
|
||||
handleStudentNumberChange={handleStudentNumberChange}
|
||||
/>
|
||||
</SelectorWrapper>
|
||||
{selectedConfigurationId !== 0 &&
|
||||
{selectedConfigurationId !== 0 && (
|
||||
<SelectorWrapper label="Select group">
|
||||
<GroupSelect
|
||||
selectedGroupId={selectedGroupId}
|
||||
@@ -135,8 +148,8 @@ export const TimeLogsSelectForm = ({
|
||||
handleStudentNumberChange={handleStudentNumberChange}
|
||||
/>
|
||||
</SelectorWrapper>
|
||||
}
|
||||
{selectedGroupId !== 0 &&
|
||||
)}
|
||||
{selectedGroupId !== 0 && (
|
||||
<SelectorWrapper label="Select student">
|
||||
<StudentSelect
|
||||
selectedGroupId={selectedGroupId}
|
||||
@@ -145,7 +158,7 @@ export const TimeLogsSelectForm = ({
|
||||
students={students}
|
||||
/>
|
||||
</SelectorWrapper>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -5,11 +5,6 @@ const setAvailableTags = (tags) => ({
|
||||
payload: tags
|
||||
})
|
||||
|
||||
const setStudentTags = (tags) => ({
|
||||
type: 'SET_STUDENT_TAGS',
|
||||
payload: tags
|
||||
})
|
||||
|
||||
const resetTags = () => ({
|
||||
type: 'RESET_TAGS'
|
||||
})
|
||||
@@ -21,18 +16,8 @@ const fetchAvailableTags = () => {
|
||||
dispatch(setAvailableTags(availableTagData))
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTagsByStudent = (studentNumber) => {
|
||||
return async (dispatch) => {
|
||||
const studentTags = await tagService.getTagsByStudent(studentNumber)
|
||||
dispatch(setStudentTags(studentTags))
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
setAvailableTags,
|
||||
setStudentTags,
|
||||
resetTags,
|
||||
fetchAvailableTags,
|
||||
fetchTagsByStudent
|
||||
fetchAvailableTags
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
const initialState = {
|
||||
availableTags: [],
|
||||
studentTags: {},
|
||||
}
|
||||
|
||||
const tagsReducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_AVAILABLE_TAGS':
|
||||
return { ...state, availableTags: action.payload }
|
||||
case 'SET_STUDENT_TAGS':
|
||||
return { ...state, studentTags: action.payload }
|
||||
case 'RESET_TAGS':
|
||||
return initialState
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user