Codeminer42/Punchclock

View on GitHub
app/javascript/packs/reducers/calendarReducer.js

Summary

Maintainability
D
2 days
Test Coverage
import Moment from 'moment';
import Immutable from 'immutable';
import * as Calendar from '../utils/calendar';
import { push } from 'react-router-redux';
import { fetchSheets, fetchHolidays, saveSheets, fetchCurrentAllocation } from '../api';

//calendar actions
export const INITIALIZE = 'calendar/initializeCalendar';
export const TOGGLE = 'calendar/toggle';
export const DESELECT = 'calendar/deselect';
export const SELECT_WEEK = 'calendar/selectWeek';
export const SET_TIME_SHEET = 'calendar/setTimeSheet';
export const SET_TIME_ON_SELECTEDS = 'calendar/setTimeSheetOnSelecteds';
export const ERASE = 'calendar/erase';
export const PREV = 'calendar/prev';
export const NEXT = 'calendar/next';

//server actions
export const UPDATE_SHEETS_SUCCESS = 'server/updateSheetsSucceded';
export const UPDATE_SHEETS_FAIL = 'server/updateSheetsFailed';
export const UPDATE_CURRENT_ALLOCATION_SUCCESS = 'server/updateCurrentAllocationSucceded';
export const UPDATE_CURRENT_ALLOCATION_FAIL = 'server/updateCurrentAllocationFailed';
export const UPDATE_HOLIDAYS_SUCCESS = 'server/updateHolidaysSucceded';
export const UPDATE_HOLIDAYS_FAIL = 'server/updateHolidaysFailed';
export const SAVE_SHEET_SUCCESS = 'server/saveSheetsSucceded';
export const SHEETS_SAVE_FAIL = 'server/saveSheetsFailed';

//setups
const emptyMap = Immutable.Map();
const emptySet = Immutable.Set();
const Punch = Immutable.Record({
  from: undefined,
  to: undefined,
  project_id: undefined,
  delta: 0,
});

const initialState = {
  base: null,
  start: false,
  hasNext: false,
  monthName: Immutable.List(),
  weeks: Immutable.List(),
  weekdays: Moment.weekdaysMin(),
  selecteds: emptySet,
  sheetsSaveds: emptyMap,
  holidays: [],
  sheets: emptyMap,
  deleteds: emptySet,
  changes: 0,
  currentAllocationId: '',
};

export default (state = initialState, action) => {
  switch (action.type) {
    case INITIALIZE:
      return {
        ...state,
        base: action.payload.base,
        start: action.payload.start,
        hasNext: action.payload.hasNext,
        monthName: action.payload.monthNames,
        weeks: action.payload.weeks,
      };
    case PREV:
      return {
        ...state,
        base: action.payload.base,
        start: action.payload.start,
        hasNext: action.payload.hasNext,
        monthName: action.payload.monthNames,
        weeks: action.payload.weeks,
      };
    case NEXT:
      return {
        ...state,
        base: action.payload.base,
        start: action.payload.start,
        hasNext: action.payload.hasNext,
        monthName: action.payload.monthNames,
        weeks: action.payload.weeks,
      };
    case SET_TIME_SHEET:
      return {
        ...state,
        selecteds: action.sheetsPayload.selecteds,
        sheets: action.sheetsPayload.sheets,
        deleteds: action.sheetsPayload.deleteds,
        changes: action.sheetsPayload.changes,
      };
    case ERASE:
      return {
        ...state,
        selecteds: action.sheetsPayload.selecteds,
        sheetsSaveds: action.sheetsPayload.sheetsSaveds,
        sheets: action.sheetsPayload.sheets,
        deleteds: action.sheetsPayload.deleteds,
        changes: action.sheetsPayload.changes,
      };
    case TOGGLE:
      return {
        ...state,
        selecteds: action.sheetsPayload.selecteds,
      };
    case SELECT_WEEK:
      return {
        ...state,
        selecteds: action.sheetsPayload.selecteds,
      };
    case DESELECT:
      return {
        ...state,
        selecteds: action.sheetsPayload.selecteds,
      };
    case UPDATE_SHEETS_SUCCESS:
      return {
        ...state,
        sheetsSaveds: action.sheetsPayload.sheetsSaveds,
      };
    case UPDATE_SHEETS_FAIL:
      return {
        ...state,
      };
    case UPDATE_CURRENT_ALLOCATION_SUCCESS:
      return {
        ...state,
        currentAllocationId: action.currentAllocationPayload.currentAllocation.id,
      };
    case UPDATE_CURRENT_ALLOCATION_FAIL:
      return {
        ...state,
      };
    case UPDATE_HOLIDAYS_SUCCESS:
      return {
        ...state,
        holidays: action.holidaysPayload.holidays,
      };
    case UPDATE_HOLIDAYS_FAIL:
      return {
        ...state,
      };
    case SAVE_SHEET_SUCCESS:
      return {
        ...state,
        sheetsSaveds: action.sheetsPayload.sheetsSaveds,
        sheets: action.sheetsPayload.sheets,
        deleteds: action.sheetsPayload.deleteds,
        changes: action.sheetsPayload.changes,
      };
    case SHEETS_SAVE_FAIL:
      return {
        ...state,
        sheets: emptyMap,
      };
    default:
      return state;
  }
}

//export functions
export const getDays = (weeks) => {
  return weeks.flatMap(function(w){ return w.days; });
};

export const sheetFor = (d, sheets, sheetsSaveds) => {
  let dayString = d.day.format('YYYY-MM-DD');
  return (sheets.get(dayString, null) || sheetsSaveds.get(dayString, []));
}

export const isSelected = (selecteds, day) => {
  if(selecteds.size > 0){
    return selecteds.has(day);
  }
  return false;
};

//local functions
const redefine = (base) => {
  let range = Calendar.innerRange(base);
  return({
    base: base,
    start: Calendar.startDate(base),
    hasNext: (Moment().diff(range[1], 'day') >= 1),
    monthNames: Calendar.monthNames(range),
    weeks: Calendar.weeks(Calendar.startDate(base), range),
  });
};

const createPunch = (dayString, sheet) => {
  return sheet.map((p)=> {
    let from = Moment(`${dayString} ${p.from}`);
    let to = Moment(`${dayString} ${p.to}`);
    return new Punch({
      from: from,
      to: to,
      project_id: p.project_id,
      delta: Moment.duration(to.diff(from)).asHours()
    });
  });
};

export const sumHours = (weeks, sheets, sheetsSaved, changesUnsaved) => {
  if (changesUnsaved >= 1) {
    return sheets.reduce((sumHours, day) =>
      day.reduce((sumDeltas, sheetsSelected) =>
        sumDeltas + sheetsSelected.delta,
      sumHours),
    0);
  }
  return getDays(weeks).reduce((sum, d) => {
    if(d.inner) {
      let _sheets = sheetFor(d, sheets, sheetsSaved);
      return sum + _sheets.reduce(((s, p) => s + p.delta), 0);
    } else return sum;
  }, 0);
}

//actions
export const onInitializeCalendar = (dispatch) => (date = '') => {
  dispatch({
    type: INITIALIZE,
    payload: redefine(Moment(date).utc()),
  });
};

export const onPrev = (dispatch) => (base) => {
  dispatch({
    type: PREV,
    payload: redefine(Calendar.prev(base)),
  });
  dispatch(push('/'+Calendar.prev(base).format('YYYY/MM')));
};

export const onNext = (dispatch) => (base) => {
  dispatch({
    type: NEXT,
    payload: redefine(Calendar.next(base)),
  });
  dispatch(push('/'+Calendar.next(base).format('YYYY/MM')));
};

export const onSetTimeSheet = (dispatch) => (sheet, selecteds, sheets, deleteds) => {
  let newSheets = sheets;
  let newDeleteds = deleteds;

  selecteds.forEach( (day)=> {
    let dayString = day.format('YYYY-MM-DD');
    let punch = createPunch(dayString, sheet);
    newSheets = newSheets.set(dayString, Immutable.fromJS(punch));
    newDeleteds = deleteds.delete(dayString);
  });

  dispatch({
    type: SET_TIME_SHEET,
    sheetsPayload:{
      selecteds: emptySet,
      sheets: newSheets,
      deleteds: newDeleteds,
      changes: newSheets.size + newDeleteds.size,
    },
  });
};

export const onErase = (dispatch) => (selecteds, sheets, deleteds, sheetsSaveds) => {
  let newSheets = sheets;
  let newDeleteds = deleteds;
  let newSheetsSaveds = sheetsSaveds;

  selecteds.forEach((day)=> {
    let dayString = day.format('YYYY-MM-DD');
    newSheets = newSheets.delete(dayString);
    newSheetsSaveds = newSheetsSaveds.delete(dayString);
    newDeleteds = newDeleteds.add(dayString);
  });

  dispatch({
    type: ERASE,
    sheetsPayload:{
      selecteds: emptySet,
      sheetsSaveds: newSheetsSaveds,
      sheets: newSheets,
      deleteds: newDeleteds,
      changes: newSheets.size + newDeleteds.size,
    },
  });
};

export const onToggle = (dispatch) => (day, selecteds) => {
  return dispatch((dispatch, getState) => {
    let newSelecteds = selecteds;
    if(isSelected(selecteds, day)) {
      newSelecteds = newSelecteds.delete(day);
    } else if (!Calendar.isHoliday(selecteds, day, Calendar.getHolidaysFromState(getState))) {
      newSelecteds = newSelecteds.add(day);
    }

    if (newSelecteds.size !== selecteds.size) {
      dispatch({
        type: TOGGLE,
        sheetsPayload:{
          selecteds: newSelecteds,
        }
      });
    }
  })
};

export const onSelectWeek = (dispatch) => (week, selecteds) => {
  return dispatch((dispatch, getState) => {
    let newSelecteds = selecteds;
    week.days.forEach(d => {
      let { day, inner } = d;
      const isDayWeekend = day.day() !== 0 && day.day() !== 6
      if(isDayWeekend && inner) {
        if (!Calendar.isHoliday(newSelecteds, day, Calendar.getHolidaysFromState(getState))) {
          newSelecteds = newSelecteds.add(day);
        }
      }
    });

    dispatch({
      type: SELECT_WEEK,
      sheetsPayload: {
        selecteds: newSelecteds
      },
    });
  })
};

export const onDeselect = (dispatch) => () => {
  dispatch({
    type: DESELECT,
    sheetsPayload: {
      selecteds: emptySet
    },
  });
};

export const onFetchSheets = (dispatch) => () => {
  fetchSheets().then((response) =>
    dispatch({
      type: UPDATE_SHEETS_SUCCESS,
      sheetsPayload:{
        sheetsSaveds: Immutable.Map(response.body),
      },
    })
  )
  .catch((response) => {
    dispatch({
      type: UPDATE_SHEETS_FAIL,
    })
  });
};

export const onFetchCurrentAllocation = (dispatch) => () => {
  fetchCurrentAllocation().then((response) => {
    dispatch({
      type: UPDATE_CURRENT_ALLOCATION_SUCCESS,
      currentAllocationPayload: {
        currentAllocation: response.body.currentAllocation
      },
    })
  })
  .catch((response) => {
    dispatch({
      type: UPDATE_CURRENT_ALLOCATION_FAIL,
    })
  });
}

export const onFetchHolidays = (dispatch) => () => {
  fetchHolidays().then((response) => {
    const { body } = response
    dispatch({
      type: UPDATE_HOLIDAYS_SUCCESS,
      holidaysPayload: {
        holidays: body
      },
    })
  })
  .catch((response) => {
    dispatch({
      type: UPDATE_HOLIDAYS_FAIL,
    })
  })
};

export const onSaveSheets = (dispatch) => (deleteds, sheets, sheetsSaveds) => {
  saveSheets(deleteds, sheets)
    .then(() =>
      dispatch({
        type: SAVE_SHEET_SUCCESS,
        sheetsPayload: {
          sheetsSaveds: sheetsSaveds.merge(sheets),
          sheets: emptyMap,
          deleteds: emptySet,
          changes: 0,
        },
      })
    )
    .catch((error) => {
      alert(JSON.parse(error.response.text));
      dispatch({
        type: SHEETS_SAVE_FAIL,
      });
    });
};