diff options
Diffstat (limited to 'app/javascript/time_tables')
27 files changed, 1872 insertions, 0 deletions
diff --git a/app/javascript/time_tables/actions/index.js b/app/javascript/time_tables/actions/index.js new file mode 100644 index 000000000..13cb96b64 --- /dev/null +++ b/app/javascript/time_tables/actions/index.js @@ -0,0 +1,328 @@ +import range from 'lodash/range' +import assign from 'lodash/assign' +import reject from 'lodash/reject' +import some from 'lodash/some' +import every from 'lodash/every' +import clone from '../../helpers/clone' +const I18n = clone(window, "I18n") + +const actions = { + weekDays: (index) => { + return range(1, 8).map(n => I18n.time_tables.edit.metas.days[n]) + }, + strToArrayDayTypes: (str) =>{ + return actions.weekDays().map(day => str.indexOf(day) !== -1) + }, + arrayToStrDayTypes: (dayTypes) => { + let newDayTypes = dayTypes.reduce((arr, dayActive, i) => { + if (dayActive) arr.push(actions.weekDays()[i]) + return arr + }, []) + + return newDayTypes.join(',') + }, + fetchingApi: () =>({ + type: 'FETCH_API' + }), + receiveErrors : (json) => ({ + type: "RECEIVE_ERRORS", + json + }), + unavailableServer: () => ({ + type: 'UNAVAILABLE_SERVER' + }), + receiveMonth: (json) => ({ + type: 'RECEIVE_MONTH', + json + }), + receiveTimeTables: (json) => ({ + type: 'RECEIVE_TIME_TABLES', + json + }), + goToPreviousPage : (dispatch, pagination) => ({ + type: 'GO_TO_PREVIOUS_PAGE', + dispatch, + pagination, + nextPage : false + }), + goToNextPage : (dispatch, pagination) => ({ + type: 'GO_TO_NEXT_PAGE', + dispatch, + pagination, + nextPage : true + }), + changePage : (dispatch, val) => ({ + type: 'CHANGE_PAGE', + dispatch, + page: val + }), + updateDayTypes: (dayTypes) => ({ + type: 'UPDATE_DAY_TYPES', + dayTypes + }), + updateCurrentMonthFromDaytypes: (dayTypes) => ({ + type: 'UPDATE_CURRENT_MONTH_FROM_DAYTYPES', + dayTypes + }), + updateComment: (comment) => ({ + type: 'UPDATE_COMMENT', + comment + }), + updateColor: (color) => ({ + type: 'UPDATE_COLOR', + color + }), + select2Tags: (selectedTag) => ({ + type: 'UPDATE_SELECT_TAG', + selectedItem: { + id: selectedTag.id, + name: selectedTag.name + } + }), + unselect2Tags: (selectedTag) => ({ + type: 'UPDATE_UNSELECT_TAG', + selectedItem: { + id: selectedTag.id, + name: selectedTag.name + } + }), + deletePeriod: (index, dayTypes) => ({ + type: 'DELETE_PERIOD', + index, + dayTypes + }), + openAddPeriodForm: () => ({ + type: 'OPEN_ADD_PERIOD_FORM' + }), + openEditPeriodForm: (period, index) => ({ + type: 'OPEN_EDIT_PERIOD_FORM', + period, + index + }), + closePeriodForm: () => ({ + type: 'CLOSE_PERIOD_FORM' + }), + resetModalErrors: () => ({ + type: 'RESET_MODAL_ERRORS' + }), + updatePeriodForm: (val, group, selectType) => ({ + type: 'UPDATE_PERIOD_FORM', + val, + group, + selectType + }), + validatePeriodForm: (modalProps, timeTablePeriods, metas, timetableInDates, error) => ({ + type: 'VALIDATE_PERIOD_FORM', + modalProps, + timeTablePeriods, + metas, + timetableInDates, + error + }), + addIncludedDate: (index, dayTypes, date) => ({ + type: 'ADD_INCLUDED_DATE', + index, + dayTypes, + date + }), + removeIncludedDate: (index, dayTypes, date) => ({ + type: 'REMOVE_INCLUDED_DATE', + index, + dayTypes, + date + }), + addExcludedDate: (index, dayTypes, date) => ({ + type: 'ADD_EXCLUDED_DATE', + index, + dayTypes, + date + }), + removeExcludedDate: (index, dayTypes, date) => ({ + type: 'REMOVE_EXCLUDED_DATE', + index, + dayTypes, + date + }), + openConfirmModal : (callback) => ({ + type : 'OPEN_CONFIRM_MODAL', + callback + }), + showErrorModal: (error) => ({ + type: 'OPEN_ERROR_MODAL', + error + }), + closeModal : () => ({ + type : 'CLOSE_MODAL' + }), + monthName(strDate) { + let monthList = range(1,13).map(n => I18n.calendars.months[n]) + let date = new Date(strDate) + return monthList[date.getMonth()] + }, + getHumanDate(strDate, mLimit) { + let origin = strDate.split('-') + let D = origin[2] + let M = actions.monthName(strDate).toLowerCase() + let Y = origin[0] + + if(mLimit && M.length > mLimit) { + M = M.substr(0, mLimit) + '.' + } + + return (D + ' ' + M + ' ' + Y) + }, + getLocaleDate(strDate) { + let date = new Date(strDate) + return date.toLocaleDateString() + }, + updateSynthesis: ({current_month, time_table_dates: dates, time_table_periods: periods}) => { + let newPeriods = reject(periods, 'deleted') + let improvedCM = current_month.map((d, i) => { + let isInPeriod = actions.isInPeriod(newPeriods, d.date) + let isIncluded = some(dates, {'date': d.date, 'in_out': true}) + + return assign({}, current_month[i], { + in_periods: isInPeriod, + include_date: isIncluded, + excluded_date: !isInPeriod ? false : current_month[i].excluded_date + }) + }) + return improvedCM + }, + isInPeriod: (periods, date) => { + date = new Date(date) + + for (let period of periods) { + let begin = new Date(period.period_start) + let end = new Date(period.period_end) + if (date >= begin && date <= end) return true + } + + return false + }, + checkConfirmModal: (event, callback, stateChanged, dispatch, metas, timetable) => { + if(stateChanged){ + const error = actions.errorModalKey(timetable.time_table_periods, metas.day_types) + if(error){ + return actions.showErrorModal(error) + }else{ + return actions.openConfirmModal(callback) + } + }else{ + dispatch(actions.fetchingApi()) + return callback + } + }, + formatDate: (props) => { + return props.year + '-' + props.month + '-' + props.day + }, + checkErrorsInPeriods: (start, end, index, periods) => { + let error = '' + start = new Date(start) + end = new Date(end) + + for (let i = 0; i < periods.length; i++) { + let period = periods[i] + if (index !== i && !period.deleted) { + if (new Date(period.period_start) <= end && new Date(period.period_end) >= start) { + error = I18n.time_tables.edit.error_submit.periods_overlaps + break + } + } + } + return error + }, + checkErrorsInDates: (start, end, in_days) => { + let error = '' + start = new Date(start) + end = new Date(end) + + for (let day of in_days) { + if (start <= new Date(day.date) && end >= new Date(day.date)) { + error = I18n.time_tables.edit.error_submit.dates_overlaps + break + } + } + return error + }, + fetchTimeTables: (dispatch, nextPage) => { + let urlJSON = window.location.pathname.split('/', 5).join('/') + if(nextPage) { + urlJSON += "/month.json?date=" + nextPage + }else{ + urlJSON += ".json" + } + let hasError = false + fetch(urlJSON, { + credentials: 'same-origin', + }).then(response => { + if(response.status == 500) { + hasError = true + } + return response.json() + }).then((json) => { + if(hasError == true) { + dispatch(actions.unavailableServer()) + } else { + if(nextPage){ + dispatch(actions.receiveMonth(json)) + }else{ + dispatch(actions.receiveTimeTables(json)) + } + } + }) + }, + submitTimetable: (dispatch, timetable, metas, next) => { + dispatch(actions.fetchingApi()) + let strDayTypes = actions.arrayToStrDayTypes(metas.day_types) + metas.day_types = strDayTypes + let sentState = assign({}, timetable, metas) + let urlJSON = window.location.pathname.split('/', 5).join('/') + let hasError = false + fetch(urlJSON + '.json', { + credentials: 'same-origin', + method: 'PATCH', + contentType: 'application/json; charset=utf-8', + Accept: 'application/json', + body: JSON.stringify(sentState), + headers: { + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') + } + }).then(response => { + if(!response.ok) { + hasError = true + } + return response.json() + }).then((json) => { + if(hasError == true) { + dispatch(actions.receiveErrors(json)) + } else { + if(next) { + dispatch(next) + } else { + dispatch(actions.receiveTimeTables(json)) + } + } + }) + }, + errorModalKey: (periods, dayTypes) => { + const withoutPeriodsWithDaysTypes = reject(periods, 'deleted').length == 0 && some(dayTypes) && "withoutPeriodsWithDaysTypes" + const withPeriodsWithoutDayTypes = reject(periods, 'deleted').length > 0 && every(dayTypes, dt => dt == false) && "withPeriodsWithoutDayTypes" + + return (withoutPeriodsWithDaysTypes || withPeriodsWithoutDayTypes) && (withoutPeriodsWithDaysTypes ? "withoutPeriodsWithDaysTypes" : "withPeriodsWithoutDayTypes") + + }, + errorModalMessage: (errorKey) => { + switch (errorKey) { + case "withoutPeriodsWithDaysTypes": + return I18n.time_tables.edit.error_modal.withoutPeriodsWithDaysTypes + case "withPeriodsWithoutDayTypes": + return I18n.time_tables.edit.error_modal.withPeriodsWithoutDayTypes + default: + return errorKey + + } + } +} + +export default actions
\ No newline at end of file diff --git a/app/javascript/time_tables/components/ConfirmModal.js b/app/javascript/time_tables/components/ConfirmModal.js new file mode 100644 index 000000000..d89170ee7 --- /dev/null +++ b/app/javascript/time_tables/components/ConfirmModal.js @@ -0,0 +1,50 @@ +import React, { PropTypes } from 'react' + +export default function ConfirmModal({dispatch, modal, onModalAccept, onModalCancel, timetable, metas}, {I18n}) { + return ( + <div className={'modal fade ' + ((modal.type == 'confirm') ? 'in' : '')} id='ConfirmModal'> + <div className='modal-container'> + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <h4 className='modal-title'>{I18n.time_tables.edit.confirm_modal.title}</h4> + </div> + <div className='modal-body'> + <div className='mt-md mb-md'> + <p>{I18n.time_tables.edit.confirm_modal.message}</p> + </div> + </div> + <div className='modal-footer'> + <button + className='btn btn-link' + data-dismiss='modal' + type='button' + onClick={() => { onModalCancel(modal.confirmModal.callback) }} + > + {I18n.cancel} + </button> + <button + className='btn btn-primary' + data-dismiss='modal' + type='button' + onClick={() => { onModalAccept(modal.confirmModal.callback, timetable, metas) }} + > + {I18n.actions.submit} + </button> + </div> + </div> + </div> + </div> + </div> + ) +} + +ConfirmModal.propTypes = { + modal: PropTypes.object.isRequired, + onModalAccept: PropTypes.func.isRequired, + onModalCancel: PropTypes.func.isRequired +} + +ConfirmModal.contextTypes = { + I18n: PropTypes.object +}
\ No newline at end of file diff --git a/app/javascript/time_tables/components/ErrorModal.js b/app/javascript/time_tables/components/ErrorModal.js new file mode 100644 index 000000000..e810f49ab --- /dev/null +++ b/app/javascript/time_tables/components/ErrorModal.js @@ -0,0 +1,42 @@ +import React, { PropTypes } from 'react' +import actions from '../actions' + +export default function ErrorModal({dispatch, modal, onModalClose}, {I18n}) { + return ( + <div className={'modal fade ' + ((modal.type == 'error') ? 'in' : '')} id='ErrorModal'> + <div className='modal-container'> + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <h4 className='modal-title'>{I18n.time_tables.edit.error_modal.title}</h4> + </div> + <div className='modal-body'> + <div className='mt-md mb-md'> + <p>{actions.errorModalMessage(modal.modalProps.error)}</p> + </div> + </div> + <div className='modal-footer'> + <button + className='btn btn-link' + data-dismiss='modal' + type='button' + onClick={() => { onModalClose() }} + > + {I18n.back} + </button> + </div> + </div> + </div> + </div> + </div> + ) +} + +ErrorModal.propTypes = { + modal: PropTypes.object.isRequired, + onModalClose: PropTypes.func.isRequired +} + +ErrorModal.contextTypes = { + I18n: PropTypes.object +}
\ No newline at end of file diff --git a/app/javascript/time_tables/components/ExceptionsInDay.js b/app/javascript/time_tables/components/ExceptionsInDay.js new file mode 100644 index 000000000..3335ee89d --- /dev/null +++ b/app/javascript/time_tables/components/ExceptionsInDay.js @@ -0,0 +1,71 @@ +import React, { PropTypes, Component } from 'react' +import actions from '../actions' + +export default class ExceptionsInDay extends Component { + constructor(props) { + super(props) + } + + handleClick() { + const {index, day, metas: {day_types} } = this.props + if (day.in_periods && day_types[day.wday]) { + day.excluded_date ? this.props.onRemoveExcludedDate(index, day_types, day.date) : this.props.onAddExcludedDate(index, day_types, day.date) + } else { + day.include_date ? this.props.onRemoveIncludedDate(index, day_types, day.date) : this.props.onAddIncludedDate(index, day_types, day.date) + } + } + + render() { + {/* display add or remove link, only if true in daytypes */} + {/* display add or remove link, according to context (presence in period, or not) */} + if(this.props.value.current_month[this.props.index].in_periods == true && this.props.blueDaytype == true) { + return ( + <div className='td'> + <button + type='button' + className={'btn btn-circle' + (this.props.value.current_month[this.props.index].excluded_date ? ' active' : '')} + data-actiontype='remove' + onClick={(e) => { + $(e.currentTarget).toggleClass('active') + this.handleClick() + }} + > + <span className='fa fa-times'></span> + </button> + </div> + ) + } else { + return ( + <div className='td'> + <button + type='button' + className={'btn btn-circle' + (this.props.value.current_month[this.props.index].include_date ? ' active' : '')} + data-actiontype='add' + onClick={(e) => { + $(e.currentTarget).toggleClass('active') + this.handleClick() + }} + > + <span className='fa fa-plus'></span> + </button> + </div> + ) + // } else if(this.props.value.current_month[this.props.index].in_periods == true && this.props.blueDaytype == false){ + // return ( + // <div className='td'></div> + // ) + // } else{ + // return false + // } + } + } +} + +ExceptionsInDay.propTypes = { + value: PropTypes.object.isRequired, + metas: PropTypes.object.isRequired, + blueDaytype: PropTypes.bool.isRequired, + onExcludeDateFromPeriod: PropTypes.func.isRequired, + onIncludeDateInPeriod: PropTypes.func.isRequired, + index: PropTypes.number.isRequired +} diff --git a/app/javascript/time_tables/components/Metas.js b/app/javascript/time_tables/components/Metas.js new file mode 100644 index 000000000..7098d2b82 --- /dev/null +++ b/app/javascript/time_tables/components/Metas.js @@ -0,0 +1,139 @@ +import React, { PropTypes } from 'react' +import actions from '../actions' +import TagsSelect2 from './TagsSelect2' + +export default function Metas({metas, onUpdateDayTypes, onUpdateComment, onUpdateColor, onSelect2Tags, onUnselect2Tags}, {I18n}) { + let colorList = ["", "#9B9B9B", "#FFA070", "#C67300", "#7F551B", "#41CCE3", "#09B09C", "#3655D7", "#6321A0", "#E796C6", "#DD2DAA"] + return ( + <div className='form-horizontal'> + <div className="row"> + <div className="col-lg-10 col-lg-offset-1"> + {/* comment (name) */} + <div className="form-group"> + <label htmlFor="" className="control-label col-sm-4 required"> + {I18n.time_tables.edit.metas.name} <abbr title="">*</abbr> + </label> + <div className="col-sm-8"> + <input + type='text' + className='form-control' + value={metas.comment} + required='required' + onChange={(e) => (onUpdateComment(e.currentTarget.value))} + /> + </div> + </div> + + {/* color */} + <div className="form-group"> + <label htmlFor="" className="control-label col-sm-4">{I18n.activerecord.attributes.time_table.color}</label> + <div className="col-sm-8"> + <div className="dropdown color_selector"> + <button + type='button' + className="btn btn-default dropdown-toggle" + id='dpdwn_color' + data-toggle='dropdown' + aria-haspopup='true' + aria-expanded='true' + > + <span + className='fa fa-circle mr-xs' + style={{color: (metas.color == '') ? 'transparent' : metas.color}} + ></span> + <span className='caret'></span> + </button> + + <div className="form-group dropdown-menu" aria-labelledby='dpdwn_color'> + {colorList.map((c, i) => + <span + className="radio" + key={i} + onClick={() => {onUpdateColor(c)}} + > + <label htmlFor=""> + <input + type='radio' + className='color_selector' + value={c} + /> + <span + className='fa fa-circle' + style={{color: ((c == '') ? 'transparent' : c)}} + ></span> + </label> + </span> + )} + </div> + </div> + </div> + </div> + + {/* tags */} + <div className="form-group"> + <label htmlFor="" className="control-label col-sm-4">{I18n.activerecord.attributes.time_table.tag_list}</label> + <div className="col-sm-8"> + <TagsSelect2 + initialTags={metas.initial_tags} + tags={metas.tags} + onSelect2Tags={(e) => onSelect2Tags(e)} + onUnselect2Tags={(e) => onUnselect2Tags(e)} + /> + </div> + </div> + + {/* calendar */} + <div className="form-group"> + <label htmlFor="" className="control-label col-sm-4">{I18n.activerecord.attributes.time_table.calendar}</label> + <div className="col-sm-8"> + <span>{metas.calendar ? metas.calendar.name : I18n.time_tables.edit.metas.no_calendar}</span> + </div> + </div> + + {/* day_types */} + <div className="form-group"> + <label htmlFor="" className="control-label col-sm-4"> + {I18n.time_tables.edit.metas.day_types} + </label> + <div className="col-sm-8"> + <div className="form-group labelled-checkbox-group"> + {metas.day_types.map((day, i) => + <div + className='lcbx-group-item' + data-wday={'day_' + i} + key={i} + > + <div className="checkbox"> + <label> + <input + onChange={(e) => {onUpdateDayTypes(i, metas.day_types)}} + id={i} + type="checkbox" + checked={day ? 'checked' : ''} + /> + <span className='lcbx-group-item-label'>{actions.weekDays()[i]}</span> + </label> + </div> + </div> + )} + </div> + </div> + </div> + </div> + </div> + </div> + ) +} + +Metas.propTypes = { + metas: PropTypes.object.isRequired, + onUpdateDayTypes: PropTypes.func.isRequired, + onUpdateColor: PropTypes.func.isRequired, + onUpdateColor: PropTypes.func.isRequired, + onSelect2Tags: PropTypes.func.isRequired, + onUnselect2Tags: PropTypes.func.isRequired +} + +Metas.contextTypes = { + I18n: PropTypes.object +} diff --git a/app/javascript/time_tables/components/Navigate.js b/app/javascript/time_tables/components/Navigate.js new file mode 100644 index 000000000..7307d819b --- /dev/null +++ b/app/javascript/time_tables/components/Navigate.js @@ -0,0 +1,88 @@ +import React, { PropTypes, Component } from 'react' +import map from 'lodash/map' +import actions from '../actions' + +export default function Navigate({ dispatch, metas, timetable, pagination, status, filters}) { + if(status.isFetching == true) { + return false + } + if(status.fetchSuccess == true) { + let pageIndex = pagination.periode_range.indexOf(pagination.currentPage) + let firstPage = pageIndex == 0 + let lastPage = pageIndex == pagination.periode_range.length - 1 + return ( + <div className="pagination pull-right"> + <form className='form-inline' onSubmit={e => {e.preventDefault()}}> + {/* date selector */} + <div className="form-group"> + <div className="dropdown month_selector" style={{display: 'inline-block'}}> + <div + className='btn btn-default dropdown-toggle' + id='date_selector' + data-toggle='dropdown' + aria-haspopup='true' + aria-expanded='true' + > + {pagination.currentPage ? (actions.monthName(pagination.currentPage) + ' ' + new Date(pagination.currentPage).getFullYear()) : ''} + <span className='caret'></span> + </div> + <ul + className='dropdown-menu' + aria-labelledby='date_selector' + > + {map(pagination.periode_range, (month, i) => ( + <li key={i}> + <button + type='button' + value={month} + onClick={e => { + e.preventDefault() + dispatch(actions.checkConfirmModal(e, actions.changePage(dispatch, e.currentTarget.value), pagination.stateChanged, dispatch, metas, timetable)) + }} + > + {actions.monthName(month) + ' ' + new Date(month).getFullYear()} + </button> + </li> + ))} + </ul> + </div> + </div> + + {/* prev/next */} + <div className="form-group"> + <div className="page_links"> + <button + onClick={e => { + e.preventDefault() + dispatch(actions.checkConfirmModal(e, actions.goToPreviousPage(dispatch, pagination), pagination.stateChanged, dispatch, metas, timetable)) + }} + type='button' + data-target='#ConfirmModal' + className={(firstPage ? 'disabled ' : '') + 'previous_page'} + disabled={(firstPage ? 'disabled' : '')} + ></button> + <button + onClick={e => { + e.preventDefault() + dispatch(actions.checkConfirmModal(e, actions.goToNextPage(dispatch, pagination), pagination.stateChanged, dispatch, metas, timetable)) + }} + type='button' + data-target='#ConfirmModal' + className={(lastPage ? 'disabled ' : '') + 'next_page'} + disabled={(lastPage ? 'disabled' : '')} + ></button> + </div> + </div> + </form> + </div> + ) + } else { + return false + } +} + +Navigate.propTypes = { + status: PropTypes.object.isRequired, + pagination: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired +}
\ No newline at end of file diff --git a/app/javascript/time_tables/components/PeriodForm.js b/app/javascript/time_tables/components/PeriodForm.js new file mode 100644 index 000000000..d9f1d3437 --- /dev/null +++ b/app/javascript/time_tables/components/PeriodForm.js @@ -0,0 +1,148 @@ +import React, { PropTypes } from 'react' +import filter from 'lodash/filter' +let monthsArray = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'] + +const formatNumber = (val) => { + return ("0" + val).slice(-2) +} + +const makeDaysOptions = (daySelected) => { + let arr = [] + for(let i = 1; i < 32; i++) { + arr.push(<option value={formatNumber(i)} key={i}>{formatNumber(i)}</option>) + } + return arr +} + +const makeMonthsOptions = (monthSelected) => { + let arr = [] + for(let i = 1; i < 13; i++) { + arr.push(<option value={formatNumber(i)} key={i}>{monthsArray[i - 1]}</option>) + } + return arr +} + +const makeYearsOptions = (yearSelected) => { + let arr = [] + let startYear = new Date().getFullYear() - 3 + for(let i = startYear; i <= startYear + 6; i++) { + arr.push(<option key={i}>{i}</option>) + } + return arr +} + +export default function PeriodForm({modal, timetable, metas, onOpenAddPeriodForm, onClosePeriodForm, onUpdatePeriodForm, onValidatePeriodForm}, {I18n}) { + return ( + <div className="container-fluid"> + <div className="row"> + <div className="col lg-6 col-lg-offset-3"> + <div className='subform'> + {modal.modalProps.active && + <div> + <div className="nested-head"> + <div className="wrapper"> + <div> + <div className="form-group"> + <label htmlFor="" className="control-label required"> + {I18n.time_tables.edit.period_form.begin} + <abbr title="requis">*</abbr> + </label> + </div> + </div> + <div> + <div className="form-group"> + <label htmlFor="" className="control-label required"> + {I18n.time_tables.edit.period_form.end} + <abbr title="requis">*</abbr> + </label> + </div> + </div> + </div> + </div> + <div className="nested-fields"> + <div className="wrapper"> + <div> + <div className={'form-group date ' + (modal.modalProps.error ? ' has-error' : '')}> + <div className="form-inline"> + <select value={formatNumber(modal.modalProps.begin.day)} onChange={(e) => onUpdatePeriodForm(e, 'begin', 'day', modal.modalProps)} id="q_validity_period_begin_gteq_3i" className="date required form-control"> + {makeDaysOptions(modal.modalProps.begin.day)} + </select> + <select value={formatNumber(modal.modalProps.begin.month)} onChange={(e) => onUpdatePeriodForm(e, 'begin', 'month', modal.modalProps)} id="q_validity_period_begin_gteq_2i" className="date required form-control"> + {makeMonthsOptions(modal.modalProps.begin.month)} + </select> + <select value={modal.modalProps.begin.year} onChange={(e) => onUpdatePeriodForm(e, 'begin', 'year', modal.modalProps)} id="q_validity_period_begin_gteq_1i" className="date required form-control"> + {makeYearsOptions(modal.modalProps.begin.year)} + </select> + </div> + </div> + </div> + <div> + <div className={'form-group date ' + (modal.modalProps.error ? ' has-error' : '')}> + <div className="form-inline"> + <select value={formatNumber(modal.modalProps.end.day)} onChange={(e) => onUpdatePeriodForm(e, 'end', 'day', modal.modalProps)} id="q_validity_period_end_gteq_3i" className="date required form-control"> + {makeDaysOptions(modal.modalProps.end.day)} + </select> + <select value={formatNumber(modal.modalProps.end.month)} onChange={(e) => onUpdatePeriodForm(e, 'end', 'month', modal.modalProps)} id="q_validity_period_end_gteq_2i" className="date required form-control"> + {makeMonthsOptions(modal.modalProps.end.month)} + </select> + <select value={modal.modalProps.end.year} onChange={(e) => onUpdatePeriodForm(e, 'end', 'year', modal.modalProps)} id="q_validity_period_end_gteq_1i" className="date required form-control"> + {makeYearsOptions(modal.modalProps.end.year)} + </select> + </div> + </div> + </div> + </div> + </div> + + <div className='links nested-linker'> + <span className='help-block small text-danger pull-left mt-xs ml-sm'> + {modal.modalProps.error} + </span> + <button + type='button' + className='btn btn-link' + onClick={onClosePeriodForm} + > + {I18n.cancel} + </button> + <button + type='button' + className='btn btn-outline-primary mr-sm' + onClick={() => onValidatePeriodForm(modal.modalProps, timetable.time_table_periods, metas, filter(timetable.time_table_dates, ['in_out', true]))} + > + {I18n.actions.submit} + </button> + </div> + </div> + } + {!modal.modalProps.active && + <div className="text-right"> + <button + type='button' + className='btn btn-outline-primary' + onClick={onOpenAddPeriodForm} + > + {I18n.time_tables.actions.add_period} + </button> + </div> + } + </div> + </div> + </div> + </div> + ) +} + +PeriodForm.propTypes = { + modal: PropTypes.object.isRequired, + metas: PropTypes.object.isRequired, + onOpenAddPeriodForm: PropTypes.func.isRequired, + onClosePeriodForm: PropTypes.func.isRequired, + onUpdatePeriodForm: PropTypes.func.isRequired, + onValidatePeriodForm: PropTypes.func.isRequired, + timetable: PropTypes.object.isRequired +} + +PeriodForm.contextTypes = { + I18n: PropTypes.object +}
\ No newline at end of file diff --git a/app/javascript/time_tables/components/PeriodManager.js b/app/javascript/time_tables/components/PeriodManager.js new file mode 100644 index 000000000..9922ce2c4 --- /dev/null +++ b/app/javascript/time_tables/components/PeriodManager.js @@ -0,0 +1,85 @@ +import React, { PropTypes, Component } from 'react' +import actions from '../actions' + +export default class PeriodManager extends Component { + constructor(props, context) { + super(props, context) + } + + toEndPeriod(curr, end) { + let diff + + let startCurrM = curr.split('-')[1] + let endPeriodM = end.split('-')[1] + + let lastDayInM = new Date(curr.split('-')[2], startCurrM + 1, 0) + lastDayInM = lastDayInM.toJSON().substr(0, 10).split('-')[2] + + if(startCurrM === endPeriodM) { + diff = (end.split('-')[2] - curr.split('-')[2]) + } else { + diff = (lastDayInM - curr.split('-')[2]) + } + + return diff + } + + render() { + return ( + <div + className='period_manager' + id={this.props.value.id} + data-toendperiod={this.toEndPeriod(this.props.currentDate.toJSON().substr(0, 10), this.props.value.period_end)} + > + <p className='strong'> + {actions.getLocaleDate(this.props.value.period_start) + ' > ' + actions.getLocaleDate(this.props.value.period_end)} + </p> + + <div className='dropdown'> + <div + className='btn dropdown-toggle' + id='period_actions' + data-toggle='dropdown' + aria-haspopup='true' + aria-expanded='true' + > + <span className='fa fa-cog'></span> + </div> + <ul + className='dropdown-menu' + aria-labelledby='date_selector' + > + <li> + <button + type='button' + onClick={() => this.props.onOpenEditPeriodForm(this.props.value, this.props.index)} + > + Modifier + </button> + </li> + <li className='delete-action'> + <button + type='button' + onClick={() => this.props.onDeletePeriod(this.props.index, this.props.metas.day_types)} + > + <span className='fa fa-trash'></span> + Supprimer + </button> + </li> + </ul> + </div> + </div> + ) + } +} + +PeriodManager.propTypes = { + value: PropTypes.object.isRequired, + currentDate: PropTypes.object.isRequired, + onDeletePeriod: PropTypes.func.isRequired, + onOpenEditPeriodForm: PropTypes.func.isRequired +} + +PeriodManager.contextTypes = { + I18n: PropTypes.object +}
\ No newline at end of file diff --git a/app/javascript/time_tables/components/PeriodsInDay.js b/app/javascript/time_tables/components/PeriodsInDay.js new file mode 100644 index 000000000..888537579 --- /dev/null +++ b/app/javascript/time_tables/components/PeriodsInDay.js @@ -0,0 +1,75 @@ +import React, { PropTypes, Component } from 'react' +import PeriodManager from './PeriodManager' + +export default class PeriodsInDay extends Component { + constructor(props) { + super(props) + } + + isIn(date) { + let currentDate = date.getTime() + let cls = 'td' + let periods = this.props.value + + periods.map((p, i) => { + if (!p.deleted){ + let begin = new Date(p.period_start).getTime() + let end = new Date(p.period_end).getTime() + + if(currentDate >= begin && currentDate <= end) { + if(currentDate == begin) { + cls += ' in_periods start_period' + } else if(currentDate == end) { + cls += ' in_periods end_period' + } else { + cls += ' in_periods' + } + } + } + }) + return cls + } + + render() { + return ( + <div + className={this.isIn(this.props.currentDate) + (this.props.metas.day_types[this.props.day.wday] || !this.props.day.in_periods ? '' : ' out_from_daytypes')} + > + {this.props.value.map((p, i) => { + if(!p.deleted){ + let begin = new Date(p.period_start).getTime() + let end = new Date(p.period_end).getTime() + let d = this.props.currentDate.getTime() + + if(d >= begin && d <= end) { + if(d == begin || (this.props.currentDate.getUTCDate() == 1)) { + return ( + <PeriodManager + key={i} + index={i} + value={p} + metas={this.props.metas} + currentDate={this.props.currentDate} + onDeletePeriod={this.props.onDeletePeriod} + onOpenEditPeriodForm={this.props.onOpenEditPeriodForm} + /> + ) + } else { + return false + } + } + }else{ + return false + } + })} + </div> + ) + } +} + +PeriodsInDay.propTypes = { + value: PropTypes.array.isRequired, + currentDate: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + onDeletePeriod: PropTypes.func.isRequired +} diff --git a/app/javascript/time_tables/components/SaveTimetable.js b/app/javascript/time_tables/components/SaveTimetable.js new file mode 100644 index 000000000..d5a57bd1c --- /dev/null +++ b/app/javascript/time_tables/components/SaveTimetable.js @@ -0,0 +1,41 @@ +import React, { PropTypes, Component } from 'react' +import actions from '../actions' + +export default class SaveTimetable extends Component{ + constructor(props){ + super(props) + } + + render() { + const error = actions.errorModalKey(this.props.timetable.time_table_periods, this.props.metas.day_types) + + return ( + <div className='row mt-md'> + <div className='col-lg-12 text-right'> + <form className='time_tables formSubmitr ml-xs' onSubmit={e => {e.preventDefault()}}> + <button + className='btn btn-default' + type='button' + onClick={e => { + e.preventDefault() + if (error) { + this.props.onShowErrorModal(error) + } else { + actions.submitTimetable(this.props.getDispatch(), this.props.timetable, this.props.metas) + } + }} + > + Valider + </button> + </form> + </div> + </div> + ) + } +} + +SaveTimetable.propTypes = { + timetable: PropTypes.object.isRequired, + status: PropTypes.object.isRequired, + metas: PropTypes.object.isRequired +}
\ No newline at end of file diff --git a/app/javascript/time_tables/components/TagsSelect2.js b/app/javascript/time_tables/components/TagsSelect2.js new file mode 100644 index 000000000..70a748a04 --- /dev/null +++ b/app/javascript/time_tables/components/TagsSelect2.js @@ -0,0 +1,80 @@ +import React, { PropTypes, Component } from 'react' +import mapKeys from 'lodash/mapKeys' +import map from 'lodash/map' +import filter from 'lodash/filter' +import assign from 'lodash/assign' +import Select2 from 'react-select2' + +// get JSON full path +let origin = window.location.origin +let path = window.location.pathname.split('/', 4).join('/') + +export default class TagsSelect2 extends Component { + constructor(props, context) { + super(props, context) + } + + mapKeys(array){ + return array.map((item) => + mapKeys(item, (v, k) => + ((k == 'name') ? 'text' : k) + ) + ) + } + + render() { + return ( + <Select2 + value={(this.props.tags.length) ? map(this.props.tags, 'id') : undefined} + data={(this.props.initialTags.length) ? this.mapKeys(this.props.initialTags) : undefined} + onSelect={(e) => this.props.onSelect2Tags(e)} + onUnselect={(e) => setTimeout( () => this.props.onUnselect2Tags(e, 150))} + multiple={true} + ref='tags_id' + options={{ + tags:true, + createTag: function(params) { + return {name: params.term, text: params.term, id: params.term} + }, + allowClear: true, + theme: 'bootstrap', + width: '100%', + placeholder: this.context.I18n.time_tables.edit.select2.tag.placeholder, + ajax: { + url: origin + path + '/tags.json', + dataType: 'json', + delay: '500', + data: function(params) { + return { + tag: params.term, + }; + }, + processResults: function(data, params) { + let items = filter(data, ({name}) => name.includes(params.term) ) + return { + results: items.map( + item => assign( + {}, + item, + {text: item.name} + ) + ) + }; + }, + cache: true + }, + minimumInputLength: 1, + templateResult: formatRepo + }} + /> + ) + } +} + +const formatRepo = (props) => { + if(props.name) return props.name +} + +TagsSelect2.contextTypes = { + I18n: PropTypes.object +}
\ No newline at end of file diff --git a/app/javascript/time_tables/components/TimeTableDay.js b/app/javascript/time_tables/components/TimeTableDay.js new file mode 100644 index 000000000..165c7b848 --- /dev/null +++ b/app/javascript/time_tables/components/TimeTableDay.js @@ -0,0 +1,31 @@ +import React, { PropTypes, Component } from 'react' + +export default class TimeTableDay extends Component { + constructor(props) { + super(props) + } + + render() { + return ( + <span + className={'day' + (this.props.value.wday == 0 ? ' last_wday' : '')} + data-wday={'S' + this.props.value.wnumber} + > + <span className='dayname'> + {((this.props.value.day).charAt(0) == 'm') ? (this.props.value.day).substr(0, 2) : (this.props.value.day).charAt(0)} + </span> + <span + className={'daynumber' + (((this.props.value.in_periods && this.props.dayTypeActive && !this.props.value.excluded_date) || (this.props.value.include_date)) ? ' included' : '')} + > + {this.props.value.mday} + </span> + </span> + ) + } +} + +TimeTableDay.propTypes = { + value: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + dayTypeActive: PropTypes.bool.isRequired +} diff --git a/app/javascript/time_tables/components/Timetable.js b/app/javascript/time_tables/components/Timetable.js new file mode 100644 index 000000000..df6e6016b --- /dev/null +++ b/app/javascript/time_tables/components/Timetable.js @@ -0,0 +1,115 @@ +import React, { PropTypes, Component } from 'react' +import actions from '../actions' +import TimeTableDay from './TimeTableDay' +import PeriodsInDay from './PeriodsInDay' +import ExceptionsInDay from './ExceptionsInDay' + + +export default class Timetable extends Component { + constructor(props, context){ + super(props, context) + } + + currentDate(mFirstday, day) { + let currentMonth = mFirstday.split('-') + let twodigitsDay = day < 10 ? ('0' + day) : day + let currentDate = new Date(currentMonth[0] + '-' + currentMonth[1] + '-' + twodigitsDay) + + return currentDate + } + + render() { + if(this.props.status.isFetching == true) { + return ( + <div className="isLoading" style={{marginTop: 80, marginBottom: 80}}> + <div className="loader"></div> + </div> + ) + } else { + return ( + <div className="table table-2entries mb-sm"> + <div className="t2e-head w20"> + <div className="th"> + <div className="strong">{this.context.I18n.time_tables.synthesis}</div> + </div> + <div className="td"><span>{this.context.I18n.time_tables.edit.day_types}</span></div> + <div className="td"><span>{this.context.I18n.time_tables.edit.periods}</span></div> + <div className="td"><span>{this.context.I18n.time_tables.edit.exceptions}</span></div> + </div> + <div className="t2e-item-list w80"> + <div> + <div className="t2e-item"> + <div className="th"> + <div className="strong monthName"> + {actions.monthName(this.props.timetable.current_periode_range)} + </div> + + <div className='monthDays'> + {this.props.timetable.current_month.map((d, i) => + <TimeTableDay + key={i} + index={i} + value={d} + dayTypeActive={this.props.metas.day_types[d.wday]} + /> + )} + </div> + </div> + + {this.props.timetable.current_month.map((d, i) => + <div + key={i} + className={'td-group'+ (d.wday == 0 ? ' last_wday' : '')} + > + {/* day_types */} + <div className={"td" + (this.props.metas.day_types[d.wday] || !d.in_periods ? '' : ' out_from_daytypes') }></div> + + {/* periods */} + <PeriodsInDay + day={d} + index={i} + value={this.props.timetable.time_table_periods} + currentDate={this.currentDate(this.props.timetable.current_periode_range, d.mday)} + onDeletePeriod={this.props.onDeletePeriod} + onOpenEditPeriodForm={this.props.onOpenEditPeriodForm} + metas={this.props.metas} + /> + + {/* exceptions */} + <ExceptionsInDay + day={d} + index={i} + value={this.props.timetable} + currentDate={d.date} + metas={this.props.metas} + blueDaytype={this.props.metas.day_types[d.wday]} + onAddIncludedDate={this.props.onAddIncludedDate} + onRemoveIncludedDate={this.props.onRemoveIncludedDate} + onAddExcludedDate={this.props.onAddExcludedDate} + onRemoveExcludedDate={this.props.onRemoveExcludedDate} + onExcludeDateFromPeriod={this.props.onExcludeDateFromPeriod} + onIncludeDateInPeriod={this.props.onIncludeDateInPeriod} + /> + </div> + )} + </div> + </div> + </div> + </div> + ) + } + } +} + +Timetable.propTypes = { + metas: PropTypes.object.isRequired, + timetable: PropTypes.object.isRequired, + status: PropTypes.object.isRequired, + onDeletePeriod: PropTypes.func.isRequired, + onExcludeDateFromPeriod: PropTypes.func.isRequired, + onIncludeDateInPeriod: PropTypes.func.isRequired +} + +Timetable.contextTypes = { + I18n: PropTypes.object +} diff --git a/app/javascript/time_tables/containers/App.js b/app/javascript/time_tables/containers/App.js new file mode 100644 index 000000000..235dccb50 --- /dev/null +++ b/app/javascript/time_tables/containers/App.js @@ -0,0 +1,55 @@ +import React, { PropTypes, Component } from 'react' +import { connect } from'react-redux' +import actions from '../actions' +import Metas from './Metas' +import Timetable from './Timetable' +import Navigate from './Navigate' +import PeriodForm from './PeriodForm' +import SaveTimetable from './SaveTimetable' +import ConfirmModal from './ConfirmModal' +import ErrorModal from './ErrorModal' +import clone from '../../helpers/clone' +const I18n = clone(window, "I18n", true) + +class App extends Component { + componentDidMount(){ + this.props.onLoadFirstPage() + } + + getChildContext() { + return { I18n } + } + + render(){ + return( + <div className='row'> + <div className="col-lg-8 col-lg-offset-2 col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1"> + <Metas /> + <Navigate /> + <Timetable /> + <PeriodForm /> + <SaveTimetable /> + <ConfirmModal /> + <ErrorModal /> + </div> + </div> + ) + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onLoadFirstPage: () =>{ + dispatch(actions.fetchingApi()) + actions.fetchTimeTables(dispatch) + } + } +} + +App.childContextTypes = { + I18n: PropTypes.object +} + +const timeTableApp = connect(null, mapDispatchToProps)(App) + +export default timeTableApp diff --git a/app/javascript/time_tables/containers/ConfirmModal.js b/app/javascript/time_tables/containers/ConfirmModal.js new file mode 100644 index 000000000..f3742b038 --- /dev/null +++ b/app/javascript/time_tables/containers/ConfirmModal.js @@ -0,0 +1,31 @@ +import { connect } from 'react-redux' +import actions from '../actions' +import ConfirmModal from '../components/ConfirmModal' + +const mapStateToProps = (state) => { + return { + modal: state.modal, + timetable: state.timetable, + metas: state.metas + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onModalAccept: (next, timetable, metas) =>{ + dispatch(actions.fetchingApi()) + actions.submitTimetable(dispatch, timetable, metas, next) + }, + onModalCancel: (next) =>{ + dispatch(actions.fetchingApi()) + dispatch(next) + }, + onModalClose: () =>{ + dispatch(actions.closeModal()) + } + } +} + +const ConfirmModalContainer = connect(mapStateToProps, mapDispatchToProps)(ConfirmModal) + +export default ConfirmModalContainer diff --git a/app/javascript/time_tables/containers/ErrorModal.js b/app/javascript/time_tables/containers/ErrorModal.js new file mode 100644 index 000000000..37099073b --- /dev/null +++ b/app/javascript/time_tables/containers/ErrorModal.js @@ -0,0 +1,22 @@ +import { connect } from 'react-redux' +import actions from '../actions' +import ErrorModal from '../components/ErrorModal' + +const mapStateToProps = (state) => { + return { + modal: state.modal + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onModalClose: () =>{ + dispatch(actions.closeModal()) + dispatch(actions.resetModalErrors()) + } + } +} + +const ErrorModalContainer = connect(mapStateToProps, mapDispatchToProps)(ErrorModal) + +export default ErrorModalContainer diff --git a/app/javascript/time_tables/containers/Metas.js b/app/javascript/time_tables/containers/Metas.js new file mode 100644 index 000000000..ebccf556e --- /dev/null +++ b/app/javascript/time_tables/containers/Metas.js @@ -0,0 +1,38 @@ +import { connect } from 'react-redux' +import actions from '../actions' +import MetasComponent from '../components/Metas' + +const mapStateToProps = (state) => { + return { + metas: state.metas + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onUpdateDayTypes: (index, dayTypes) => { + let newDayTypes = dayTypes.slice(0) + newDayTypes[index] = !newDayTypes[index] + dispatch(actions.updateDayTypes(newDayTypes)) + dispatch(actions.updateCurrentMonthFromDaytypes(newDayTypes)) + }, + onUpdateComment: (comment) => { + dispatch(actions.updateComment(comment)) + }, + onUpdateColor: (color) => { + dispatch(actions.updateColor(color)) + }, + onSelect2Tags: (e) => { + e.preventDefault() + dispatch(actions.select2Tags(e.params.data)) + }, + onUnselect2Tags: (e) => { + e.preventDefault() + dispatch(actions.unselect2Tags(e.params.data)) + } + } +} + +const Metas = connect(mapStateToProps, mapDispatchToProps)(MetasComponent) + +export default Metas diff --git a/app/javascript/time_tables/containers/Navigate.js b/app/javascript/time_tables/containers/Navigate.js new file mode 100644 index 000000000..8d163659c --- /dev/null +++ b/app/javascript/time_tables/containers/Navigate.js @@ -0,0 +1,18 @@ +import React from 'react' +import { connect } from 'react-redux' +import actions from '../actions' +import NavigateComponent from '../components/Navigate' + +const mapStateToProps = (state) => { + return { + metas: state.metas, + timetable: state.timetable, + status: state.status, + pagination: state.pagination + } +} + + +const Navigate = connect(mapStateToProps)(NavigateComponent) + +export default Navigate diff --git a/app/javascript/time_tables/containers/PeriodForm.js b/app/javascript/time_tables/containers/PeriodForm.js new file mode 100644 index 000000000..1bde039e2 --- /dev/null +++ b/app/javascript/time_tables/containers/PeriodForm.js @@ -0,0 +1,46 @@ +import { connect } from 'react-redux' +import assign from 'lodash/assign' +import actions from '../actions' +import PeriodFormComponent from '../components/PeriodForm' + + + +const mapStateToProps = (state) => { + return { + modal: state.modal, + timetable: state.timetable, + metas: state.metas, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onOpenAddPeriodForm: () => { + dispatch(actions.openAddPeriodForm()) + }, + onClosePeriodForm: () => { + dispatch(actions.closePeriodForm()) + }, + onUpdatePeriodForm: (e, group, selectType, modalProps) => { + dispatch(actions.updatePeriodForm(e.currentTarget.value, group, selectType)) + let mProps = assign({}, modalProps) + mProps[group][selectType] = e.currentTarget.value + let val = window.correctDay([parseInt(mProps[group]['day']), parseInt(mProps[group]['month']), parseInt(mProps[group]['year'])]) + val = (val < 10) ? '0' + String(val) : String(val) + dispatch(actions.updatePeriodForm(val, group, 'day')) + }, + onValidatePeriodForm: (modalProps, timeTablePeriods, metas, timetableInDates) => { + let period_start = actions.formatDate(modalProps.begin) + let period_end = actions.formatDate(modalProps.end) + let error = '' + if (new Date(period_end) <= new Date(period_start)) error = 'La date de départ doit être antérieure à la date de fin' + if (error == '') error = actions.checkErrorsInPeriods(period_start, period_end, modalProps.index, timeTablePeriods) + if (error == '') error = actions.checkErrorsInDates(period_start, period_end, timetableInDates) + dispatch(actions.validatePeriodForm(modalProps, timeTablePeriods, metas, timetableInDates, error)) + } + } +} + +const PeriodForm = connect(mapStateToProps, mapDispatchToProps)(PeriodFormComponent) + +export default PeriodForm diff --git a/app/javascript/time_tables/containers/SaveTimetable.js b/app/javascript/time_tables/containers/SaveTimetable.js new file mode 100644 index 000000000..7574dc5cc --- /dev/null +++ b/app/javascript/time_tables/containers/SaveTimetable.js @@ -0,0 +1,25 @@ +import { connect } from 'react-redux' +import actions from '../actions' +import SaveTimetableComponent from '../components/SaveTimetable' + +const mapStateToProps = (state) => { + return { + timetable: state.timetable, + metas: state.metas, + status: state.status + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onShowErrorModal: (errorKey) => { + dispatch(actions.showErrorModal(errorKey)) + }, + getDispatch: () => { + return dispatch + } + } +} +const SaveTimetable = connect(mapStateToProps, mapDispatchToProps)(SaveTimetableComponent) + +export default SaveTimetable diff --git a/app/javascript/time_tables/containers/Timetable.js b/app/javascript/time_tables/containers/Timetable.js new file mode 100644 index 000000000..e78e8840a --- /dev/null +++ b/app/javascript/time_tables/containers/Timetable.js @@ -0,0 +1,44 @@ +import { connect } from 'react-redux' +import actions from '../actions' +import TimetableComponent from '../components/Timetable' + +const mapStateToProps = (state) => { + return { + metas: state.metas, + timetable: state.timetable, + status: state.status + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onDeletePeriod: (index, dayTypes) =>{ + dispatch(actions.deletePeriod(index, dayTypes)) + }, + onAddIncludedDate: (index, dayTypes, date) => { + dispatch(actions.addIncludedDate(index, dayTypes, date)) + }, + onRemoveIncludedDate: (index, dayTypes, date) => { + dispatch(actions.removeIncludedDate(index, dayTypes, date)) + }, + onAddExcludedDate: (index, dayTypes, date) => { + dispatch(actions.addExcludedDate(index, dayTypes, date)) + }, + onRemoveExcludedDate: (index, dayTypes, date) => { + dispatch(actions.removeExcludedDate(index, dayTypes, date)) + }, + onExcludeDateFromPeriod: (index, dayTypes, date) => { + dispatch(actions.excludeDateFromPeriod(index, dayTypes, date)) + }, + onIncludeDateInPeriod: (index, dayTypes, date) => { + dispatch(actions.includeDateInPeriod(index, dayTypes, date)) + }, + onOpenEditPeriodForm: (period, index) => { + dispatch(actions.openEditPeriodForm(period, index)) + } + } +} + +const Timetable = connect(mapStateToProps, mapDispatchToProps)(TimetableComponent) + +export default Timetable diff --git a/app/javascript/time_tables/reducers/index.js b/app/javascript/time_tables/reducers/index.js new file mode 100644 index 000000000..aed9035b5 --- /dev/null +++ b/app/javascript/time_tables/reducers/index.js @@ -0,0 +1,16 @@ +import { combineReducers } from 'redux' +import status from './status' +import pagination from './pagination' +import modal from './modal' +import timetable from './timetable' +import metas from './metas' + +const timeTablesApp = combineReducers({ + timetable, + metas, + status, + pagination, + modal +}) + +export default timeTablesApp diff --git a/app/javascript/time_tables/reducers/metas.js b/app/javascript/time_tables/reducers/metas.js new file mode 100644 index 000000000..51e1ec149 --- /dev/null +++ b/app/javascript/time_tables/reducers/metas.js @@ -0,0 +1,41 @@ +import assign from 'lodash/assign' +import filter from 'lodash/filter' +import actions from '../actions' + +export default function metas(state = {}, action) { + switch (action.type) { + case 'RECEIVE_TIME_TABLES': + return assign({}, state, { + comment: action.json.comment, + day_types: actions.strToArrayDayTypes(action.json.day_types), + tags: action.json.tags, + initial_tags: action.json.tags, + color: action.json.color, + calendar: action.json.calendar ? action.json.calendar : null + }) + case 'RECEIVE_MONTH': + let dt = (typeof state.day_types === 'string') ? actions.strToArrayDayTypes(state.day_types) : state.day_types + return assign({}, state, {day_types: dt}) + case 'ADD_INCLUDED_DATE': + case 'REMOVE_INCLUDED_DATE': + case 'ADD_EXCLUDED_DATE': + case 'REMOVE_EXCLUDED_DATE': + case 'DELETE_PERIOD': + case 'VALIDATE_PERIOD_FORM': + return assign({}, state, {calendar: null}) + case 'UPDATE_DAY_TYPES': + return assign({}, state, {day_types: action.dayTypes, calendar : null}) + case 'UPDATE_COMMENT': + return assign({}, state, {comment: action.comment}) + case 'UPDATE_COLOR': + return assign({}, state, {color: action.color}) + case 'UPDATE_SELECT_TAG': + let tags = [...state.tags] + tags.push(action.selectedItem) + return assign({}, state, {tags: tags}) + case 'UPDATE_UNSELECT_TAG': + return assign({}, state, {tags: filter(state.tags, (t) => (t.id != action.selectedItem.id))}) + default: + return state + } +}
\ No newline at end of file diff --git a/app/javascript/time_tables/reducers/modal.js b/app/javascript/time_tables/reducers/modal.js new file mode 100644 index 000000000..5e870a6ef --- /dev/null +++ b/app/javascript/time_tables/reducers/modal.js @@ -0,0 +1,64 @@ +import assign from 'lodash/assign' +import actions from '../actions' + +let newModalProps = {} +let emptyDate = { + day: '01', + month: '01', + year: String(new Date().getFullYear()) +} +let period_start = '', period_end = '' + +export default function modal(state = {}, action) { + switch (action.type) { + case 'OPEN_CONFIRM_MODAL': + $('#ConfirmModal').modal('show') + return assign({}, state, { + type: 'confirm', + confirmModal: { + callback: action.callback, + } + }) + case 'OPEN_ERROR_MODAL': + $('#ErrorModal').modal('show') + newModalProps = assign({}, state.modalProps, {error: action.error}) + return assign({}, state, {type: 'error'}, {modalProps: newModalProps}) + case 'RESET_MODAL_ERRORS': + newModalProps = assign({}, state.modalProps, {error: ''}) + return assign({}, state, {type: ''}, {modalProps: newModalProps}) + case 'CLOSE_PERIOD_FORM': + newModalProps = assign({}, state.modalProps, {active: false, error: ""}) + return assign({}, state, {modalProps: newModalProps}) + case 'OPEN_EDIT_PERIOD_FORM': + period_start = action.period.period_start.split('-') + period_end = action.period.period_end.split('-') + newModalProps = JSON.parse(JSON.stringify(state.modalProps)) + + newModalProps.begin.year = period_start[0] + newModalProps.begin.month = period_start[1] + newModalProps.begin.day = period_start[2] + + newModalProps.end.year = period_end[0] + newModalProps.end.month = period_end[1] + newModalProps.end.day = period_end[2] + + newModalProps.active = true + newModalProps.index = action.index + newModalProps.error = '' + return assign({}, state, {modalProps: newModalProps}) + case 'OPEN_ADD_PERIOD_FORM': + newModalProps = assign({}, state.modalProps, {active: true, begin: emptyDate, end: emptyDate, index: false, error: ''}) + return assign({}, state, {modalProps: newModalProps}) + case 'UPDATE_PERIOD_FORM': + newModalProps = JSON.parse(JSON.stringify(state.modalProps)) + newModalProps[action.group][action.selectType] = action.val + return assign({}, state, {modalProps: newModalProps}) + case 'VALIDATE_PERIOD_FORM': + newModalProps = JSON.parse(JSON.stringify(state.modalProps)) + newModalProps.error = action.error + newModalProps.active = (newModalProps.error == '') ? false : true + return assign({}, state, {modalProps: newModalProps}) + default: + return state + } +}
\ No newline at end of file diff --git a/app/javascript/time_tables/reducers/pagination.js b/app/javascript/time_tables/reducers/pagination.js new file mode 100644 index 000000000..53a753356 --- /dev/null +++ b/app/javascript/time_tables/reducers/pagination.js @@ -0,0 +1,44 @@ +import assign from 'lodash/assign' + +export default function pagination(state = {}, action) { + switch (action.type) { + case 'RECEIVE_TIME_TABLES': + return assign({}, state, { + currentPage: action.json.current_periode_range, + periode_range: action.json.periode_range, + stateChanged: false + }) + case 'RECEIVE_MONTH': + case 'RECEIVE_ERRORS': + return assign({}, state, {stateChanged: false}) + case 'GO_TO_PREVIOUS_PAGE': + case 'GO_TO_NEXT_PAGE': + let nextPage = action.nextPage ? 1 : -1 + let newPage = action.pagination.periode_range[action.pagination.periode_range.indexOf(action.pagination.currentPage) + nextPage] + toggleOnConfirmModal() + return assign({}, state, {currentPage : newPage, stateChanged: false}) + case 'CHANGE_PAGE': + toggleOnConfirmModal() + return assign({}, state, {currentPage : action.page, stateChanged: false}) + case 'ADD_INCLUDED_DATE': + case 'REMOVE_INCLUDED_DATE': + case 'ADD_EXCLUDED_DATE': + case 'REMOVE_EXCLUDED_DATE': + case 'DELETE_PERIOD': + case 'VALIDATE_PERIOD_FORM': + case 'UPDATE_COMMENT': + case 'UPDATE_COLOR': + case 'UPDATE_DAY_TYPES': + case 'UPDATE_CURRENT_MONTH_FROM_DAYTYPES': + toggleOnConfirmModal('modal') + return assign({}, state, {stateChanged: true}) + default: + return state + } +} + +const toggleOnConfirmModal = (arg = '') =>{ + $('.confirm').each(function(){ + $(this).data('toggle','') + }) +}
\ No newline at end of file diff --git a/app/javascript/time_tables/reducers/status.js b/app/javascript/time_tables/reducers/status.js new file mode 100644 index 000000000..256059191 --- /dev/null +++ b/app/javascript/time_tables/reducers/status.js @@ -0,0 +1,15 @@ +import assign from 'lodash/assign' + +export default function status(state = {}, action) { + switch (action.type) { + case 'UNAVAILABLE_SERVER': + return assign({}, state, {fetchSuccess: false}) + case 'FETCH_API': + return assign({}, state, {isFetching: true}) + case 'RECEIVE_TIME_TABLES': + case 'RECEIVE_MONTH': + return assign({}, state, {fetchSuccess: true, isFetching: false}) + default: + return state + } +}
\ No newline at end of file diff --git a/app/javascript/time_tables/reducers/timetable.js b/app/javascript/time_tables/reducers/timetable.js new file mode 100644 index 000000000..21ca1efed --- /dev/null +++ b/app/javascript/time_tables/reducers/timetable.js @@ -0,0 +1,120 @@ +import assign from 'lodash/assign' +import reject from 'lodash/reject' +import sortBy from 'lodash/sortBy' +import reduce from 'lodash/reduce' +import actions from '../actions' +let newState, newPeriods, newDates, newCM + +export default function timetable(state = {}, action) { + switch (action.type) { + case 'RECEIVE_TIME_TABLES': + let fetchedState = assign({}, state, { + current_month: action.json.current_month, + current_periode_range: action.json.current_periode_range, + periode_range: action.json.periode_range, + time_table_periods: action.json.time_table_periods, + time_table_dates: sortBy(action.json.time_table_dates, ['date']) + }) + return assign({}, fetchedState, {current_month: actions.updateSynthesis(fetchedState)}) + case 'RECEIVE_MONTH': + newState = assign({}, state, { + current_month: action.json.days + }) + return assign({}, newState, {current_month: actions.updateSynthesis(newState)}) + case 'GO_TO_PREVIOUS_PAGE': + case 'GO_TO_NEXT_PAGE': + let nextPage = action.nextPage ? 1 : -1 + let newPage = action.pagination.periode_range[action.pagination.periode_range.indexOf(action.pagination.currentPage) + nextPage] + $('#ConfirmModal').modal('hide') + actions.fetchTimeTables(action.dispatch, newPage) + return assign({}, state, {current_periode_range: newPage}) + case 'CHANGE_PAGE': + $('#ConfirmModal').modal('hide') + actions.fetchTimeTables(action.dispatch, action.page) + return assign({}, state, {current_periode_range: action.page}) + case 'DELETE_PERIOD': + newPeriods = state.time_table_periods.map((period, i) =>{ + if(i == action.index){ + period.deleted = true + } + return period + }) + let deletedPeriod = Array.of(state.time_table_periods[action.index]) + newDates = reject(state.time_table_dates, d => actions.isInPeriod(deletedPeriod, d.date) && !d.in_out) + newState = assign({}, state, {time_table_periods : newPeriods, time_table_dates: newDates}) + return assign({}, newState, { current_month: actions.updateSynthesis(newState)}) + case 'ADD_INCLUDED_DATE': + newDates = state.time_table_dates.concat({date: action.date, in_out: true}) + newCM = state.current_month.map((d, i) => { + if (i == action.index) d.include_date = true + return d + }) + return assign({}, state, {current_month: newCM, time_table_dates: newDates}) + case 'REMOVE_INCLUDED_DATE': + newDates = reject(state.time_table_dates, ['date', action.date]) + newCM = state.current_month.map((d, i) => { + if (i == action.index) d.include_date = false + return d + }) + return assign({}, state, {current_month: newCM, time_table_dates: newDates}) + case 'ADD_EXCLUDED_DATE': + newDates = state.time_table_dates.concat({date: action.date, in_out: false}) + newCM = state.current_month.map((d, i) => { + if (i == action.index) d.excluded_date = true + return d + }) + return assign({}, state, {current_month: newCM, time_table_dates: newDates}) + case 'REMOVE_EXCLUDED_DATE': + newDates = reject(state.time_table_dates, ['date', action.date]) + newCM = state.current_month.map((d, i) => { + if (i == action.index) d.excluded_date = false + return d + }) + return assign({}, state, {current_month: newCM, time_table_dates: newDates}) + case 'UPDATE_DAY_TYPES': + // We get the week days of the activated day types to reject the out_dates that that are out of newDayTypes + let weekDays = reduce(action.dayTypes, (array, dt, i) => { + if (dt) array.push(i) + return array + }, []) + + newDates = reject(state.time_table_dates, (d) => { + let weekDay = new Date(d.date).getDay() + + if (d.in_out) { + return actions.isInPeriod(state.time_table_periods, d.date) && weekDays.includes(weekDay) + } else { + return !weekDays.includes(weekDay) + } + }) + return assign({}, state, {time_table_dates: newDates}) + case 'UPDATE_CURRENT_MONTH_FROM_DAYTYPES': + return assign({}, state, {current_month: actions.updateSynthesis(state)}) + case 'VALIDATE_PERIOD_FORM': + if (action.error != '') return state + + let period_start = actions.formatDate(action.modalProps.begin) + let period_end = actions.formatDate(action.modalProps.end) + + let newPeriods = JSON.parse(JSON.stringify(action.timeTablePeriods)) + + if (action.modalProps.index !== false){ + let updatedPeriod = newPeriods[action.modalProps.index] + updatedPeriod.period_start = period_start + updatedPeriod.period_end = period_end + newDates = reject(state.time_table_dates, d => actions.isInPeriod(newPeriods, d.date) && !d.in_out) + }else{ + let newPeriod = { + period_start: period_start, + period_end: period_end + } + newPeriods.push(newPeriod) + } + + newDates = newDates || state.time_table_dates + newState =assign({}, state, {time_table_periods: newPeriods, time_table_dates: newDates}) + return assign({}, newState, {current_month: actions.updateSynthesis(newState)}) + default: + return state + } +}
\ No newline at end of file |
