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:
Amanda Hämynen
2026-04-09 13:41:01 +03:00
committed by GitHub
11 changed files with 619 additions and 57 deletions
@@ -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()
})
})
+24
View File
@@ -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', () => {
+11
View File
@@ -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(
+1 -1
View File
@@ -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'),
}
]
}
@@ -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>
)
+1 -16
View File
@@ -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
}
-3
View File
@@ -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: