Home Reference Source

application/components/assignment/assignment-calendar.js

  1. import React from 'react';
  2. import {browserHistory, Link} from 'react-router';
  3. import {timeToFormat} from './../../util/strings';
  4. import {connect} from 'react-redux';
  5. import moment from 'moment';
  6. import _ from 'lodash';
  7. import Immutable from 'immutable';
  8.  
  9. import HTML5Backend from 'react-dnd-html5-backend'
  10. import { DragDropContext } from 'react-dnd'
  11.  
  12. import CurrentUser from './../../current-user';
  13.  
  14. import BigCalendar from 'react-big-calendar';
  15. import withDragAndDrop from './addons/dragAndDrop';
  16.  
  17. import Paper from 'material-ui/Paper';
  18. import Dialog from 'material-ui/Dialog';
  19. import DatePicker from 'material-ui/DatePicker';
  20. import FlatButton from 'material-ui/FlatButton';
  21. import SelectField from 'material-ui/SelectField';
  22. import MenuItem from 'material-ui/MenuItem';
  23. import FontIcon from 'material-ui/FontIcon';
  24. import Subheader from 'material-ui/Subheader';
  25. import {RadioButton, RadioButtonGroup} from 'material-ui/RadioButton';
  26.  
  27. import {Row, Col} from './../flexbox';
  28. import ceoTheme from './../../theme';
  29.  
  30. import {IssueModal} from './../issue';
  31.  
  32. import {
  33. assignmentsUpdate,
  34. assignmentsCheckOut,
  35. assignmentsCheckIn,
  36. assignmentsFetch,
  37. assignmentsFetchOne
  38. } from './../../redux/actions/assignment-actions';
  39.  
  40. import {
  41. issuesFetch,
  42. issueUpdate
  43. } from './../../redux/actions/issue-actions';
  44.  
  45. import {
  46. snackbarShowMessage
  47. } from './../../redux/actions/snackbar-actions';
  48.  
  49. import {
  50. assignmentViewUpdate
  51. } from './../../redux/actions/assignment-view-actions';
  52.  
  53. import AssignmentManager from './../../managers/assignment-manager';
  54.  
  55. import request from './../../util/request';
  56. import LoadingIndicator from '../common/loading-indicator';
  57.  
  58. const localizer = BigCalendar.momentLocalizer(moment)
  59. const DragAndDropCalendar = withDragAndDrop(BigCalendar);
  60.  
  61. function getUrlParameter(name) {
  62. name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
  63. var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
  64. var results = regex.exec(location.search);
  65. return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
  66. };
  67.  
  68. function Event({ event }) {
  69. let colors = {
  70. 'photo': '#DA8AE5',
  71. 'article': '#01BCD4',
  72. 'issue': 'green',
  73. 'misc': 'orange'
  74. }
  75.  
  76. const lock = <FontIcon style={{color:'white', verticalAlign:'middle', fontSize:'20px', marginLeft:'-2px'}} className='mui-icons'>lock</FontIcon>;
  77. const lockBlack = <FontIcon style={{color:'black', verticalAlign:'middle', fontSize:'16px', marginLeft:'-2px', marginRight:'1px'}} className='mui-icons'>lock</FontIcon>;
  78.  
  79. let color = colors['misc'];
  80. if (colors[event.type]) {
  81. color = colors[event.type];
  82. }
  83.  
  84. if (event.type == 'issue') {
  85. return (
  86. <div style={{
  87. backgroundColor: color,
  88. padding: '2px 5px',
  89. }}>
  90. {event.lock ? lock : ''}<strong style={{verticalAlign:'middle'}}>{event.title ? event.title : (moment(event.start).format('MMMM Do') + ' Issue')}</strong>
  91. </div>
  92. )
  93. }
  94.  
  95. if (!event.unscheduled) {
  96. return (
  97. <div style={{
  98. padding: '2px 5px',
  99. color: 'black'
  100. }}>
  101. {event.lock ? lockBlack : <span style={{width:'10px', height:'10px', backgroundColor:color, display:'inline-block', marginRight:'3px'}}></span>}
  102. <span style={{verticalAlign:'middle'}}>{event.title ? event.title : event.slug}</span>
  103. </div>
  104. )
  105. }
  106.  
  107. return null;
  108. }
  109.  
  110. function CustomToolbar(toolbar) {
  111. const goToBack = () => {
  112. toolbar.date.setMonth(toolbar.date.getMonth() - 1);
  113. toolbar.onNavigate('prev');
  114. };
  115.  
  116. const goToNext = () => {
  117. toolbar.date.setMonth(toolbar.date.getMonth() + 1);
  118. toolbar.onNavigate('next');
  119. };
  120.  
  121. const goToCurrent = () => {
  122. const now = new Date();
  123. toolbar.date.setMonth(now.getMonth());
  124. toolbar.date.setYear(now.getFullYear());
  125. toolbar.onNavigate('current');
  126. };
  127.  
  128. const label = () => {
  129. const date = moment(toolbar.date);
  130. return (
  131. <h3 style={{margin:'0px'}}>{date.format('MMMM')} {date.format('YYYY')}</h3>
  132. );
  133. };
  134.  
  135. let calView = window.Store.getState().assignmentView.calview;
  136.  
  137. const swapView = () => {
  138. if (window.Store.getState().assignmentView.calview == 'issue') {
  139. window.Store.dispatch(assignmentViewUpdate({
  140. 'calview': 'assignment'
  141. }))
  142. .then(() => AssignmentManager.fetchFromView());
  143. } else if (window.Store.getState().assignmentView.calview == 'assignment') {
  144. window.Store.dispatch(assignmentViewUpdate({
  145. 'calview': 'issue'
  146. }))
  147. .then(() => AssignmentManager.fetchFromView());
  148. }
  149.  
  150. }
  151.  
  152. return (
  153. <div>
  154. <Row className='rbc-toolbar'>
  155. <Col xs={4}>
  156. <FlatButton
  157. label="BACK"
  158. labelPosition="after"
  159. onClick={goToBack}
  160. icon={<FontIcon className='mui-icons'>keyboard_arrow_left</FontIcon>}
  161. />
  162. <FlatButton
  163. label="TODAY"
  164. onClick={goToCurrent}
  165. />
  166. <FlatButton
  167. label="NEXT"
  168. labelPosition="before"
  169. onClick={goToNext}
  170. icon={<FontIcon className='mui-icons'>keyboard_arrow_right</FontIcon>}
  171. />
  172. </Col>
  173. <Col xs={4} style={{textAlign:'center'}}>
  174. {label()}
  175. </Col>
  176. <Col xs={4} style={{textAlign:'right'}}>
  177. <span style={{fontSize:'14px', verticalAlign:'middle'}}>SORT BY: </span>
  178. <FlatButton
  179. label="ISSUE"
  180. onClick={swapView}
  181. disabled={calView == 'issue'}
  182. icon={calView == 'issue'
  183. ? (<FontIcon className='mui-icons'>check_circle</FontIcon>)
  184. : ''
  185. }
  186. />
  187. <FlatButton
  188. label="DUE DATE"
  189. onClick={swapView}
  190. disabled={calView == 'assignment'}
  191. icon={calView == 'assignment'
  192. ? (<FontIcon className='mui-icons'>check_circle</FontIcon>)
  193. : ''
  194. }
  195. />
  196. </Col>
  197. </Row>
  198. </div>
  199. );
  200. };
  201.  
  202. class UnwrappedAssignmentCalendar extends React.Component {
  203. constructor(props) {
  204. super(props);
  205.  
  206. this.state = {
  207. eventSetterOpen: false,
  208. eventSetterDate: null,
  209. eventSetterAssignment: null,
  210. eventSetterIssue: null,
  211. multiIssueDrop: [],
  212. multiIssueSetterOpen: false,
  213. multiIssueSetterId: null,
  214. multiIssueSetterAssignment: null,
  215. issueModalOpen: false,
  216. issueModalDate: null,
  217. unassigned: Immutable.fromJS([])
  218. }
  219.  
  220. this.handleMove = this.handleMove.bind(this);
  221. this.handleNavigate = this.handleNavigate.bind(this);
  222. this.handleClose = this.handleClose.bind(this);
  223. this.handleSetterSave = this.handleSetterSave.bind(this);
  224. this.handleAssignmentChange = this.handleAssignmentChange.bind(this);
  225. this.handleDateChange = this.handleDateChange.bind(this);
  226. this.handleMultiIssueSave = this.handleMultiIssueSave.bind(this);
  227. this.handleMultiIssueChange = this.handleMultiIssueChange.bind(this);
  228. this.handleIssueModalClose = this.handleIssueModalClose.bind(this);
  229.  
  230. this.fetchUpdate = this.fetchUpdate.bind(this);
  231. }
  232.  
  233. componentWillMount() {
  234. if (this.props.assignmentView.useDate) {
  235. this.fetchUpdate();
  236. } else {
  237. this.props.dispatch(assignmentViewUpdate({
  238. useDate: moment()
  239. }))
  240. .then(() => this.fetchUpdate());
  241. }
  242. }
  243.  
  244. fetchUpdate() {
  245. const {dispatch} = this.props;
  246. dispatch(issuesFetch({
  247. 'start': this.props.assignmentView.useDate.startOf('month').format('YYYY-MM-DD'),
  248. 'end': this.props.assignmentView.useDate.endOf('month').format('YYYY-MM-DD')
  249. }))
  250. .then(() => {
  251. dispatch(assignmentsFetch({'type': 'unscheduled'}))
  252. .then(() => {
  253. this.setState({unassigned: this.props.assignments.items});
  254. })
  255. .then(() => {
  256. if (this.props.assignmentView.calview == 'issue') {
  257. let issues = [];
  258. this.props.issues.items.map((issue) => {
  259. issues.push(issue.get('uuid'));
  260. });
  261.  
  262. dispatch(assignmentViewUpdate({
  263. issues: issues,
  264. type: 'any'
  265. }))
  266. .then(() => AssignmentManager.fetchFromView());
  267. } else {
  268. dispatch(assignmentViewUpdate({
  269. start: this.props.assignmentView.useDate.startOf('month').format('YYYY-MM-DD'),
  270. end: this.props.assignmentView.useDate.endOf('month').format('YYYY-MM-DD'),
  271. issues: [],
  272. type: 'any'
  273. }))
  274. .then(() => AssignmentManager.fetchFromView());
  275. }
  276. });
  277. });
  278. }
  279.  
  280. // shouldComponentUpdate(nextProps, nextState) {
  281. // if (this.props.assignments != nextProps.assignments) {
  282. // return true;
  283. // }
  284.  
  285. // if (this.props.issueDueDates != nextProps.dueDates) {
  286. // return true;
  287. // }
  288.  
  289. // if (this.state.eventSetterOpen != nextState.eventSetterOpen) {
  290. // return true;
  291. // }
  292.  
  293. // return false;
  294. // }
  295.  
  296. handleMove({ event, start, end }) {
  297. let useEvent = this.props.assignments.items.find(item => item.get('uuid') == event.uuid);
  298.  
  299. if (this.props.assignmentView.calview == 'issue') {
  300. const day = moment(start).format('MMDDYYYY');
  301.  
  302. let issues = this.props.issues.items.filter((issue) => {
  303. if (moment(issue.get('published_at')).format('MMDDYYYY') == day) {
  304. return true;
  305. }
  306. return false;
  307. });
  308.  
  309. if (issues.size == 1) {
  310. let issue = this.props.issues.items.find((item) => moment(item.get('published_at')).format('MMDDYYYY') == day);
  311. this.handleAssignmentSave(useEvent, event.start, event.end, issue.get('id'));
  312. } else if (issues.size > 1){
  313. this.setState({multiIssueDrop: issues, multiIssueSetterOpen: true, multiIssueSetterAssignment: event.uuid});
  314. }
  315. } else if (this.props.assignmentView.calview == 'assignment') {
  316. if (!event.lock && event.start !== start) {
  317. this.handleAssignmentSave(useEvent, start, end);
  318. event.unscheduled = false;
  319. }
  320. }
  321. }
  322.  
  323. handleEventClick(event) {
  324. if (event.type == 'issue') {
  325. this.setState({eventSetterOpen: true, eventSetterIssue: event});
  326. } else {
  327. browserHistory.push('/ceo/redirect?next=assignment/' + event.uuid);
  328. }
  329. }
  330.  
  331. handleDayClick(day) {
  332. if (this.props.calView == 'assignment') {
  333. this.setState({eventSetterOpen: true, eventSetterDate: day.start});
  334. } else {
  335. this.setState({issueModalOpen: true, issueModalDate: day.start});
  336. }
  337. }
  338.  
  339. handleAssignmentSave(event, start, end, issueId) {
  340. const {dispatch} = this.props;
  341. let data = {};
  342.  
  343. if (!issueId) {
  344. data.due_at = moment(start).utc();
  345. } else {
  346. data.issue = issueId;
  347. }
  348.  
  349. if (event.get('lock')) {
  350. console.log('handling the lock');
  351. } else {
  352. dispatch(assignmentsCheckOut(event.get('srn')))
  353. .then(() => {
  354. dispatch(assignmentsUpdate(event.get('uuid'), data));
  355. })
  356. .then(() => {
  357. dispatch(snackbarShowMessage('Assignment updated'));
  358. })
  359. .then(() => {
  360. this.handleCheckIn(event);
  361. })
  362. }
  363. }
  364.  
  365. handleIssueSave(event, start, end) {
  366. const {dispatch} = this.props;
  367. let data = {};
  368.  
  369. //set datetime as utc
  370. data.published_at = moment(start).utc();
  371.  
  372. dispatch(issueUpdate(event.uuid, data))
  373. .then(() => dispatch(issuesFetch()))
  374. .then(() => dispatch(snackbarShowMessage('Issue updated')));
  375. }
  376.  
  377. handleNavigate(date) {
  378. //do stuff when you navigate
  379. const {dispatch} = this.props;
  380.  
  381. dispatch(assignmentViewUpdate({
  382. useDate: moment(date)
  383. }))
  384. .then(() => this.fetchUpdate());
  385. }
  386.  
  387. handleAssignmentChange(e, i, uuid) {
  388. console.log(uuid);
  389. this.setState({eventSetterAssignment: uuid});
  390. }
  391.  
  392. handleDateChange(e, i, date) {
  393. this.setState({eventSetterDate: date})
  394. }
  395.  
  396. handleSetterSave() {
  397. let event = this.state.unassigned.filter((item) => item.get('uuid') == this.state.eventSetterAssignment).first();
  398.  
  399. let issue = null;
  400.  
  401. if (this.props.assignmentView.calview == 'issue' && this.state.eventSetterIssue) {
  402. issue = this.props.issues.items.find((item) => item.get('uuid') == this.state.eventSetterIssue.uuid);
  403. }
  404.  
  405. if (this.state.eventSetterDate) {
  406. event = event.set('start', this.state.eventSetterDate)
  407. .set('end', this.state.eventSetterDate);
  408. }
  409.  
  410. event = event.set('unscheduled', false);
  411.  
  412. if (event && event.type !== 'issue' && !issue) {
  413. this.handleAssignmentSave(event, event.start, event.end);
  414. } else if (event && issue) {
  415. this.handleAssignmentSave(event, event.start, event.end, issue.get('id'));
  416. }
  417.  
  418. this.handleClose();
  419. }
  420.  
  421. handleMultiIssueChange(e, i, id) {
  422. this.setState({multiIssueSetterId: id})
  423. };
  424.  
  425. handleMultiIssueSave() {
  426. const event = this.props.issueDueDates.find((item) => item.uuid == this.state.multiIssueSetterAssignment);
  427. const id = this.state.multiIssueSetterId;
  428.  
  429. if (event && id) {
  430. this.handleAssignmentSave(event, event.start, event.end, id);
  431. }
  432.  
  433. this.handleClose();
  434. }
  435.  
  436. handleClose() {
  437. this.setState({eventSetterOpen: false, eventSetterAssignment: null, multiIssueSetterOpen: false, multiIssueSetterId: null});
  438. }
  439.  
  440. handleCheckIn(event) {
  441. const {dispatch} = this.props;
  442.  
  443. dispatch(assignmentsFetchOne(event.get('uuid')))
  444. .then((response) => {
  445. // console.log(response);
  446. const assignment = this.props.assignments.items.find((item) => item.get('uuid') == event.get('uuid'));
  447. dispatch(assignmentsCheckIn(assignment.get('uuid'), assignment.get('lock').get('uuid')));
  448. });
  449. }
  450.  
  451. handleIssueModalClose(e) {
  452. this.setState({issueModalOpen: false, issueModalDate: null});
  453. }
  454.  
  455. render() {
  456. const formats = {
  457. dateFormat: 'D'
  458. }
  459.  
  460. let assignments = [];
  461.  
  462. if (this.props.assignmentView.calview == 'issue') {
  463. this.props.issues.items.map((issue,i) => {
  464. let event = {};
  465.  
  466. //event.allDay = true;
  467. event.title = issue.get('label') ? issue.get('label') : issue.get('slug');
  468. event.uuid = issue.get('uuid');
  469. event.type = 'issue';
  470.  
  471. if (!CurrentUser.hasRole('Administrator')) {
  472. event.lock = true;
  473. } else {
  474. event.lock = false;
  475. }
  476.  
  477. if (issue.get('published_at')) {
  478. event.start = moment.utc(issue.get('published_at')).local().toDate();
  479. event.end = moment.utc(issue.get('published_at')).local().toDate();
  480. } else {
  481. event.unscheduled = true;
  482. }
  483.  
  484. assignments.push(event);
  485.  
  486. let issueId = issue.get('id');
  487. let filtered = this.props.assignments.items.filter((assignment,i) => {
  488. if (assignment.get('issue_id') == issueId) {
  489. return true;
  490. }
  491.  
  492. return false;
  493. })
  494.  
  495. filtered.map((assignment,i) => {
  496. let event = {};
  497.  
  498. //event.allDay = true;
  499. event.title = assignment.get('title') ? assignment.get('title') : assignment.get('slug');
  500. event.type = assignment.get('type');
  501. event.slug = assignment.get('slug');
  502. event.uuid = assignment.get('uuid');
  503. event.srn = assignment.get('srn');
  504.  
  505. // check if the assignment is locked
  506. if (assignment.get('lock') && assignment.get('lock').get('user_id') !== CurrentUser.getId()) {
  507. event.lock = assignment.get('lock').get('uuid');
  508. event.lockUser = assignment.get('lock').get('user_id');
  509. }
  510.  
  511. event.start = moment.utc(issue.get('published_at')).local().toDate();
  512. event.end = moment.utc(issue.get('published_at')).local().toDate();
  513.  
  514. assignments.push(event);
  515. })
  516. });
  517. } else {
  518. this.props.assignments.items.map((item,i) => {
  519. let event = {};
  520.  
  521. //event.allDay = true;
  522. event.title = item.get('title') ? item.get('title') : item.get('slug');
  523. event.type = item.get('type');
  524. event.slug = item.get('slug');
  525. event.uuid = item.get('uuid');
  526. event.srn = item.get('srn');
  527. event.issue = item.get('issue_id') ? item.get('issue_id') : null;
  528. //need a way of differentiating viewmodes in backgroundWrapper
  529. event.flag = true;
  530.  
  531. // check if the assignment is locked
  532. if (item.get('lock') && item.get('lock').get('user_id') !== CurrentUser.getId()) {
  533. event.lock = item.get('lock').get('uuid');
  534. event.lockUser = item.get('lock').get('user_id');
  535. }
  536.  
  537. // set the event unscheduled if it has no due_at
  538. if (item.get('due_at')) {
  539. event.start = moment.utc(item.get('due_at')).local().toDate();
  540. event.end = moment.utc(item.get('due_at')).local().toDate();
  541. } else {
  542. event.unscheduled = true;
  543. }
  544.  
  545. assignments.push(event);
  546. });
  547. }
  548.  
  549. const eventSetterActions = [
  550. <FlatButton
  551. label="Cancel"
  552. primary={true}
  553. onClick={this.handleClose}
  554. />,
  555. <FlatButton
  556. label="Submit"
  557. primary={true}
  558. disabled={this.state.eventSetterAssignment ? false : true}
  559. onClick={this.handleSetterSave}
  560. />,
  561. ];
  562.  
  563. const multiIssueSetterActions = [
  564. <FlatButton
  565. label="Cancel"
  566. primary={true}
  567. onClick={this.handleClose}
  568. />,
  569. <FlatButton
  570. label="Submit"
  571. primary={true}
  572. disabled={this.state.multiIssueSetterId ? false : true}
  573. onClick={this.handleMultiIssueSave}
  574. />,
  575. ];
  576.  
  577. return (
  578. <Paper style={{height:window.innerHeight}}>
  579. {
  580. this.props.issues.isFetching || this.props.assignments.isFetching
  581. ? <LoadingIndicator centered={true} />
  582. : ''
  583. }
  584. <DragAndDropCalendar
  585. className={this.props.assignmentView.calView}
  586. selectable
  587. popup
  588. views={['month', 'agenda']}
  589. events={assignments}
  590. onNavigate={this.handleNavigate.bind(this)}
  591. onSelectEvent={this.handleEventClick.bind(this)}
  592. onSelectSlot={this.handleDayClick.bind(this)}
  593. onEventDrop={this.handleMove}
  594. formats={formats}
  595. localizer={localizer}
  596. components={{
  597. event: Event,
  598. toolbar: CustomToolbar
  599. }}
  600. />
  601. <Dialog
  602. title='Schedule an Assignment Due Date'
  603. actions={eventSetterActions}
  604. modal={true}
  605. open={this.state.eventSetterOpen}
  606. contentStyle={{width:'500px'}}
  607. >
  608. <div>
  609. {
  610. this.props.assignmentView.calview == 'issue'
  611. ? (
  612. <SelectField
  613. floatingLabelText={'Add Assignment to Issue'}
  614. onChange={this.handleAssignmentChange}
  615. value={this.state.eventSetterAssignment}
  616. >
  617. {
  618. this.state.unassigned.map((item,i) => {
  619. if (!item.get('issue_id') && !item.get('lock')) {
  620. return (
  621. <MenuItem key={i} value={item.get('uuid')} primaryText={item.get('title') ? item.get('title') : item.get('slug')} />
  622. )
  623. }
  624. })
  625. }
  626. </SelectField>
  627. )
  628. : (
  629. <div>
  630. <DatePicker
  631. floatingLabelText='Selected Due Date'
  632. container='inline'
  633. mode='landscape'
  634. value={this.state.eventSetterDate}
  635. onChange={this.handleDateChange}
  636. firstDayOfWeek={0}
  637. />
  638. <SelectField
  639. floatingLabelText='Unscheduled Assignments'
  640. onChange={this.handleAssignmentChange}
  641. value={this.state.eventSetterAssignment}
  642. >
  643. {
  644. assignments.map((event,i) => {
  645. if (event.unscheduled && !event.lock) {
  646. return (
  647. <MenuItem key={i} value={event.uuid} primaryText={event.title} />
  648. )
  649. }
  650. })
  651. }
  652. </SelectField>
  653. </div>
  654. )
  655. }
  656. </div>
  657. </Dialog>
  658. <Dialog
  659. title='Which Issue do you want this Assignment linked to?'
  660. actions={multiIssueSetterActions}
  661. modal={true}
  662. open={this.state.multiIssueSetterOpen}
  663. contentStyle={{width:'500px'}}
  664. >
  665. <SelectField
  666. floatingLabelText='Select an Issue'
  667. onChange={this.handleMultiIssueChange}
  668. value={this.state.multiIssueSetterId}
  669. >
  670. {
  671. this.state.multiIssueDrop.map((issue,i) => {
  672. return (
  673. <MenuItem key={i} value={issue.get('id')} primaryText={issue.get('label') ? issue.get('label') : (moment(issue.get('published_at')).format('MMMM Do') + ' Issue')} />
  674. )
  675. })
  676. }
  677. </SelectField>
  678. </Dialog>
  679. {
  680. this.state.issueModalDate
  681. ? (
  682. <IssueModal onClose={this.handleIssueModalClose} open={this.state.issueModalOpen} issueDate={this.state.issueModalDate} />
  683. )
  684. : ''
  685. }
  686. </Paper>
  687. );
  688. }
  689. }
  690.  
  691. const mapStateToProps = (state) => {
  692. return {
  693. assignments: state.assignments,
  694. issues: state.issues,
  695. workflows: state.workflows,
  696. assignmentView: state.assignmentView
  697. };
  698. }
  699.  
  700. const AssignmentCalendar = connect(mapStateToProps)(UnwrappedAssignmentCalendar);
  701.  
  702. export default DragDropContext(HTML5Backend)(AssignmentCalendar);