Skip to content

Commit

Permalink
added basic mission planning #33
Browse files Browse the repository at this point in the history
  • Loading branch information
maxbeier committed Apr 27, 2017
1 parent 6bc1376 commit b2b7fb9
Show file tree
Hide file tree
Showing 11 changed files with 555 additions and 18 deletions.
105 changes: 90 additions & 15 deletions client/components/Mission.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,54 @@
import React from 'react';
import moment from 'moment';
import _ from 'lodash';
import { Pill } from 'elemental';
import { Card, Table, Button, Pill } from 'elemental';
import { Map, TileLayer } from 'react-leaflet';
import MissionForm from './MissionForm';

const formatDate = date => moment(date).format(moment.localeData().longDateFormat('L'));

const statusMap = {
none: 'default',
pending: 'info',
yes: 'success',
no: 'danger',
};

export default React.createClass({

propTypes: {
mission: React.PropTypes.object.isRequired,
isEditable: React.PropTypes.bool,
onChange: React.PropTypes.func,
},

contextTypes: {
volunteers: React.PropTypes.object,
},

getDefaultProps() {
return {
mission: {},
isEditable: false,
onChange: _.noop,
};
},

getInitialState() {
// coordinates come in in wrong order, so we have to reverse them
const position = _.has(this.props.mission, 'area.location.geo')
? this.props.mission.area.location.geo.slice().reverse()
: null;

return {
position: this.props.mission.area.location.geo,
position,
zoom: 10,
isEditing: false,
};
},

componentDidMount() {

if (!this.state.position) {
// if address is missing, load if from open street map
if (!this.state.position && this.props.mission.area) {
const location = this.props.mission.area.location;
const fields = ['country', 'postcode', 'state', 'street1'];
const query = _.map(_.pick(location, fields), part => part ? part.replace(/\s/g, '+') : '').join(',+');
Expand All @@ -40,26 +61,80 @@ export default React.createClass({
}
},

render() {
onChange(mission) {
this.toggleEdit();
this.props.onChange(mission);
},

toggleEdit() {
const isEditing = !this.state.isEditing;
this.setState({ isEditing });
},

renderCrew(crew) {
if (_.isEmpty(crew)) return null;

const rows = _.map(crew, (assignment) => {
const volunteer = this.context.volunteers
? this.context.volunteers[assignment.volunteer]
: assignment.volunteer;

return (
<tr key={volunteer.id}>
<td><Pill label={assignment.status} type={statusMap[assignment.status]} /></td>
<td>{_.startCase(volunteer.group)}</td>
<td>{volunteer.name.first} {volunteer.name.last}</td>
</tr>
);
});

return (
<Table style={{ tableLayout: 'fixed' }}>
<thead>
<tr><th>Status</th><th>Group</th><th>Name</th></tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
},

renderReadView() {
const mission = this.props.mission;
const area = mission.area ? mission.area.name : '';
const position = this.state.position;
const right = { float: 'right' };

return (
<div>
<Pill label={mission.status} type="info" style={{ float: 'right' }} />
<h2>{mission.name} in {mission.area.name} from {formatDate(mission.start)} till {formatDate(mission.end)}</h2>
<div style={{ marginBottom: '1rem' }}>
{_.map(mission.crew, (member, i) =>
<Pill key={i} label={`${member.name.first} ${member.name.last}`} />
)}
</div>
<Card>
{this.props.isEditable
? <Button onClick={this.toggleEdit} style={right}>Edit</Button>
: <Pill label={mission.status} type="info" style={right} />
}
<h2>{mission.name} in {area} from {formatDate(mission.start)} till {formatDate(mission.end)}</h2>

{this.renderCrew(mission.crew)}

{position &&
<Map center={position} zoom={this.state.zoom}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</Map>
}
</div>
</Card>
);
},

renderEditView() {
return (
<Card>
<MissionForm mission={this.props.mission} onChange={this.onChange} />
</Card>
);
},

render() {
return this.state.isEditing
? this.renderEditView()
: this.renderReadView();
},

});
177 changes: 177 additions & 0 deletions client/components/MissionForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import React from 'react';
import _ from 'lodash';
import { Alert, Button, Form, FormRow, FormField, FormInput, Table, Spinner } from 'elemental';
import DateInput from './DateInput';
import VolunteerGroupSelect from './VolunteerGroupSelect';
import * as http from '../lib/http';
import formData from '../lib/formData';

export default React.createClass({

propTypes: {
mission: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func,
},

contextTypes: {
volunteers: React.PropTypes.object,
},

getDefaultProps() {
return {
volunteers: {},
onChange: _.noop,
};
},

getInitialState() {
return {
message: null,
isSubmitting: false,
mission: _.cloneDeep(this.props.mission),
};
},

onChange(event) {
const mission = this.state.mission;
const target = event.target;

if (target) {
mission[target.name] = target.value;
}
else {
mission[event.name] = event.value;
}

this.setState({ mission });
},

onSubmit(event) {
event.preventDefault();

this.setState({ isSubmitting: true });

const oldMission = this.props.mission;
const newMission = this.state.mission;
const values = this.getDiff(oldMission, newMission);

if (values.crew) {
values.crew = _.filter(values.crew, a => a.volunteer);
}

const body = formData(values);

http.put(`/api/missions/${oldMission.id}`, { body })
.then(({ mission }) => {
this.props.onChange(mission);
})
.catch(({ error }) => this.setMessage(error, 'danger'));
},

setMessage(text, type) {
this.setState({ message: { text, type } });
_.delay(() => this.setState({ message: null }), 3000);
},

getDiff(oldObject, newObject) {
return _.transform(newObject, (result, value, key) => {
if (_.isEqual(oldObject[key], value)) return;
result[key] = value;
}, {});
},

renderCrew(mission) {
const crew = mission.crew;

if (_.isEmpty(crew)) return null;

const onChange = oldVolunteerID => newVolunteerID => {
const assignment = _.find(mission.crew, a => a.volunteer === oldVolunteerID);
assignment.status = 'none';
assignment.volunteer = newVolunteerID;
this.setState({ mission });
};

const onRemove = volunteerID => () => {
_.remove(crew, n => n.volunteer === volunteerID);
this.setState({ mission });
};

const addMember = () => {
crew.push({ status: 'none', volunteer: _.uniqueId() });
this.setState({ mission });
};

return (
<Table style={{ tableLayout: 'fixed' }}>
<thead>
<tr>
<th>Status</th><th>Group</th><th>Name (Einsätze)</th>
<th style={{ paddingBottom: 0 }}>
<Button type="link" onClick={addMember}>Add Crew Member</Button>
</th>
</tr>
</thead>
<tbody>
{_.map(crew, assignment =>
<VolunteerGroupSelect
key={assignment.volunteer}
assignment={assignment}
onChange={onChange(assignment.volunteer)}
onRemove={onRemove(assignment.volunteer)}
/>
)}
</tbody>
</Table>
);
},

render() {
const mission = this.state.mission;

return (
<div>
{this.state.message &&
<Alert type={this.state.message.type}>{this.state.message.text}</Alert>
}

<Form onChange={this.onChange} onSubmit={this.onSubmit}>

<FormField label="Name">
<FormInput name="name" type="text" defaultValue={mission.name} required />
</FormField>

{/* TODO: Load areas and set area */}

<FormRow>
<FormField label="Start" width="one-half">
<DateInput
name="start"
required
defaultValue={mission.start}
onChange={value => this.onChange({ name: 'start', value })}
/>
</FormField>
<FormField label="End" width="one-half">
<DateInput
name="end"
required
defaultValue={mission.end}
onChange={value => this.onChange({ name: 'end', value })}
/>
</FormField>
</FormRow>

{this.renderCrew(mission)}

<div style={{ textAlign: 'center', marginTop: '1rem' }}>
<Button type="primary" submit style={{ padding: '0 2rem' }}>
Save Data and Notify Crew {this.state.isSubmitting && <Spinner type="inverted" />}
</Button>
</div>
</Form>
</div>
);
},

});
Loading

0 comments on commit b2b7fb9

Please sign in to comment.