import React from "react" import { Link } from "react-router-dom" import { Row, Col, Button, Table, Icon, Tooltip, Tag, Popconfirm, Progress, Modal, Select, notification, Input } from "antd" import dayjs from "dayjs" import relativeTime from 'dayjs/plugin/relativeTime' import ModalPreview from "./ModalPreview" import * as cs from "./constants" class Campaigns extends React.PureComponent { defaultPerPage = 20 state = { formType: null, pollID: -1, queryParams: "", stats: {}, record: null, previewRecord: null, cloneName: "", cloneModalVisible: false, modalWaiting: false } // Pagination config. paginationOptions = { hideOnSinglePage: true, showSizeChanger: true, showQuickJumper: true, defaultPageSize: this.defaultPerPage, pageSizeOptions: ["20", "50", "70", "100"], position: "both", showTotal: (total, range) => `${range[0]} to ${range[1]} of ${total}`, onChange: (page, perPage) => { this.fetchRecords({ page: page, per_page: perPage }) }, onShowSizeChange: (page, perPage) => { this.fetchRecords({ page: page, per_page: perPage }) } } constructor(props) { super(props) this.columns = [{ title: "Name", dataIndex: "name", sorter: true, width: "20%", vAlign: "top", render: (text, record) => { const out = []; out.push(
{ text }
{ record.subject }
) if(record.tags.length > 0) { for (let i = 0; i < record.tags.length; i++) { out.push({ record.tags[i] }); } } return out } }, { title: "Status", dataIndex: "status", className: "status", width: "10%", render: (status, record) => { let color = cs.CampaignStatusColors.hasOwnProperty(status) ? cs.CampaignStatusColors[status] : "" return (
{status} {record.send_at && Scheduled — { dayjs(record.send_at).format(cs.DateFormat) } }
) } }, { title: "Lists", dataIndex: "lists", width: "25%", align: "left", className: "lists", render: (lists, record) => { const out = [] lists.forEach((l) => { out.push( { l.name } ) }) return out } }, { title: "Stats", className: "stats", width: "30%", render: (text, record) => { if(record.status !== cs.CampaignStatusDraft && record.status !== cs.CampaignStatusScheduled) { return this.renderStats(record) } } }, { title: "", dataIndex: "actions", className: "actions", width: "15%", render: (text, record) => { return (
{ ( record.status === cs.CampaignStatusPaused ) && this.handleUpdateStatus(record, cs.CampaignStatusRunning)}> } { ( record.status === cs.CampaignStatusRunning ) && this.handleUpdateStatus(record, cs.CampaignStatusPaused)}> } {/* Draft with send_at */} { ( record.status === cs.CampaignStatusDraft && record.send_at) && this.handleUpdateStatus(record, cs.CampaignStatusScheduled) }> } { ( record.status === cs.CampaignStatusDraft && !record.send_at) && this.handleUpdateStatus(record, cs.CampaignStatusRunning) }> } { ( record.status === cs.CampaignStatusPaused || record.status === cs.CampaignStatusRunning) && this.handleUpdateStatus(record, cs.CampaignStatusCancelled)}> } { this.handlePreview(record) }}> { let r = { ...record, lists: record.lists.map((i) => { return i.id }) } this.handleToggleCloneForm(r) }}> { ( record.status === cs.CampaignStatusDraft || record.status === cs.CampaignStatusScheduled ) && this.handleDeleteRecord(record)}> }
) } }] } progressPercent(record) { return Math.round(this.getStatsField("sent", record) / this.getStatsField("to_send", record) * 100, 2) } isDone(record) { return this.getStatsField("status", record) === cs.CampaignStatusFinished || this.getStatsField("status", record) === cs.CampaignStatusCancelled } // getStatsField returns a stats field value of a given record if it // exists in the stats state, or the value from the record itself. getStatsField = (field, record) => { if(this.state.stats.hasOwnProperty(record.id)) { return this.state.stats[record.id][field] } return record[field] } renderStats = (record) => { let color = cs.CampaignStatusColors.hasOwnProperty(record.status) ? cs.CampaignStatusColors[record.status] : "" const startedAt = this.getStatsField("started_at", record) const updatedAt = this.getStatsField("updated_at", record) const sent = this.getStatsField("sent", record) const toSend = this.getStatsField("to_send", record) const isDone = this.isDone(record) const r = this.getStatsField("rate", record) const rate = r ? r : 0 return (
{ !isDone && } Sent { sent >= toSend && { toSend } } { sent < toSend && { sent } / { toSend } }   { record.status === cs.CampaignStatusRunning && } { rate > 0 && Rate{ Math.round(rate, 2) } / min } Views{ record.views } Clicks{ record.clicks }
Created{ dayjs(record.created_at).format(cs.DateFormat) } { startedAt && Started{ dayjs(startedAt).format(cs.DateFormat) } } { isDone && Ended { dayjs(updatedAt).format(cs.DateFormat) } } { startedAt && updatedAt && Duration { dayjs(updatedAt).from(dayjs(startedAt), true) } }
) } componentDidMount() { this.props.pageTitle("Campaigns") dayjs.extend(relativeTime) this.fetchRecords() // Did we land here to start a campaign? let loc = this.props.route.location let state = loc.state if(state && state.hasOwnProperty("campaign")) { this.handleUpdateStatus(state.campaign, state.campaignStatus) delete state.campaign delete state.campaignStatus this.props.route.history.replace({ ...loc, state }) } } componentWillUnmount() { window.clearInterval(this.state.pollID) } fetchRecords = (params) => { let qParams = { page: this.state.queryParams.page, per_page: this.state.queryParams.per_page } // The records are for a specific list. if(this.state.queryParams.listID) { qParams.listID = this.state.queryParams.listID } if(params) { qParams = { ...qParams, ...params } } this.props.modelRequest(cs.ModelCampaigns, cs.Routes.GetCampaigns, cs.MethodGet, qParams).then((r) => { this.startStatsPoll() }) } startStatsPoll = () => { window.clearInterval(this.state.pollID) this.setState({ "stats": {} }) // If there's at least one running campaign, start polling. let hasRunning = false this.props.data[cs.ModelCampaigns].forEach((c) => { if(c.status === cs.CampaignStatusRunning) { hasRunning = true return } }) if(!hasRunning) { return } // Poll for campaign stats. let pollID = window.setInterval(() => { this.props.request(cs.Routes.GetRunningCampaignStats, cs.MethodGet).then((r) => { // No more running campaigns. if(r.data.data.length === 0) { window.clearInterval(this.state.pollID) this.fetchRecords() return } let stats = {} r.data.data.forEach((s) => { stats[s.id] = s }) this.setState({ stats: stats }) }).catch(e => { console.log(e.message) }) }, 3000) this.setState({ pollID: pollID }) } handleUpdateStatus = (record, status) => { this.props.modelRequest(cs.ModelCampaigns, cs.Routes.UpdateCampaignStatus, cs.MethodPut, { id: record.id, status: status }) .then(() => { notification["success"]({ placement: cs.MsgPosition, message: `Campaign ${status}`, description: `"${record.name}" ${status}` }) // Reload the table. this.fetchRecords() }).catch(e => { notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message }) }) } handleDeleteRecord = (record) => { this.props.modelRequest(cs.ModelCampaigns, cs.Routes.DeleteCampaign, cs.MethodDelete, { id: record.id }) .then(() => { notification["success"]({ placement: cs.MsgPosition, message: "Campaign deleted", description: `"${record.name}" deleted` }) // Reload the table. this.fetchRecords() }).catch(e => { notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message }) }) } handleToggleCloneForm = (record) => { this.setState({ cloneModalVisible: !this.state.cloneModalVisible, record: record, cloneName: record.name }) } handleCloneCampaign = (record) => { this.setState({ modalWaiting: true }) this.props.modelRequest(cs.ModelCampaigns, cs.Routes.CreateCampaign, cs.MethodPost, record).then((resp) => { notification["success"]({ placement: cs.MsgPosition, message: "Campaign created", description: `${record.name} created` }) this.setState({ record: null, modalWaiting: false }) this.props.route.history.push(cs.Routes.ViewCampaign.replace(":id", resp.data.data.id)) }).catch(e => { notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message }) this.setState({ modalWaiting: false }) }) } handlePreview = (record) => { this.setState({ previewRecord: record }) } render() { const pagination = { ...this.paginationOptions, ...this.state.queryParams } return (

Campaigns


record.uuid } dataSource={ this.props.data[cs.ModelCampaigns] } loading={ this.props.reqStates[cs.ModelCampaigns] !== cs.StateDone } pagination={ pagination } /> { this.state.previewRecord && { this.setState({ previewRecord: null }) }} /> } { this.state.cloneModalVisible && this.state.record && { this.handleCloneCampaign({ ...this.state.record, name: this.state.cloneName }) }}> { this.setState({ cloneName: e.target.value }) }} /> } ) } } export default Campaigns