Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add/operations menu #73

Merged
merged 3 commits into from
Feb 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/api/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,11 @@ const rootSchema = `
userAgreeTerms(userId: String!): User
reassignCampaignContacts(organizationId:String!, campaignIdsContactIds:[CampaignIdContactId]!, newTexterUserId:String!):[CampaignIdAssignmentId],
megaReassignCampaignContacts(organizationId:String!, campaignIdsContactIds:[CampaignIdContactId]!, newTexterUserIds:[String]!):[CampaignIdAssignmentId],
markForSecondPass(organizationId: String!, campaignIdsContactIds: [CampaignIdContactId]!): [CampaignContact]
bulkReassignCampaignContacts(organizationId:String!, campaignsFilter:CampaignsFilter, assignmentsFilter:AssignmentsFilter, contactsFilter:ContactsFilter, newTexterUserId:String!):[CampaignIdAssignmentId]
megaBulkReassignCampaignContacts(organizationId:String!, campaignsFilter:CampaignsFilter, assignmentsFilter:AssignmentsFilter, contactsFilter:ContactsFilter, newTexterUserIds:[String]!):[CampaignIdAssignmentId]
requestTexts(count: Int!, email: String!): String!
releaseUnsentMessages(campaignId: String!): String!
markForSecondPass(campaignId: String!): String!
}

schema {
Expand Down
136 changes: 116 additions & 20 deletions src/containers/CampaignList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import wrapMutations from './hoc/wrap-mutations'
import Empty from '../components/Empty'
import LoadingIndicator from '../components/LoadingIndicator'
import { dataTest } from '../lib/attributes'
import Dialog from 'material-ui/Dialog'
import IconMenu from 'material-ui/IconMenu'
import MenuItem from 'material-ui/MenuItem'
import FlatButton from 'material-ui/FlatButton'
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert';

const campaignInfoFragment = `
id
Expand Down Expand Up @@ -43,7 +48,52 @@ const inlineStyles = {
}
}

const operations = {
releaseUnsentMessages: {
title: campaign => `Release Unsent Messages for ${campaign.title}`,
body: () => `Releasing unsent messages for this campaign will cause unsent messages in this campaign\
from texter's assignments. This means that these texters will no longer be able to send\
these messages, but these messages will become available to assign via the autoassignment\
functionality.`
},
markForSecondPass: {
title: campaign => `Mark Unresponded to Messages in ${campaign.title} for a Second Pass`,
body: () => `Marking unresponded to messages for this campaign will reset the state of messages that have\
not been responded to by the contact, causing them to show up as needing a first text, as long as the campaign\
is not past due. After running this operation, the texts will still be assigned to the same texter, so please\
run 'Release Unsent Messages' after if you'd like these second pass messages to be available for auto-assignment.`
}
}

class CampaignList extends React.Component {
state ={
inProgress: undefined,
error: undefined,
executing: false,
finished: undefined
}

start = (operation, campaign) => () => this.setState({ inProgress: [operation, campaign] })
clearInProgress = () => this.setState({
inProgress: undefined,
error: undefined,
executing: false,
finished: undefined
})

executeOperation = () => {
this.setState({ executing: true })
const [operationName, campaign] = this.state.inProgress

this.props.mutations[operationName](campaign.id)
.then(resp => {
this.setState({finished: resp.data[operationName], executing: false })
})
.catch(error => {
this.setState({ error, executing: false })
})
}

renderRow(campaign) {
const {
isStarted,
Expand Down Expand Up @@ -109,32 +159,20 @@ class CampaignList extends React.Component {
style={listItemStyle}
key={campaign.id}
primaryText={primaryText}
onTouchTap={() => (!isStarted ?
onClick={() => (!isStarted ?
this.props.router.push(`${campaignUrl}/edit`) :
this.props.router.push(campaignUrl))}
secondaryText={secondaryText}
leftIcon={leftIcon}
rightIconButton={adminPerms ?
(campaign.isArchived ? (
<IconButton
tooltip='Unarchive'
onTouchTap={async () => this.props.mutations.unarchiveCampaign(campaign.id)}
>
<UnarchiveIcon />
</IconButton>
) : (
<IconButton
tooltip='Archive'
onTouchTap={async () => this.props.mutations.archiveCampaign(campaign.id)}
>
<ArchiveIcon />
</IconButton>
)) : null}
rightIconButton={adminPerms && this.renderMenu(campaign)}
/>
)
}

render() {
const { inProgress, error, finished, executing } = this.state
console.log(this.state)

if (this.props.data.loading) {
return <LoadingIndicator />
}
Expand All @@ -145,11 +183,57 @@ class CampaignList extends React.Component {
icon={<SpeakerNotesIcon />}
/>
) : (
<List>
{campaigns.campaigns.map((campaign) => this.renderRow(campaign))}
</List>
<div>
{inProgress &&
<Dialog
title={operations[inProgress[0]].title(inProgress[1])}
onRequestClose={this.clearInProgress} open={true}
actions={ finished
? [<FlatButton label="Done" primary={true} onClick={this.clearInProgress} />]
: [<FlatButton
label="Cancel"
primary={true}
disabled={executing}
onClick={this.clearInProgress}
/>,
<FlatButton
label="Execute Operation"
primary={true}
onClick={this.executeOperation}
/>]
}
>
{executing
? <LoadingIndicator />
: error
? <span style={{color: 'red'}}> {JSON.stringify(error)} </span>
: finished
? finished
: operations[inProgress[0]].body(inProgress[1])
}
</Dialog>
}
<List>
{campaigns.campaigns.map((campaign) => this.renderRow(campaign))}
</List>
</div>
)
}

renderMenu(campaign) {
return (
<IconMenu
iconButtonElement={<IconButton onClick={console.log}><MoreVertIcon /></IconButton>}
onClick={console.log}
>
<MenuItem primaryText="Release Unsent Messages" onClick={this.start('releaseUnsentMessages', campaign)} />
<MenuItem primaryText="Mark for a Second Pass" onClick={this.start('markForSecondPass', campaign)} />
{!campaign.isArchived && <MenuItem primaryText="Archive Campaign" leftIcon={<ArchiveIcon />} onClick={() => this.props.mutations.archiveCampaign(campaign.id)} />}
{campaign.isArchived && <MenuItem primaryText="Unarchive Campaign" leftIcon={<UnarchiveIcon />} onClick={() => this.props.mutations.unarchiveCampaign(campaign.id)} />}

</IconMenu>
)
}
}

CampaignList.propTypes = {
Expand Down Expand Up @@ -183,6 +267,18 @@ const mapMutationsToProps = () => ({
}
}`,
variables: { campaignId }
}),
releaseUnsentMessages: (campaignId) => ({
mutation: gql`mutation releaseUnsentMessages($campaignId: String!) {
releaseUnsentMessages(campaignId: $campaignId)
}`,
variables: { campaignId }
}),
markForSecondPass: (campaignId) => ({
mutation: gql`mutation markForSecondPass($campaignId: String!) {
markForSecondPass(campaignId: $campaignId)
}`,
variables: { campaignId }
})
})

Expand Down
65 changes: 41 additions & 24 deletions src/server/api/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -1316,37 +1316,44 @@ const rootMutations = {
},
markForSecondPass: async (
_ignore,
{ organizationId, campaignIdsContactIds },
{ campaignId },
{ user }
) => {
// verify permissions
await accessRequired(user, organizationId, "SUPERVOLUNTEER", true);
const organizationId = (await r.knex('campaign')
.where({ id: parseInt(campaignId) }))[0].organization_id

let affectedCampaignContactIds = [];
const groupedByCampaign = _.groupBy(
campaignIdsContactIds,
c => c.campaignId
);
await accessRequired(user, organizationId, "ADMIN", true);

await Promise.all(
Object.keys(groupedByCampaign).map(async campaignId => {
const campaignContactIds = groupedByCampaign[campaignId].map(
c => c.campaignContactId
);
affectedCampaignContactIds = affectedCampaignContactIds.concat(
campaignContactIds
);
return r
.knex("campaign_contact")
.update({ message_status: "needsMessage" })
.where({ campaign_id: campaignId })
.whereIn("id", campaignContactIds);
/*
"Mark Campaign for Second Pass", will only mark contacts for a second
pass that do not have a more recently created membership in another campaign.
*/
const skippingCells = await r.knex('campaign_contact')
.select('cell')
.where({
campaign_id: parseInt(campaignId),
message_status: 'messaged'
})
);
.whereRaw(`
campaign_contact.cell in (
select cell
from campaign_contact as other_campaign_contact
where other_campaign_contact.created_at > campaign_contact.created_at
)
`)

return affectedCampaignContactIds.map(id => {
id;
});
const updateResult = await r.knex('campaign_contact')
.update({ message_status: 'needsMessage' })
.where({
campaign_id: parseInt(campaignId),
message_status: 'messaged'
})
.whereNotIn('cell', skippingCells.map(cc => cc.cell))

return `Marked ${updateResult} campaign contacts for a second pass.\
Did not mark ${skippingCells.length} contacts because they were\
present in another, more recent campaign.`
},
reassignCampaignContacts: async (
_,
Expand Down Expand Up @@ -1541,6 +1548,16 @@ const rootMutations = {
} catch (e) {
return e.response.body.message;
}
},
releaseUnsentMessages: async (_, { campaignId }, { user }) => {
const updatedCount = await r.knex('campaign_contact').where({
campaign_id: parseInt(campaignId),
message_status: 'needsMessage'
}).update({
assignment_id: null
})

return `Released ${updatedCount} unsent messages for reassignment`;
}
}
};
Expand Down