diff --git a/package.json b/package.json index a666da0c590cd3b6ce21faf23860da73593aa6ab..dc6c05bd1548e6b4c47793afac288ebf7492bdfa 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "react-dom": "^16.7.0", "react-redux": "^6.0.0", "react-router-dom": "^4.3.1", - "react-scripts": "2.1.2", + "react-scripts": "^2.1.3", "react-slick": "^0.23.2", "redux": "^4.0.1", "redux-logger": "^3.0.6", @@ -20,6 +20,7 @@ "semantic-ui-calendar-react": "^0.13.0", "semantic-ui-css": "^2.4.0", "semantic-ui-react": "^0.84.0", + "semantic-ui-calendar-react": "^0.12.2", "slick-carousel": "^1.8.1" }, "scripts": { diff --git a/src/actions/auth.js b/src/actions/auth.js index 1b10cf88d5d11d398e35209050e5f63de45a3eac..ac0e9e51764333deae7eacf7741ad0510b9bc3f9 100644 --- a/src/actions/auth.js +++ b/src/actions/auth.js @@ -1,12 +1,6 @@ -// TODO: Separate actions +import axios from './session'; +import { GET_USERDATA, PROFILE_CHANGE, GROUP_CHANGE } from './types'; -import ax from 'axios'; -import { GET_USERDATA, PROFILE_CHANGE, GROUP_CHANGE, GET_NEWS } from './types'; - -export const axios = ax.create({ - xsrfCookieName: 'csrftoken', - xsrfHeaderName: 'X-CSRFToken', -}); export const getUserData = () => ( async (dispatch) => { @@ -21,11 +15,28 @@ export const getUserData = () => ( motivation_exercise: motivationExercise, signed, groups, + role, } = user.data; + let permission; + switch (role) { + case 'Applicant': + permission=1; + break; + case 'Student': + permission=2; + break; + case 'Staff': + permission=3; + break; + default: + permission=0; + break; + } + dispatch({ type: GET_USERDATA, payload: { - id, joinDate, nick, motivationAbout, motivationProfession, motivationExercise, signed, groups, + id, joinDate, nick, motivationAbout, motivationProfession, motivationExercise, signed, groups, role, permission }, }); } catch (e) { @@ -34,20 +45,6 @@ export const getUserData = () => ( } ); -export const getNews = () => ( - async (dispatch) => { - try { - const response = await axios.get('/api/v1/news'); - dispatch({ - type: GET_NEWS, - payload: response.data, - }); - } catch (e) { - console.log(e); - } - } -); - export const textChange = ({ target: { name, value } }) => ( (dispatch) => { dispatch({ type: PROFILE_CHANGE, payload: value, target: name }); diff --git a/src/actions/news.js b/src/actions/news.js index cc564fa280fa61e6ae3aa2749dd5df8401b2a25e..0a953af5d51335bad8809c32f9eb2ad1c8d709e0 100644 --- a/src/actions/news.js +++ b/src/actions/news.js @@ -1,4 +1,4 @@ -import { axios } from './auth'; +import axios from './session'; import { GET_NEWS, WRITE_NEWS, ADD_NEWS, DELETE_NEWS, CLEAR_WRITE, SELECT_NEWS, EDIT_NEWS } from './types'; diff --git a/src/actions/notes.js b/src/actions/notes.js new file mode 100644 index 0000000000000000000000000000000000000000..04362abded024bf05f5e633f1948309eca9e4ca6 --- /dev/null +++ b/src/actions/notes.js @@ -0,0 +1,51 @@ +import axios from './session'; +import { + GET_NOTES_BY_EVENT, + WRITE_NOTE, + ADD_EVENT_NOTE, + CLEAR_WRITE, +} from './types'; + +export const getNotesByEvent = id => ( + async (dispatch) => { + try { + const response = await axios.get('/api/v1/notes/', { params: { eventID: id } }); + dispatch({ + type: GET_NOTES_BY_EVENT, + payload: response.data, + }); + } catch (e) { + console.log(e); + } + } +); + +export const writeNote = (event) => { + return (dispatch => (dispatch({ type: WRITE_NOTE, payload: event.target.value }))); +}; + +export const postEventNote = ({ eventid, userid, note }) => ( + async (dispatch) => { + try { + const response = await axios.post('/api/v1/notes/', { + event: eventid ? eventid : '', + profile: userid ? eventid : '', + note, + }); + if (response.data.id) { + alert('Sikeres mentĂŠs!'); + dispatch({ + type: ADD_EVENT_NOTE, + payload: response.data, + }); + } + } catch (e) { + console.log(e); + } + }); + +export const clearWrite = () => ( + (dispatch) => { + dispatch({ type: CLEAR_WRITE }); + } +); diff --git a/src/actions/session.js b/src/actions/session.js new file mode 100644 index 0000000000000000000000000000000000000000..223a831985e0f07d6f7049e576b60200dbec9f70 --- /dev/null +++ b/src/actions/session.js @@ -0,0 +1,8 @@ +import ax from 'axios'; + +const axios = ax.create({ + xsrfCookieName: 'csrftoken', + xsrfHeaderName: 'X-CSRFToken', +}); + +export default axios; diff --git a/src/actions/statistics.js b/src/actions/statistics.js new file mode 100644 index 0000000000000000000000000000000000000000..ce574c26e099bad3df53766b86381527f2528699 --- /dev/null +++ b/src/actions/statistics.js @@ -0,0 +1,196 @@ +import axios from './session'; +import { + GET_EVENTS, + GET_EVENT_BY_ID, + GET_TRAINEES, + VISITOR_CHANGE, + WRITE_EVENT, + ADD_EVENT, + DELETE_EVENT, + GET_PROFILES, + GET_SELECTED_PROFILE, + SET_STATUS, + ABSENT_CHANGE, + CHANGE_NO, +} from './types'; + +export const getStaffEvents = () => ( + async (dispatch) => { + try { + const response = await axios.get('/api/v1/staff_events/'); + dispatch({ + type: GET_EVENTS, + payload: response.data, + }); + } catch (e) { + console.log(e); + } + } +); + +export const getStudentEvents = () => ( + async (dispatch) => { + try { + const response = await axios.get('/api/v1/student_events/'); + dispatch({ + type: GET_EVENTS, + payload: response.data, + }); + } catch (e) { + console.log(e); + } + } +); + +export const getEventById = id => ( + async (dispatch) => { + try { + const response = await axios.get(`/api/v1/staff_events/${id}`); + dispatch({ + type: GET_EVENT_BY_ID, + payload: response.data, + }); + } catch (e) { + console.log(e); + } + } +); + +export const getTrainees = () => ( + async (dispatch) => { + try { + const response = await axios.get('/api/v1/profiles/'); + dispatch({ + type: GET_TRAINEES, + payload: response.data, + }); + } catch (e) { + console.log(e); + } + } +); + +export const visitorChange = ({ id, value }) => { + switch (value){ + case 'Visitor': + return (dispatch => (dispatch({ type: VISITOR_CHANGE, payload: id }))); + case 'Absent': + return (dispatch => (dispatch({ type: ABSENT_CHANGE, payload: id }))); + case 'No': + return (dispatch => (dispatch({ type: CHANGE_NO, payload: id }))); + default: + } +}; + +export const submitVisitors = ({ id, visitors }) => ( + async () => { + try { + const response = await axios.patch(`/api/v1/staff_events/${id}/`, { + visitors + }); + } catch (e) { + console.log(e); + } + } +); + +export const writeEvent = ({ target: { name, value } }) => ( + (dispatch) => { + dispatch({ type: WRITE_EVENT, payload: value, target: name }); + } +); + + +export const eventDate = (name, value) => ( + (dispatch) => { + dispatch({ type: WRITE_EVENT, payload: value, target: name }); + } +); + +export const addEvent = ({ name, date, description }) => ( + async (dispatch) => { + try { + const response = await axios.post('/api/v1/staff_events/', { + name, + date, + description, + absent: [], + }); + if (response.data.id) { + alert('Sikeres mentĂŠs!'); + dispatch({ + type: ADD_EVENT, + payload: response.data, + }); + } else { + alert('MentĂŠs nem sikerĂźlt!'); + } + } catch (e) { + console.log(e); + } + } +); + +export const deleteEvent = event => ( + async (dispatch) => { + try { + const response = await axios.delete(`/api/v1/staff_events/${event.id}/`); + if (!response.data.id) { + alert('Sikeres tĂśrlĂŠs!'); + dispatch({ + type: DELETE_EVENT, + payload: event, + }); + } else { + alert('A tĂśrlĂŠs nem sikerĂźlt!'); + } + } catch (e) { + console.log(e); + } + }); + +export const getProfiles = () => ( + async (dispatch) => { + try { + const response = await axios.get('/api/v1/profiles/'); + dispatch({ + type: GET_PROFILES, + payload: response.data, + }); + } catch (e) { + console.log(e); + } + } +); + +export const setStatus = (id, status) => ( + async (dispatch) => { + try { + const response = await axios.patch(`/api/v1/profiles/${id}/`, { + role: status, + }); + if (response.data.id) { + dispatch({ + type: SET_STATUS, + payload: response.data, + }); + } + } catch (e) { + console.log(e); + } + } +); + +export const getSelectedProfile = id => ( + async (dispatch) => { + try { + const response = await axios.get(`/api/v1/profiles/${id}/`); + dispatch({ + type: GET_SELECTED_PROFILE, + payload: response.data, + }); + } catch (e) { + console.log(e); + } + } +); diff --git a/src/components/Header.js b/src/components/Header.js index 4ea3c7d2808de32ca3855ad5215d7c3964b53355..6667d99d42b6418326e4b6d036c87a68bae438b8 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -12,27 +12,42 @@ import { connect } from 'react-redux'; import { getUserData } from '../actions'; import KSZKlogo from './images/kszk_logo.svg'; - const menuItems = [ { text: 'FĹoldal', to: '/home', prefix: <Image size='mini' src={KSZKlogo} style={{ marginRight: '1.5em' }} />, + permissionLevel: 0, }, { text: 'HĂrek', to: '/news', prefix: '', + permissionLevel: 0, }, { text: 'KĂśreink', to: '/groups', prefix: '', + permissionLevel: 0, }, { text: 'Ătemterv', to: '/schedule', prefix: '', + permissionLevel: 1, + }, + { + text: 'Statisztika', + to: '/statistics', + prefix: '', + permissionLevel: 3, + }, + { + text: 'JelentkezĂŠsek', + to: '/applications', + prefix: '', + permissionLevel: 3, }, { text: 'HĂĄzi feladatok', @@ -44,7 +59,13 @@ const menuItems = [ const FixedMenu = ({ user }) => ( <Menu fixed='top' size='large' pointing> <Container> - {menuItems.map( (item, i) => <Menu.Item key={i} as={Link} to={item.to}>{item.text}</Menu.Item>)} + {menuItems.map((item, i) => + (user.permission >= item.permissionLevel || + (item.permissionLevel === 0) + ? + <Menu.Item key={i} as={Link} to={item.to}>{item.text}</Menu.Item> + : + null))} <Menu.Menu position='right'> <Menu.Item className='item'> @@ -82,6 +103,8 @@ class Header extends Component { this.setState({ visible: true }); } + + render() { const { visible } = this.state; @@ -97,11 +120,12 @@ class Header extends Component { <Container> <Menu inverted secondary size='large'> - {menuItems.map( - (item, i) => ( + {menuItems.map((item, i) => + (this.props.user.permission >= item.permissionLevel || + (item.permissionLevel === 0) ? <Menu.Item key={i} as={Link} to={item.to}>{item.prefix}{item.text}</Menu.Item> - ) - )} + : + null))} <Menu.Item position='right'> { diff --git a/src/components/forms/AddEventForm.js b/src/components/forms/AddEventForm.js new file mode 100644 index 0000000000000000000000000000000000000000..a9dd5d98df4a8e3e717b8ef2003caddc05a3fd46 --- /dev/null +++ b/src/components/forms/AddEventForm.js @@ -0,0 +1,104 @@ +import React, { Component } from 'react'; +import { Modal, Button, Form, Input, TextArea, Icon } from 'semantic-ui-react'; +import { connect } from 'react-redux'; +import { DateTimeInput } from 'semantic-ui-calendar-react'; +import { writeEvent, eventDate, addEvent } from '../../actions/statistics' +import { clearWrite } from '../../actions/news' + +class AddEventForm extends Component { + constructor(props) { + super(props); + this.state = { + showModal: false, + date: '', + }; + } + + + // Handling change in redux action creator throws an exception + // Temporal solotion using the components state to display, instead redux state + handleChange = (event, {name, value}) => { + if (this.state.hasOwnProperty(name)) { + this.setState({ [name]: value }); + } + this.props.eventDate(name, value) + } + + render() { + const { name, date, description } = this.props.newEvent; + return ( + <Modal + open={this.state.showModal} + trigger={ + <Button + size='big' + onClick={() => { this.setState({ showModal: true }); }} + >Alkalom hozzĂĄadĂĄsa + </Button> + } + > + <Modal.Header>Ăj alkalom:</Modal.Header> + <Modal.Content + style={{ + paddingTop: '20px', + }} + > + <Form> + <Form.Field + control={Input} + label='NĂŠv' + name='name' + onChange={e => this.props.writeEvent(e)} + value={name} + style={{ + marginBottom: '20px', + }} + placeholder='Title' + /> + <Form.TextArea + name='description' + label='LeĂrĂĄs:' + placeholder='RĂśvid leĂrĂĄs' + value={description} + onChange={e => this.props.writeEvent(e)} + /> + <DateTimeInput + name="date" + label="DĂĄtum:" + dateFormat='YYYY-MM-DD' + placeholder="Date" + value={this.state.date} + iconPosition="left" + onChange={this.handleChange} + /> + </Form> + </Modal.Content> + <Modal.Actions> + <Button + inverted + color='red' + onClick={() => { this.setState({ showModal: false }); + this.props.clearWrite();}} + > + <Icon name='remove' /> + Cancel + </Button> + <Button + inverted + color='green' + onClick={() => { + this.props.addEvent(this.props.newEvent); + this.setState({ showModal: false, date: '' }); + }} + > + <Icon name='checkmark' /> Add + </Button> + </Modal.Actions> + </Modal> + ); + } +} + +const mapStateToProps = ({ events: { newEvent } }) => ({ newEvent }); + +export default connect(mapStateToProps, { writeEvent, addEvent, eventDate, clearWrite })(AddEventForm); diff --git a/src/components/forms/ConfirmModal.js b/src/components/forms/ConfirmModal.js new file mode 100644 index 0000000000000000000000000000000000000000..586d624b7caceb695bbb8154e553e29b0aa03b00 --- /dev/null +++ b/src/components/forms/ConfirmModal.js @@ -0,0 +1,61 @@ +import React, { Component } from 'react'; +import { Button, Header, Icon, Modal } from 'semantic-ui-react'; + +class ConfirmModal extends Component { + constructor(props) { + super(props); + this.state = { + showModal: false, + }; + } + + close = () => this.setState({ showModal: false }) + + open = () => this.setState({ showModal: true}) + + render() { + const { button, text, onAccept } = this.props; + const open = this.state.showModal; + return ( + <Modal + open={open} + closeOnDimmerClick + trigger={button} + onOpen={this.open} + onClose={this.close} + size='small' + basic + > + <Header icon='question' content='Confirm' /> + <Modal.Content> + <p> + Biztos hogy {text}? + </p> + </Modal.Content> + <Modal.Actions> + <Button + basic + color='red' + inverted + inverted + onClick={() => this.close()} + > + <Icon name='remove' /> No + </Button> + <Button + color='green' + inverted + onClick={() => { onAccept(); + this.close(); + } + } + > + <Icon name='checkmark' /> Yes + </Button> + </Modal.Actions> + </Modal> + ); + } +} + +export default ConfirmModal; diff --git a/src/components/pages/ApplicantProfile.js b/src/components/pages/ApplicantProfile.js new file mode 100644 index 0000000000000000000000000000000000000000..535617c75f03d53c51b485bdf0578051c346a6df --- /dev/null +++ b/src/components/pages/ApplicantProfile.js @@ -0,0 +1,95 @@ +import React, { Component } from 'react'; +import { Container, Header, Item, Button, Label } from 'semantic-ui-react'; +import { connect } from 'react-redux'; +import { getSelectedProfile, setStatus } from '../../actions/statistics'; +import ConfirmModal from '../forms/ConfirmModal'; + +class ApplicantProfile extends Component { + componentWillMount() { + this.props.getSelectedProfile(this.props.match.params.id); + } + + render() { + const { id, signed, role, full_name, nick, motivation_about, motivation_exercise, motivation_profession } + = this.props.selectedProfile; + return ( + <Container style={{ padding: '60px' }}> + <Item> + <Item.Content> + <Container textAlign='center'> + <Header as='h2'>{full_name}</Header> + <Item.Meta>{nick}</Item.Meta> + </Container> + <Item.Description> + <Container textAlign='justified' style={{ padding: '30px' }}> + <Header as='h3'>MagamrĂłl, eddigi tevĂŠkenysĂŠgem:</Header> + <p>{motivation_about}</p> + <Header as='h3'>Szakmai motivĂĄciĂł:</Header> + <p>{motivation_profession}</p> + <Header as='h3'>Feladatok megoldĂĄsa:</Header> + <p>{motivation_exercise}</p> + </Container> + <Container textAlign='center' style={{ padding: '20px' }}> + <Header as='h3'>StĂĄtusz:</Header> + { signed ? + <div> + { role === 'Student' ? + <Label color='green' size='huge'>Elfogadva</Label> + : + null + } + { role === 'Staff' ? + <Label color='blue' size='huge'>Staff</Label> + : + null + } + { role === 'Applicant' ? + <Label color='orange' size='huge'>Jelentkezett</Label> + : + null + } + { role === 'Denied' ? + <Label color='red' size='huge'>ElutasĂtva</Label> + : + null + } + </div> + : + <Label color='red' size='huge'>Nem jelentkezett</Label> + } + </Container> + </Item.Description> + </Item.Content> + </Item> + { signed && role !== 'Staff' ? + <Container textAlign='center'> + <ConfirmModal + button={ + <Button + color='green' + >JelentkezĂŠs elfogadĂĄsa + </Button>} + text='elfogadod a jelentkezĂŠst' + onAccept={() => this.props.setStatus(id, 'Student')} + /> + <ConfirmModal + button={ + <Button + color='red' + >JelentkezĂŠs elutasĂtĂĄsa + </Button>} + text='elutasĂtod a jelentkezĂŠst' + onAccept={() => this.props.setStatus(id, 'Denied')} + /> + </Container> + : + null + } + </Container> + ); + } +} + +const mapStateToProps = ({ trainees: { selectedProfile } }) => ({ selectedProfile }); + +export default connect(mapStateToProps, { getSelectedProfile, setStatus })(ApplicantProfile); diff --git a/src/components/pages/Applications.js b/src/components/pages/Applications.js new file mode 100644 index 0000000000000000000000000000000000000000..6fac3330108f0610c51405e8b85772edbd6aab6c --- /dev/null +++ b/src/components/pages/Applications.js @@ -0,0 +1,95 @@ +import React, { Component } from 'react'; +import { Link } from 'react-router-dom'; +import { Container, Table, Label, Button } from 'semantic-ui-react'; +import { connect } from 'react-redux'; +import { getProfiles, setStatus } from '../../actions/statistics'; +import ConfirmModal from '../forms/ConfirmModal'; + +class Applications extends Component { + componentWillMount() { + this.props.getProfiles(); + } + + renderApplicants() { + return this.props.profiles.map((profile) => + { return ( + <Table.Row> + <Table.Cell> + <Link to={`applicant/${profile.id}`}> + {profile.full_name} + </Link> + </Table.Cell> + { profile.signed ? + <Table.Cell textAlign='center'> + { profile.role === 'Student' ? + <Label color='green'>Elfogadva</Label> + : + null + } + { profile.role === 'Staff' ? + <Label color='blue'>Staff</Label> + : + null + } + { profile.role === 'Applicant' ? + <Label color='orange'>Jelentkezett</Label> + : + null + } + { profile.role === 'Denied' ? + <Label color='red'>ElutasĂtva</Label> + : + null + } + </Table.Cell> + : + <Table.Cell textAlign='center'> + <Label color='red'>Nem jelentkezett</Label> + </Table.Cell> + } + <Table.Cell> + <ConfirmModal + button = {<Button + color='blue' + size='tiny' + > + ADD STAFF STATUS + </Button>} + text='staff jogot adsz neki' + onAccept={() => this.props.setStatus(profile.id, 'Staff')} + /> + </Table.Cell> + </Table.Row> + ); + }); + } + + render() { + return ( + <Container + textAlign='center' + style={{ + padding: '80px' + }} + > + <Table color='blue' celled selectable compact> + <Table.Header> + <Table.Row> + <Table.HeaderCell>Jelentkezettek</Table.HeaderCell> + <Table.HeaderCell textAlign='center'>JelentkezĂŠs stĂĄtusza:</Table.HeaderCell> + <Table.HeaderCell /> + </Table.Row> + </Table.Header> + + <Table.Body> + {this.renderApplicants()} + </Table.Body> + </Table> + </Container> + ); + } +} + +const mapStateToProps = ({ trainees: { profiles }, user }) => ({ profiles, user }); + +export default connect(mapStateToProps, { getProfiles, setStatus })(Applications); diff --git a/src/components/pages/EventDetail.js b/src/components/pages/EventDetail.js new file mode 100644 index 0000000000000000000000000000000000000000..e48fe9bffbeb815014d6804003e6ab4f53ca0976 --- /dev/null +++ b/src/components/pages/EventDetail.js @@ -0,0 +1,185 @@ +import React, { Component } from 'react'; +import { + Container, + Item, + Button, + Comment, + Form, + Header, + Table, + Icon, + Checkbox, + Popup, + Grid, +} from 'semantic-ui-react'; +import { connect } from 'react-redux'; +import moment from 'moment'; +import { getEventById, getTrainees, visitorChange, submitVisitors } from '../../actions/statistics'; +import { getNotesByEvent, writeNote, clearWrite, postEventNote } from '../../actions/notes'; +import TraineeTableRow from './TraineeTableRow'; + + +class EventDetail extends Component { + constructor(props) { + super(props); + this.state = { + edit: false, + }; + } + + componentWillMount() { + this.props.getEventById(this.props.match.params.id); + this.props.getTrainees(); + this.props.getNotesByEvent(this.props.match.params.id); + } + + + renderTrainees() { + const event = this.props.selectedEvent; + const note = this.props.actualNote; + return this.props.trainees.map((item) => { + const notes = this.props.eventNotes.filter(note => note.profile === item.id); + return ( + <TraineeTableRow + selectedEvent={event} + notes={notes} + trainee={item} + edit={this.state.edit} + /> + ); + }); + } + + renderEvent() { + const { name, date, description } = this.props.selectedEvent; + return ( + <Item> + <Item.Header as='h2'>{name}</Item.Header> + <Item.Header as='h3'>DĂĄtum: {moment(date).format('LL')}</Item.Header> + <Container textAlign='justified'> + <Item.Header as='h3'>LeĂrĂĄs</Item.Header> + <Item.Content>{description}</Item.Content> + </Container> + </Item> + ); + } + + renderComments() { + const notes = this.props.eventNotes; + return notes.map((note) => { + if (!note.profile) { + return ( + <Comment> + <Comment.Content> + <Comment.Author>{note.created_by_name}</Comment.Author> + <Comment.Metadata> + {moment(note.created_at).format('LL')} + </Comment.Metadata> + <Comment.Text> + {note.note} + </Comment.Text> + </Comment.Content> + </Comment>); + } + return ''; + }); + } + + render() { + const event = this.props.selectedEvent; + const note = this.props.actualNote; + return ( + <Container + style={{ + padding: '80px' + }} + > + <Container textAlign='center'> + { this.props.selectedEvent && this.props.trainees ? + this.renderEvent() + : + '' + } + </Container> + <Table celled centered> + <Table.Header> + <Table.Row> + <Table.HeaderCell>NĂŠv</Table.HeaderCell> + <Table.HeaderCell>Jelen volt</Table.HeaderCell> + <Table.HeaderCell>MegjegyzĂŠsek</Table.HeaderCell> + </Table.Row> + </Table.Header> + <Table.Body> + { this.props.selectedEvent ? + this.renderTrainees() + : + '' + } + </Table.Body> + </Table> + <Button + onClick={() => this.setState({ edit: true })} + > + Edit + </Button> + { this.state.edit ? + <Button + onClick={() => { + this.setState({ edit: false }); + this.props.submitVisitors(this.props.selectedEvent); + } + } + >Save + </Button> + : + '' + } + <Comment.Group> + <Header dividing> + MegjegyzĂŠsek + </Header> + {this.props.eventNotes ? + this.renderComments() + : + '' + } + <Form reply> + <Form.TextArea + value={note.note} + onChange={e => this.props.writeNote(e)} + /> + <Button + onClick={() => { + this.props.postEventNote({ eventid: event.id, + note: note.note }); + this.props.clearWrite(); + } + } + content='MegjegyzĂŠs hozzĂĄadĂĄsa' + labelPosition='left' + icon='edit' + primary + /> + </Form> + </Comment.Group> + </Container> + ); + } +} + +const mapStateToProps = ({ + notes: { eventNotes, actualNote }, + events: { selectedEvent }, + trainees: { trainees } +}) => ({ eventNotes, selectedEvent, trainees, actualNote }); + +export default connect(mapStateToProps, { + getEventById, + getTrainees, + visitorChange, + getNotesByEvent, + submitVisitors, + writeNote, + clearWrite, + postEventNote, +})(EventDetail); diff --git a/src/components/pages/Events.js b/src/components/pages/Events.js new file mode 100644 index 0000000000000000000000000000000000000000..3d5c8fca3566e71690424e84075a51c52c0a0130 --- /dev/null +++ b/src/components/pages/Events.js @@ -0,0 +1,65 @@ +import React, { Component } from 'react'; +import moment from 'moment'; +import { Link } from 'react-router-dom'; +import { Container, Table, Button } from 'semantic-ui-react'; +import { connect } from 'react-redux'; +import { getStaffEvents, deleteEvent } from '../../actions/statistics'; +import AddEventForm from '../forms/AddEventForm'; + +class Events extends Component { + componentWillMount() { + this.props.getStaffEvents(); + } + + renderEvents() { + return this.props.events.map((event) => + { return ( + <Table.Row> + <Table.Cell> + <Link to={`events/${event.id}`}> + {event.name} + </Link> + </Table.Cell> + <Table.Cell>{moment(event.date).format('LL')}</Table.Cell> + <Table.Cell>{event.visitor_number}</Table.Cell> + <Table.Cell> + <Button + onClick={() => this.props.deleteEvent(event)} + color='red' + compact + size='small' + > + Delete + </Button> + </Table.Cell> + </Table.Row> + ); + }); + } + + render() { + return ( + <Container textAlign='center'> + <Table color='blue' celled selectable compact> + <Table.Header> + <Table.Row> + <Table.HeaderCell>Alkalom neve</Table.HeaderCell> + <Table.HeaderCell>DĂĄtum</Table.HeaderCell> + <Table.HeaderCell>Jelen voltak</Table.HeaderCell> + <Table.HeaderCell /> + </Table.Row> + </Table.Header> + + <Table.Body> + {this.props.events ? this.renderEvents() : 'Nincs mĂŠg alaklom beĂrva'} + </Table.Body> + </Table> + <AddEventForm /> + </Container> + ); + } +} + +const mapStateToProps = ({ events: { events }, user }) => ({ events, user }); + +export default connect(mapStateToProps, { getStaffEvents, deleteEvent })(Events); diff --git a/src/components/pages/News.js b/src/components/pages/News.js index acaad62ee38619b4a59fa8383f71b419b262e31b..41260ea92dd30a22084df8dc50bcb0e638380b97 100644 --- a/src/components/pages/News.js +++ b/src/components/pages/News.js @@ -26,6 +26,7 @@ class News extends Component { <Grid.Column floated='center' width={12}> {item.title} </Grid.Column> + { this.props.user.role === 'Staff' ? <Grid.Column floated='right' width={4}> <EditNewsForm onClick={() => this.props.setSelectedNews(item)} @@ -39,6 +40,7 @@ class News extends Component { Delete </Button> </Grid.Column> + : null } </Grid> </Item.Header> <Item.Description className='news-text' style={{ fontSize: '1.33em' }}> @@ -75,7 +77,10 @@ class News extends Component { <Segment style={{ padding: '3em 3em' }} vertical> {/* { this.props.user.is_superuser ? <AddNewsForm /> : ''} */} <Container text textAlign='center'> - <AddNewsForm /> + {this.props.user.role === 'Staff' ? + <AddNewsForm /> + : + null} <Item.Group divided> {this.renderNews()} </Item.Group> diff --git a/src/components/pages/Schedule.js b/src/components/pages/Schedule.js index d6a6ac619ed242e3c78a0af5d41896f4c227df90..db3877fad2df7893212e1164a84a5fe6595d41e2 100644 --- a/src/components/pages/Schedule.js +++ b/src/components/pages/Schedule.js @@ -1,26 +1,77 @@ import React, { Component } from 'react'; -import { Container, Header, Segment } from 'semantic-ui-react'; +import { Container, Accordion, Icon, Grid } from 'semantic-ui-react'; +import { connect } from 'react-redux'; +import moment from 'moment'; +import { getStudentEvents } from '../../actions/statistics'; + +class Schedule extends Component { + state = { activeIndex: 0 } + + componentWillMount() { + this.props.getStudentEvents(); + } + + handleClick = (e, titleProps) => { + const { index } = titleProps + const { activeIndex } = this.state + const newIndex = activeIndex === index ? -1 : index + + this.setState({ activeIndex: newIndex }) + } -export default class Schedule extends Component { render() { + const { activeIndex } = this.state + + const events = this.props.events; + const panels = events.map(event => ( + <> + <Accordion.Title + active={activeIndex === event.id} + index={event.id} + onClick={this.handleClick} + > + <h2> + <Grid> + <Grid.Column floated='left' width={5} textAlign='left'> + <Icon name='quidditch' color='blue' />{event.name} + </Grid.Column> + <Grid.Column floated='right' width={8} textAlign='right'> + {moment(event.date).locale('hu').format('LLLL')} + </Grid.Column> + </Grid> + </h2> + </Accordion.Title> + <Accordion.Content active={activeIndex === event.id}> + <p> + {event.description} + </p> + </Accordion.Content> + </> + )); + return ( - <div> - <Segment inverted textAlign='center' vertical> - <Container> - <Header - as='h1' - content='Ătemterv - Hamarosan' - inverted - style={{ - fontSize: '3em', - fontWeight: 'normal', - marginBottom: 0, - marginTop: '0.5em', - }} - /> - </Container> - </Segment> - </div> + <Container + textAlign='center' + style={{ + padding: '60px' + }} + > + <h2>KĂŠpzĂŠs alkalmak:</h2> + <Accordion + fluid + styled + defaultActiveIndex={-1} + panels={panels} + > + {panels} + </Accordion> + <h2>TĂĄbor:</h2> + </Container> ); } } + + +const mapStateToProps = ({ events: { events }, user }) => ({ events, user }); + +export default connect(mapStateToProps, { getStudentEvents })(Schedule); diff --git a/src/components/pages/Statistics.js b/src/components/pages/Statistics.js index 3c2baaf2395b0ef0385e2489547586c34430228d..44a4cdde14ce51f9060973b59c3967c4d96f170d 100644 --- a/src/components/pages/Statistics.js +++ b/src/components/pages/Statistics.js @@ -1,25 +1,43 @@ import React, { Component } from 'react'; -import { Container, Header, Segment } from 'semantic-ui-react'; +import { Container, Menu } from 'semantic-ui-react'; +import Events from './Events' +import Trainees from './Trainees' export default class Statistics extends Component { + state = { activeItem: 'events' } + handleItemClick = (e, { name }) => this.setState({ activeItem: name }) + render() { + const { activeItem } = this.state return ( <div> - <Segment inverted textAlign='center' vertical> - <Container> - <Header - as='h1' - content='StatisztikĂĄk - Hamarosan' - inverted - style={{ - fontSize: '3em', - fontWeight: 'normal', - marginBottom: 0, - marginTop: '0.5em', - }} - /> - </Container> - </Segment> + <Container + textAlign="center" + style={{ + padding: '60px', + }} + > + <Menu + attached='top' + tabular + size='huge' + compact={true}> + <Menu.Item + name='events' + active={activeItem === 'events'} + onClick={this.handleItemClick} + >Alkalmak + </Menu.Item> + <Menu.Item + name='trainees' + active={activeItem === 'trainees'} + onClick={this.handleItemClick} + >KĂŠpzĹdĹk + </Menu.Item> + </Menu> + { activeItem === 'events' ? <Events /> : '' } + { activeItem === 'trainees' ? <Trainees /> : '' } + </Container> </div> ); } diff --git a/src/components/pages/TraineeTableRow.js b/src/components/pages/TraineeTableRow.js new file mode 100644 index 0000000000000000000000000000000000000000..3a9ae4fb2e1e478c5f3b550bafc29694eff1377a --- /dev/null +++ b/src/components/pages/TraineeTableRow.js @@ -0,0 +1,150 @@ +import React, { Component } from 'react'; +import { + Comment, + Table, + Icon, + Popup, + Grid, + Button, + Form, + Dropdown, +} from 'semantic-ui-react'; +import { connect } from 'react-redux'; +import { visitorChange, submitVisitors } from '../../actions/statistics'; +import { writeNote, clearWrite, postEventNote } from '../../actions/notes'; + +const visitStates = [ + { + text: 'Igen', + value: 'Visitor', + }, + { + text: 'SzĂłlt h nem', + value: 'Absent', + }, + { + text: 'Nem', + value: 'No', + } +] + +class TraineeTableRow extends Component { + constructor(props) { + super(props); + this.state = { + showAddPopup: false, + showMorePopup: false, + }; + } + + triggerAdd = () => this.setState({ ...this.state, showAddPopup: !this.state.showAddPopup}) + triggerMore = () => this.setState({ ...this.state, showMorePopup: !this.state.showMorePopup }) + + render() { + const note = this.props.actualNote; + const { trainee, edit, actualNote, selectedEvent, notes } = this.props; + const isVisitor = selectedEvent.visitors.includes(trainee.id); + const isAbsent = selectedEvent.absent.includes(trainee.id); + return ( + <Table.Row> + <Table.Cell> + {trainee.full_name} + </Table.Cell> + {!this.props.edit ? + <Table.Cell textAlign='center'> + { + isVisitor ? + <Icon color='green' name='checkmark' /> + : + isAbsent ? + <Icon color='orange' name='minus' /> + : + <Icon color='red' name='cancel' /> + } + </Table.Cell> + : + <Table.Cell textAlign='center'> + <Dropdown + defaultValue={isVisitor ? 'Visitor' : isAbsent ? 'Absent' : 'No'} + selection + options={visitStates} + onChange={(_, v) => this.props.visitorChange({ id : trainee.id, value: v.value })} + /> + </Table.Cell> + } + <Table.Cell> + <Grid> + <Grid.Row> + <Grid.Column floated='left' width={8} textAlign='left'> + + {notes.length > 0 ? + <Comment> + <Comment.Content> + <Comment.Author>{notes[0].created_by_name}</Comment.Author> + <Comment.Text> + {notes[0].note.length > 50 ? notes[0].note.slice(0, 50).concat('...') + : + notes[0].note } + </Comment.Text> + </Comment.Content> + </Comment> + : + null + } + </Grid.Column> + <Grid.Column floated='right' width={3} textAlign='right'> + {notes.length > 0 ? + <Popup + basic + open={this.state.showMorePopup} + trigger={<Button icon='comment alternate outline' onClick={this.triggerMore} />} + content={notes.map((note) => { + return ( + <Comment.Content> + <Comment.Author>{note.created_by_name}</Comment.Author> + <Comment.Text> + {note.note} + </Comment.Text> + </Comment.Content> + ); + })} + /> + : + null} + <Popup + trigger={<Button icon='plus' onClick={this.triggerAdd}/>} + basic + open={this.state.showAddPopup} + content={ + <Form reply> + <Form.TextArea + value={note.note} + onChange={e => this.props.writeNote(e)} + /> + <Button + onClick={() => { + this.props.postEventNote({ eventid:selectedEvent.id, + userid: trainee.id, + note: note.note }); + this.props.clearWrite(); + } + } + content='MegjegyzĂŠs hozzĂĄadĂĄsa' + labelPosition='left' + icon='edit' + primary + /> + </Form> + } + /> + </Grid.Column> + </Grid.Row> + </Grid> + </Table.Cell> + </Table.Row> + ); + } +} +const mapStateToProps = ({ notes: { actualNote } }) => ({ actualNote }) + +export default connect(mapStateToProps, { writeNote, clearWrite, postEventNote, visitorChange})(TraineeTableRow) diff --git a/src/components/pages/Trainees.js b/src/components/pages/Trainees.js new file mode 100644 index 0000000000000000000000000000000000000000..804c801a8b82f25d8b33da7d5c8732dd15399413 --- /dev/null +++ b/src/components/pages/Trainees.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react'; +import { Container, Table, Icon } from 'semantic-ui-react'; +import { connect } from 'react-redux'; +import { getTrainees, getStaffEvents } from '../../actions/statistics'; + +class Trainees extends Component { + componentWillMount() { + this.props.getTrainees(); + this.props.getStaffEvents(); + } + + renderVisitedStatus(trainee) { + return (this.props.events.map((event) => { + if (event.visitors.includes(trainee.id)) { + return ( + <Table.Cell textAlign='center'> + <Icon color='green' name='checkmark' /> + </Table.Cell>); + } + return ( + <Table.Cell textAlign='center'> + <Icon color='red' name='cancel' /> + </Table.Cell>); + })); + } + + renderTrainees() { + return this.props.trainees.map((trainee) => + { return ( + <Table.Row> + <Table.Cell> + {trainee.full_name} + </Table.Cell> + {this.renderVisitedStatus(trainee)} + </Table.Row> + ); + }); + } + + renderTableHeader() { + return (this.props.events.map(event => ( + <Table.HeaderCell> + {event.name} + </Table.HeaderCell>))); + } + + render() { + return ( + <Container textAlign='center'> + <Table color='blue' celled selectable compact> + <Table.Header> + <Table.Row> + <Table.HeaderCell>KĂŠpzĹdĹk</Table.HeaderCell> + { this.renderTableHeader() } + </Table.Row> + </Table.Header> + + <Table.Body> + {this.props.trainees ? this.renderTrainees() : 'Nincsenek kĂŠpzĹdĹk'} + </Table.Body> + </Table> + </Container> + ); + } +} + +const mapStateToProps = ({ trainees: { trainees }, events: { events }, user }) => ({ trainees, events, user }); + +export default connect(mapStateToProps, { getTrainees, getStaffEvents })(Trainees); diff --git a/src/reducers/EventReducer.js b/src/reducers/EventReducer.js new file mode 100644 index 0000000000000000000000000000000000000000..5dd067d018da28027a91239edfb17d9b7a41637c --- /dev/null +++ b/src/reducers/EventReducer.js @@ -0,0 +1,83 @@ +import { + GET_EVENTS, + GET_EVENT_BY_ID, + VISITOR_CHANGE, + WRITE_EVENT, + ADD_EVENT, + DELETE_EVENT, + CLEAR_WRITE, + ABSENT_CHANGE, + CHANGE_NO, +} from '../actions/types'; + +const INITIAL_STATE = { events: [], newEvent: {} }; + +export default (state = INITIAL_STATE, action) => { + switch (action.type) { + case GET_EVENTS: + return { ...state, events: [...action.payload] }; + case GET_EVENT_BY_ID: + return { ...state, selectedEvent: action.payload }; + case VISITOR_CHANGE: + if (state.selectedEvent.visitors.includes(action.payload)) { + // Benne van nem kell megvĂĄltoztatni + return { ...state } + } + if (state.selectedEvent.absent.indexOf(action.payload) > -1) { + // Ha az absentbe van ki kell venni + state.selectedEvent.absent.splice(state.selectedEvent.absent.indexOf(action.payload), 1); + } + state.selectedEvent.visitors.push(action.payload) + return { + ...state, + selectedEvent: { + ...state.selectedEvent, + visitors: state.selectedEvent.visitors, + absent: state.selectedEvent.absent, + }, + }; + case ABSENT_CHANGE: + if (state.selectedEvent.absent.includes(action.payload)) { + return { ...state }; + } + if (state.selectedEvent.visitors.indexOf(action.payload) > -1) { + state.selectedEvent.visitors.splice(state.selectedEvent.visitors.indexOf(action.payload), 1); + } + state.selectedEvent.absent.push(action.payload); + return { + ...state, + selectedEvent: { + ...state.selectedEvent, + visitors: state.selectedEvent.visitors, + absent: state.selectedEvent.absent, + }, + }; + case CHANGE_NO: + if (state.selectedEvent.visitors.indexOf(action.payload) > -1) { + state.selectedEvent.visitors.splice(state.selectedEvent.visitors.indexOf(action.payload), 1); + } + if (state.selectedEvent.absent.indexOf(action.payload) > -1) { + // Ha az absentbe van ki kell venni + state.selectedEvent.absent.splice(state.selectedEvent.absent.indexOf(action.payload), 1); + } + return { + ...state, + selectedEvent: { + ...state.selectedEvent, + visitors: state.selectedEvent.visitors, + absent: state.selectedEvent.absent, + }, + }; + case WRITE_EVENT: + return { ...state, newEvent: { ...state.newEvent, [action.target]: action.payload } }; + case ADD_EVENT: + return { ...state, events: [...state.events, action.payload] }; + case DELETE_EVENT: + state.events.splice(state.events.indexOf(action.payload), 1); + return { ...state, events: [...state.events] }; + case CLEAR_WRITE: + return { ...state, newEvent: {} }; + default: + return state; + } +}; diff --git a/src/reducers/NoteReducer.js b/src/reducers/NoteReducer.js new file mode 100644 index 0000000000000000000000000000000000000000..90e4c8db7dba6f73a721b4eec4bbb8d51d3b5101 --- /dev/null +++ b/src/reducers/NoteReducer.js @@ -0,0 +1,23 @@ +import { + GET_NOTES_BY_EVENT, + WRITE_NOTE, + ADD_EVENT_NOTE, + CLEAR_WRITE, +} from '../actions/types'; + +const INITIAL_STATE = { eventNotes: [], actualNote: {} }; + +export default (state = INITIAL_STATE, action) => { + switch (action.type) { + case GET_NOTES_BY_EVENT: + return { ...state, eventNotes: action.payload }; + case WRITE_NOTE: + return { ...state, actualNote: { ...state.actualNote, note: action.payload } }; + case ADD_EVENT_NOTE: + return { ...state, eventNotes: [...state.eventNotes, action.payload] }; + case CLEAR_WRITE: + return { ...state, actualNote: { note: '' } }; + default: + return state; + } +}; diff --git a/src/reducers/TraineeReducer.js b/src/reducers/TraineeReducer.js new file mode 100644 index 0000000000000000000000000000000000000000..526cd7ace95c4e1b758d31e7e81d84242e46a927 --- /dev/null +++ b/src/reducers/TraineeReducer.js @@ -0,0 +1,23 @@ +import { GET_TRAINEES, GET_PROFILES, GET_SELECTED_PROFILE, SET_STATUS } from '../actions/types'; + +const INITIAL_STATE = { profiles: [], selectedProfile: {} }; + +export default (state = INITIAL_STATE, action) => { + switch (action.type) { + case GET_TRAINEES: + return { ...state, trainees: [...action.payload] }; + case GET_PROFILES: + return { ...state, profiles: [...action.payload] }; + case GET_SELECTED_PROFILE: + return { ...state, selectedProfile: action.payload }; + case SET_STATUS: + const index = state.profiles.findIndex(item => item.id === action.payload.id); + state.profiles.splice(index, 1, action.payload); + if (action.payload.id === state.selectedProfile.id) { + return { ...state, profiles: [...state.profiles], selectedProfile: action.payload }; + } + return { ...state, profiles: [...state.profiles] } + default: + return state; + } +};