Skip to content

Commit

Permalink
Merge pull request #172 from impactasaurus/108
Browse files Browse the repository at this point in the history
Adding record list
  • Loading branch information
drimpact committed Mar 4, 2018
2 parents 3058bdb + b6f3a78 commit 4b6a741
Show file tree
Hide file tree
Showing 12 changed files with 448 additions and 108 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
"rc-slider": "8.2.0",
"react": "15.6.1",
"react-addons-shallow-compare": "15.6.0",
"react-apollo": "1.4.15",
"react-apollo": "1.4.16",
"react-css-modules": "4.1.0",
"react-dates": "12.5.1",
"react-dom": "15.6.1",
Expand Down
45 changes: 44 additions & 1 deletion src/app/apollo/modules/meetings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const getMeetingsGQL = gql`
}
${fragmentWithOutcomeSetAndAggregates}`;

export const getMeetings = <T>(idExtractor: IDExtractor<T>) => {
export const getMeetings = <T>(idExtractor: IDExtractor<T>, name?: string) => {
return graphql<any, T>(getMeetingsGQL, {
options: (props: T) => {
return {
Expand All @@ -47,6 +47,31 @@ export const getMeetings = <T>(idExtractor: IDExtractor<T>) => {
},
};
},
name: name ? name : 'data',
});
};

const getAllMeetingsGQL = gql`
query getAllMeetings ($beneficiaryID: String!) {
getIncompleteMeetings: incompleteMeetings(beneficiary: $beneficiaryID) {
...meetingWithOutcomeSetAndAggregates
}
getMeetings: meetings(beneficiary: $beneficiaryID) {
...meetingWithOutcomeSetAndAggregates
}
}
${fragmentWithOutcomeSetAndAggregates}`;

export const getAllMeetings = <T>(idExtractor: IDExtractor<T>, name?: string) => {
return graphql<any, T>(getAllMeetingsGQL, {
options: (props: T) => {
return {
variables: {
beneficiaryID: idExtractor(props),
},
};
},
name: name ? name : 'data',
});
};

Expand All @@ -70,6 +95,11 @@ export const newMeeting = graphql(gql`
variables: {
beneficiaryID: config.beneficiaryID,
},
}, {
query: getAllMeetingsGQL,
variables: {
beneficiaryID: config.beneficiaryID,
},
}],
}).then(mutationResultExtractor<IMeeting>('newMeeting')),
}),
Expand All @@ -95,6 +125,11 @@ export const newRemoteMeeting = graphql(gql`
variables: {
beneficiaryID: config.beneficiaryID,
},
}, {
query: getAllMeetingsGQL,
variables: {
beneficiaryID: config.beneficiaryID,
},
}],
}).then(mutationResultExtractor<{
JTI: string,
Expand Down Expand Up @@ -139,6 +174,9 @@ export function completeMeeting<T>(component) {
refetchQueries: [{
query: getMeetingsGQL,
variables: { beneficiaryID },
}, {
query: getAllMeetingsGQL,
variables: { beneficiaryID },
}],
}).then(mutationResultExtractor<IMeeting>('completeMeeting')),
}),
Expand All @@ -150,6 +188,11 @@ export interface IMeetingResult extends QueryProps {
getMeetings?: IMeeting[];
}

export interface IGetAllMeetingsResult extends QueryProps {
getIncompleteMeetings?: IMeeting[];
getMeetings?: IMeeting[];
}

export interface IMeetingMutation {
newMeeting(config: IAssessmentConfig): Promise<IMeeting>;
newRemoteMeeting(config: IAssessmentConfig, daysToComplete: number): Promise<string>;
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Header extends React.Component<IProps, any> {
if (exact) {
return this.props.currentURL === url;
}
return this.props.currentURL !== undefined && this.props.currentURL.includes(url);
return this.props.currentURL !== undefined && this.props.currentURL.startsWith(url);
}

private handleClick(url: string) {
Expand Down
98 changes: 98 additions & 0 deletions src/app/components/RecordList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as React from 'react';
import {Table, IconProps, Label, Icon, Popup} from 'semantic-ui-react';
import './style.less';
import {IMeeting, sortMeetingsByConducted} from '../../models/meeting';
import {renderArray, renderArrayForArray} from '../../helpers/react';
import {getHumanisedDate} from '../../helpers/moment';

interface IProp {
meetings: IMeeting[];
}

interface IState {
open: string[];
}

class RecordList extends React.Component<IProp, IState> {

constructor(props) {
super(props);
this.state = {
open: [],
};

this.renderRecord = this.renderRecord.bind(this);
this.toggleRecord = this.toggleRecord.bind(this);
}

private toggleRecord(r: IMeeting): () => void {
return () => {
if (this.state.open.indexOf(r.id) === -1) {
this.setState({
open: this.state.open.concat(r.id),
});
} else {
this.setState({
open: this.state.open.filter((i) => i !== r.id),
});
}
};
}

private renderTag(t: string): JSX.Element {
return (
<Label>{t}</Label>
);
}

private renderRecord(r: IMeeting, idx: number): JSX.Element[] {
let clz = 'record';
if (idx % 2 !== 0) {
clz += ' stripe';
}
const open = this.state.open.find((o) => o === r.id);
const iconProps: IconProps = {};
if (!open) {
iconProps.rotated = 'counterclockwise';
}
let incomplete = (<span/>);
if (r.incomplete) {
incomplete = (<Popup trigger={<Icon name="hourglass half"/>} content="Incomplete" /> );
}
return [
<Table.Row className={clz} key={r.id}>
<Table.Cell className="name" onClick={this.toggleRecord(r)}>
{incomplete}
<span>{getHumanisedDate(new Date(r.conducted))}</span>
</Table.Cell>
<Table.Cell>{r.outcomeSet.name}</Table.Cell>
<Table.Cell>{renderArray(this.renderTag, r.tags)}</Table.Cell>
<Table.Cell>{r.user}</Table.Cell>
<Table.Cell>Coming Soon!</Table.Cell>
</Table.Row>,
];
}

public render() {
return (
<div id="record-list">
<Table celled={true} className="record-list-table">
<Table.Header>
<Table.Row>
<Table.HeaderCell>Date</Table.HeaderCell>
<Table.HeaderCell>Questionnaire</Table.HeaderCell>
<Table.HeaderCell>Tags</Table.HeaderCell>
<Table.HeaderCell>Conducted</Table.HeaderCell>
<Table.HeaderCell>Actions</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{renderArrayForArray(this.renderRecord, sortMeetingsByConducted(this.props.meetings, false))}
</Table.Body>
</Table>
</div>
);
}
}

export { RecordList };
25 changes: 25 additions & 0 deletions src/app/components/RecordList/style.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#record-list {
.record-list-table {
tr.record.stripe {
background-color: rgba(0, 0, 50, 0.02);
}
.record-inner {
box-shadow: inset -4px 15px 10px -11px rgba(0,0,0,0.04);
background-color: #FDFDFD;

& > td {
padding: 30px;
border-bottom: 1px solid #ccc;
}

table.question-list-table {
tr th, tr td {
border-left: none;
}
th, td {
padding: 0.4em 0.6em;
}
}
}
}
}
138 changes: 138 additions & 0 deletions src/app/containers/Journey/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import * as React from 'react';
import { Loader, Message } from 'semantic-ui-react';
import {QuestionSetSelect} from 'components/QuestionSetSelect';
import {VizControlPanel} from 'components/VizControlPanel';
import {setURL} from 'modules/url';
import { bindActionCreators } from 'redux';
import {IStore} from 'redux/IStore';
import {IURLConnector} from 'redux/modules/url';
import {Aggregation, Visualisation, getAggregation, getVisualisation, getSelectedQuestionSetID} from 'models/pref';
import {getMeetings, IMeetingResult} from 'apollo/modules/meetings';
import {IMeeting} from 'models/meeting';
import {MeetingRadar} from 'components/MeetingRadar';
import {MeetingTable} from 'components/MeetingTable';
import {isBeneficiaryUser} from 'modules/user';
import {MeetingGraph} from '../../components/MeetingGraph';
const { connect } = require('react-redux');

interface IProps extends IURLConnector {
params: {
id: string,
};
vis?: Visualisation;
agg?: Aggregation;
selectedQuestionSetID?: string;
data?: IMeetingResult;
isCategoryAgPossible?: boolean;
}

function isCategoryAggregationAvailable(meetings: IMeeting[], selectedQuestionSetID: string|undefined): boolean {
if (!Array.isArray(meetings) || meetings.length === 0) {
return false;
}
const meetingsBelongingToSelectedQS = meetings.filter((m) => {
return selectedQuestionSetID !== undefined && m.outcomeSetID === selectedQuestionSetID;
});
const meetingsWithCategories = meetingsBelongingToSelectedQS.filter((m) => {
return m.outcomeSet.categories.length > 0;
});
return meetingsWithCategories.length > 0;
}

function getQuestionSetOptions(ms: IMeeting[]): string[] {
if (!Array.isArray(ms)) {
return [];
}
return ms.map((m) => m.outcomeSetID);
}

function filterMeetings(m: IMeeting[], questionSetID: string): IMeeting[] {
return m.filter((m) => m.outcomeSetID === questionSetID);
}

@connect((state: IStore, ownProps: IProps) => {
const selectedQuestionSetID = getSelectedQuestionSetID(state.pref);
const canCatAg = isCategoryAggregationAvailable(ownProps.data.getMeetings, selectedQuestionSetID);
return {
vis: getVisualisation(state.pref, true),
agg: getAggregation(state.pref, canCatAg),
isCategoryAgPossible: canCatAg,
selectedQuestionSetID,
isBeneficiary: isBeneficiaryUser(state.user),
};
}, (dispatch) => ({
setURL: bindActionCreators(setURL, dispatch),
}))
class JourneyInner extends React.Component<IProps, any> {

constructor(props) {
super(props);
this.renderVis = this.renderVis.bind(this);
this.renderJourney = this.renderJourney.bind(this);
}

private renderVis(): JSX.Element {
const { data: { getMeetings }, vis, selectedQuestionSetID, agg } = this.props;
const meetings = filterMeetings(getMeetings, selectedQuestionSetID);

if (vis === Visualisation.RADAR) {
return (
<MeetingRadar aggregation={agg} meetings={meetings} />
);
}
if (vis === Visualisation.GRAPH) {
return (
<MeetingGraph meetings={meetings} aggregation={agg}/>
);
}
return (
<MeetingTable aggregation={agg} meetings={meetings} />
);
}

private renderJourney(): JSX.Element {
if (this.props.data.loading) {
return (
<Loader active={true} inline="centered" />
);
}
if (this.props.data.error !== undefined) {
return (
<Message error={true}>
<Message.Header>Error</Message.Header>
<div>Failed to load records</div>
</Message>
);
}
if (!Array.isArray(this.props.data.getMeetings) || this.props.data.getMeetings.length === 0) {
return (
<p>No complete meetings found for beneficiary {this.props.params.id}</p>
);
}
return (
<div>
<VizControlPanel canCategoryAg={this.props.isCategoryAgPossible} allowGraph={true}/>
<QuestionSetSelect
allowedQuestionSetIDs={getQuestionSetOptions(this.props.data.getMeetings)}
autoSelectFirst={true}
/>
{this.renderVis()}
</div>
);
}

public render() {
if(this.props.params.id === undefined) {
return (<div />);
}

return (
<div id="journey">
{this.renderJourney()}
</div>
);
}
}

const Journey = getMeetings<IProps>((p) => p.params.id)(JourneyInner);
export { Journey }
Loading

0 comments on commit 4b6a741

Please sign in to comment.