mirror of
https://github.com/UniversityOfHelsinkiCS/ohtuilmo.git
synced 2026-10-12 19:03:41 +03:00
added AdminSprintsPage to frontend, admin can now edit sprint dates
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user