diff --git a/src/extensions/service-managers/numpicker-campaign/index.js b/src/extensions/service-managers/numpicker-campaign/index.js new file mode 100644 index 000000000..5a77b0aa0 --- /dev/null +++ b/src/extensions/service-managers/numpicker-campaign/index.js @@ -0,0 +1,110 @@ +import { getFeatures } from "../../../server/api/lib/config"; +import { r, cacheableData } from "../../../server/models"; +import _ from "lodash"; + +export const name = "numpicker-campaign"; + +export const metadata = () => ({ + displayName: "Campaign Number Picker", + description: + "Allows specific numbers to be chosen for a campaign. If there are multiple numbers, one is picked at random.", + canSpendMoney: false, + moneySpendingOperations: [], + supportsOrgConfig: false, + supportsCampaignConfig: true +}); + +export async function onMessageSend({ + message, + contact, + organization, + campaign, + serviceManagerData +}) { + console.log( + "numpicker-campaign.onMessageSend", + message.id, + message.user_number, + serviceManagerData + ); + if ( + message.user_number || + (serviceManagerData && serviceManagerData.user_number) + ) { + // This is meant as a fallback -- if another serviceManager already + // chose a phone number then don't change anything + return; + } + const campaignNumbers = getFeatures(campaign).campaignNumbers || []; + const selectedPhone = _.sample(campaignNumbers); + console.log("numpicker-campaign.onMessageSend selectedPhone", selectedPhone); + // TODO: caching + // TODO: something better than pure rotation -- maybe with caching we use metrics + // based on sad deliveryreports + if (selectedPhone) { + return { user_number: selectedPhone }; + } else { + console.log( + "numpicker-campaign.onMessageSend none found", + serviceName, + organization.id + ); + } +} + +async function _getAvailableNumbers(organization) { + const serviceName = cacheableData.organization.getMessageService( + organization + ); + const availableNumbers = await r + .knex("owned_phone_number") + .where({ service: serviceName, organization_id: organization.id }) + .whereNull("allocated_to_id") + .pluck("phone_number"); + + return availableNumbers; +} + +export async function getCampaignData({ + organization, + campaign, + user, + loaders, + fromCampaignStatsPage +}) { + // MUST NOT RETURN SECRETS! + // called both from edit and stats contexts: editMode==true for edit page + if (fromCampaignStatsPage) { + return {}; + } else { + const availableNumbers = await _getAvailableNumbers(organization); + const campaignNumbers = getFeatures(campaign).campaignNumbers; + + return { + data: { + availableNumbers, + campaignNumbers + }, + fullyConfigured: campaignNumbers > 0 + }; + } +} + +export async function onCampaignUpdateSignal({ + organization, + campaign, + updateData +}) { + await cacheableData.campaign.setFeatures(campaign.id, { + campaignNumbers: updateData + }); + + return { + data: { + campaignNumbers: updateData, + availableNumbers: await _getAvailableNumbers(organization) + }, + fullyConfigured: updateData.length > 0, + unArchiveable: false + }; +} diff --git a/src/extensions/service-managers/numpicker-campaign/react-component.js b/src/extensions/service-managers/numpicker-campaign/react-component.js new file mode 100644 index 000000000..e6a1c6790 --- /dev/null +++ b/src/extensions/service-managers/numpicker-campaign/react-component.js @@ -0,0 +1,75 @@ +import PropTypes from "prop-types"; +import React from "react"; +import Form from "react-formal"; +import GSForm from "../../../components/forms/GSForm"; +import TextField from "@material-ui/core/TextField"; +import Autocomplete from "@material-ui/lab/Autocomplete"; +import GSSubmitButton from "../../../components/forms/GSSubmitButton"; +import { dataSourceItem } from "../../../components/utils"; +import { getDisplayPhoneNumber } from "../../../lib/phone-format"; + +export class CampaignConfig extends React.Component { + constructor(props) { + super(props); + const selectedNumbers = this.props.serviceManagerInfo.data.campaignNumbers || []; + this.state = { + selectedNumbers: selectedNumbers.map(n => + dataSourceItem(getDisplayPhoneNumber(n), n)) + }; + } + + onNumberSelected = (event, selection) => { + this.setState({selectedNumbers: selection}); + } + + render() { + const selected = this.props.serviceManagerInfo.data.campaignNumbers || [] + .map(n => getDisplayPhoneNumber(n)) + .join(', '); + + const numberOptions = + this.props.serviceManagerInfo && + this.props.serviceManagerInfo.data && + this.props.serviceManagerInfo.data.availableNumbers && + this.props.serviceManagerInfo.data.availableNumbers.map(n => + dataSourceItem(getDisplayPhoneNumber(n), n)); + return ( +
+ Select the phone number(s) to use for this campaign. + {!this.props.campaign.isStarted || !selected ? ( + { + this.props.onSubmit(this.state.selectedNumbers.map(n => n.rawValue)); + }} + > + option.text || ""} + value={this.state.selectedNumbers || []} + renderInput={params => ( + + )} + /> + + + ) : ( +
Using numbers: {selected}
+ )} +
+ ); + } +} + +CampaignConfig.propTypes = { + user: PropTypes.object, + campaign: PropTypes.object, + serviceManagerInfo: PropTypes.object, + saveLabel: PropTypes.string, + onSubmit: PropTypes.func +}; diff --git a/src/lib/phone-format.js b/src/lib/phone-format.js index a1e0c92e9..b6ab5d62f 100644 --- a/src/lib/phone-format.js +++ b/src/lib/phone-format.js @@ -26,6 +26,7 @@ const parsePhoneNumber = (e164Number, country = "US") => { }; export const getDisplayPhoneNumber = (e164Number, country = "US") => { + const phoneUtil = PhoneNumberUtil.getInstance(); const parsed = parsePhoneNumber(e164Number, country); return phoneUtil.format(parsed, PhoneNumberFormat.NATIONAL); };