added AdminSprintsPage to frontend, admin can now edit sprint dates

This commit is contained in:
Aaro Koivukangas
2025-11-29 03:18:00 +02:00
parent 9a26dc40b8
commit 6beae0cf90
5 changed files with 422 additions and 1 deletions
+6
View File
@@ -34,6 +34,7 @@ import TimeLogsPage from './components/TimeLogsPage/TimeLogsPage'
import InstructorTimeLogsPage from './components/TimeLogsPage/InstructorTimeLogsPage'
import SprintsDashboard from './components/SprintsPage/SprintsDashboard'
import TagsDashboard from './components/TagManagementPage/TagsDashboard'
import AdminSprintsPage from "./components/AdminSprintsPage/AdminSprintsPage"
// Actions
import appActions from './reducers/actions/appActions'
@@ -222,6 +223,11 @@ const App = (props) => {
path="/administration/tags"
render={renderWithLoadingCheck(<TagsDashboard />)}
/>
<AdminRoute
exact
path="/administration/sprints"
render={renderWithLoadingCheck(<AdminSprintsPage />)}
/>
<Route
exact
path="/customer-review/:id"
@@ -0,0 +1,155 @@
import React, { useEffect, useState } from 'react'
import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
import LoadingSpinner from '../common/LoadingSpinner'
import { SprintsSelectForm } from "./SprintsSelectForm"
import { SprintListItem } from "./SprintListItem"
import { Typography } from '@material-ui/core'
import sprintService from '../../services/sprints'
import configurationService from '../../services/configuration'
import groupManagementService from '../../services/groupManagement'
import * as notificationActions from '../../reducers/actions/notificationActions'
const AdminSprintsPage = (props) => {
const {
setError,
setSuccess
} = props
const [allConfigurations, setAllConfigurations] = useState([])
const [allGroups, setAllGroups] = useState([])
const [allSprints, setAllSprints] = useState([])
const [selectedConfiguration, setSelectedConfiguration] = useState(null)
const [selectedGroup, setSelectedGroup] = useState(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
// TODO set newest configuration by default
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
)
notificationActions.setError(error.response.data.error)
}
}
const fetchAllGroups = async () => {
try {
const allGroups = await groupManagementService.get()
setAllGroups(allGroups)
console.log(allGroups)
} catch (error) {
console.error(
'Error fetching groups:',
error.message,
' / ',
error.response.data.error
)
notificationActions.setError(error.response.data.error)
}
}
const fetchData = async () => {
setIsLoading(true)
await fetchAllConfigurations()
await fetchAllGroups()
setIsLoading(false)
}
fetchData()
}, [])
useEffect(() => {
const fetchSprints = async (group_id) => {
try {
const sprintData = await sprintService.getSprintsByGroup(group_id);
setAllSprints(sprintData);
} catch (error) {
console.error(
'Error fetching all sprints:',
error.message,
' / ',
error.message.data.error
)
notificationActions.setError(error.response.data.error)
}
}
const fetchData = async (selectedGroupId) => {
setIsLoading(true)
console.log("Group:", selectedGroup)
console.log("Configuration:", selectedConfiguration)
await fetchSprints(selectedGroupId)
console.log("Sprints:", allSprints)
setIsLoading(false)
}
selectedGroup?.id && fetchData(selectedGroup.id)
}, [selectedGroup, selectedConfiguration])
if (isLoading) return <LoadingSpinner />
return (
<div>
<SprintsSelectForm
configurations={allConfigurations}
selectedConfiguration={selectedConfiguration}
handleConfigurationChange={setSelectedConfiguration}
groups={allGroups}
selectedGroup={selectedGroup}
handleGroupChange={setSelectedGroup}
/>
{selectedGroup && (
<div>
<Typography variant='h5'>Sprints by {selectedGroup.name}</Typography>
{allSprints.length > 0 && (
<div className="sprint-list-container">
<table>
<thead>
<tr>
<th>Sprint Number</th>
<th>Start Date</th>
<th>End Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="sprint-list-rows">
{allSprints.sort((a, b) => b.sprint - a.sprint).map((sprint) => {
return <SprintListItem sprint={sprint} setError={setError} setSuccess={setSuccess}/>
})}
</tbody>
</table>
</div>
)}
</div>
)}
</div>
)
}
const mapStateToProps = (state) => ({
state: state,
user: state.login.user,
})
const mapDispatchToProps = {
setError: notificationActions.setError,
setSuccess: notificationActions.setSuccess,
}
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(AdminSprintsPage)
)
@@ -0,0 +1,156 @@
import React, { useState, useEffect, useImperativeHandle } from 'react'
import Button from '@material-ui/core/Button'
import TextField from '@material-ui/core/TextField'
import sprintService from '../../services/sprints'
export const SprintListItem = (props) => {
const { sprint, setError, setSuccess } = props
const [isEditing, setIsEditing] = useState(false)
const [startDate, setStartDate] = useState("")
const [endDate, setEndDate] = useState("")
const [newStartDate, setNewStartDate] = useState("")
const [newEndDate, setNewEndDate] = useState("")
const [startDateErrorMessage, setStartDateErrorMessage] = useState("")
const [endDateErrorMessage, setEndDateErrorMessage] = useState("")
const formatDate = (date) => {
const dateObj = new Date(date)
const formattedDate = dateObj.toLocaleDateString('fi-FI', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
}).replace(/\./g, '/')
return formattedDate
}
useEffect(() => {
if (sprint) {
setStartDate(sprint.start_date)
setEndDate(sprint.end_date)
setNewStartDate(sprint.start_date)
setNewEndDate(sprint.end_date)
console.log("Formatted vs non-formatted", formatDate(sprint.start_date), sprint.start_date)
}
}, [sprint])
const handleEditButtonClick = () => {
// Editing -> Cancel -> Reset back to normal
if (isEditing) {
setNewStartDate(startDate)
setNewEndDate(endDate)
setStartDateErrorMessage("")
setEndDateErrorMessage("")
}
setIsEditing(!isEditing)
}
const validDates = () => {
// NOTE: This checks only for correctness of dates with respect to only itself.
// Dates can still be invalid compared to other sprint's dates
// This happens through the backend
let valid = true
if (new Date(newStartDate) > new Date(newEndDate)) {
setStartDateErrorMessage("Start date is higher than end date.")
setEndDateErrorMessage("End date is lower than start date.")
setError("Start date is higher than end date.")
valid = false
}
return valid
}
const handleUpdate = async () => {
if (validDates()) {
setStartDateErrorMessage("")
setEndDateErrorMessage("")
try {
const res = await sprintService.updateSprint({
id: sprint.id,
start_date: newStartDate,
end_date: newEndDate,
sprint: sprint.sprint
})
setSuccess(res.message)
setStartDate(newStartDate)
setEndDate(newEndDate)
setIsEditing(false)
} catch (error) {
console.log(error.response?.data?.error)
setError(error.response?.data?.error, 15*1000)
}
}
}
return (
<tr key={sprint.id}>
<td className="sprint-list-sprint-number">{sprint.sprint}</td>
<td>{!isEditing ? formatDate(startDate) :
<TextField
errors={!!startDateErrorMessage}
helperText={startDateErrorMessage}
className="date"
id="date"
type="date"
label="Date"
aria-describedby="date"
value={newStartDate}
onChange={(e) => setNewStartDate(e.target.value)}
InputLabelProps={{
shrink: true,
}}
variant="outlined"
/>
}</td>
<td>{!isEditing ? formatDate(endDate) :
<TextField
errors={!!endDateErrorMessage}
helperText={endDateErrorMessage}
className="date"
id="date"
type="date"
label="Date"
aria-describedby="date"
value={newEndDate}
onChange={(e) => setNewEndDate(e.target.value)}
InputLabelProps={{
shrink: true,
}}
variant="outlined"
/>
}</td>
<td>
{isEditing &&
<Button
id={`sprint-edit-save-button-${sprint.id}`}
onClick={handleUpdate}
className="submit-sprint-button"
variant="contained"
color="secondary"
>
Save
</Button>
}
<Button
id={`sprint-edit-button-${sprint.id}`}
onClick={handleEditButtonClick}
className="edit-sprint-button"
variant="contained"
color="secondary"
>
{isEditing ? "Cancel" : "Edit" }
</Button>
</td>
</tr>
)
}
@@ -0,0 +1,97 @@
import React from 'react'
import MenuItem from '@material-ui/core/MenuItem'
import Typography from '@material-ui/core/Typography'
import Select from '@material-ui/core/Select'
export const SprintsSelectForm = ({
configurations, selectedConfiguration, handleConfigurationChange,
groups, selectedGroup, handleGroupChange }) => {
const SelectorWrapper = ({ label, children }) => (
<div style={{ padding: 20 }}>
<Typography variant="caption">{label}</Typography>
{children}
</div>
)
const ConfigurationSelect = ({
configurations,
selectedConfiguration,
handleConfigurationChange,
handleGroupChange,
}) => {
return (
<Select
data-cy="configuration-selector"
value={selectedConfiguration}
onChange={(e) => {
handleConfigurationChange(e.target.value)
handleGroupChange(null)
}}
MenuProps={{ style: { zIndex: 1600 } }}
>
{configurations.map((configuration) => (
<MenuItem
key={configuration.id}
className="configuration-menu-item"
value={configuration}
>
{configuration.name}
</MenuItem>
))}
</Select>
)}
const GroupIsInConfiguration = ( group, configuration ) => {
return group.configurationId === configuration.id
}
const GroupSelect = ({
groups,
selectedGroup,
handleGroupChange
}) => {
return (
<Select
data-cy="group-selector"
value={selectedGroup}
onChange={(e) => { handleGroupChange(e.target.value) }}
MenuProps={{ style: { zIndex: 1600 } }}
>
{groups.filter(group => GroupIsInConfiguration(group, selectedConfiguration))
.map((group) => (
<MenuItem
key={group.id}
className="group-menu-item"
value={group}
>
{group.name}
</MenuItem>
))}
</Select>
)}
return (
<div className="timelog-select-container">
<div className="selector-container">
<SelectorWrapper label="Select configuration">
<ConfigurationSelect
configurations={configurations}
selectedConfiguration={selectedConfiguration}
handleConfigurationChange={handleConfigurationChange}
handleGroupChange={handleGroupChange}
/>
</SelectorWrapper>
{selectedConfiguration &&
<SelectorWrapper label="Select group">
<GroupSelect
selectedGroup={selectedGroup}
handleGroupChange={handleGroupChange}
groups={groups}
/>
</SelectorWrapper>
}
</div>
</div>
)
}
+8 -1
View File
@@ -25,6 +25,13 @@ const createSprint = async (sprintData) => {
return response.data
}
const updateSprint = async (sprintData) => {
const response = await axios.put(`${url}/${sprintData.id}`, sprintData, {
headers: { Authorization: `Bearer ${getUserToken()}` },
})
return response.data
}
const deleteSprint = async (id) => {
const response = await axios.delete(`${url}/${id}`, {
headers: { Authorization: `Bearer ${getUserToken()}` },
@@ -32,4 +39,4 @@ const deleteSprint = async (id) => {
return response.data
}
export default { getSprints, getSprintsByGroup, createSprint, deleteSprint }
export default { getSprints, getSprintsByGroup, createSprint, updateSprint, deleteSprint }