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: "",
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: "30%",
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: "20%",
align: "left",
className: "lists",
render: (lists, record) => {
const out = []
lists.forEach((l) => {
out.push(
{ l.name }
)
})
return out
}
},
{
title: "Stats",
className: "stats",
render: (text, record) => {
if(record.status !== cs.CampaignStatusDraft && record.status !== cs.CampaignStatusScheduled) {
return this.renderStats(record)
}
}
},
{
title: "",
dataIndex: "actions",
className: "actions",
width: "20%",
render: (text, record) => {
return (
{
this.handlePreview(record)
}}>
{
let r = { ...record, lists: record.lists.map((i) => { return i.id }) }
this.handleToggleCloneForm(r)
}}>
{ ( 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)}>
}
{ ( 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
}
Views0
Clicks0
Created{ dayjs(record.created_at).format(cs.DateFormat) }
{ startedAt &&
Started{ dayjs(startedAt).format(cs.DateFormat) }
}
{ isDone &&
Ended
{ dayjs(updatedAt).format(cs.DateFormat) }
}
Duration
{ startedAt ? dayjs(updatedAt).from(dayjs(startedAt), true) : "" }
)
}
componentDidMount() {
dayjs.extend(relativeTime)
this.fetchRecords()
}
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: "topRight", message: `Campaign ${status}`, description: `"${record.name}" ${status}` })
// Reload the table.
this.fetchRecords()
}).catch(e => {
notification["error"]({ message: "Error", description: e.message })
})
}
handleDeleteRecord = (record) => {
this.props.modelRequest(cs.ModelCampaigns, cs.Routes.DeleteCampaign, cs.MethodDelete, { id: record.id })
.then(() => {
notification["success"]({ placement: "topRight", message: "Campaign deleted", description: `"${record.name}" deleted` })
// Reload the table.
this.fetchRecords()
}).catch(e => {
notification["error"]({ message: "Error", description: e.message })
})
}
handleToggleCloneForm = (record) => {
this.setState({ 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: "topRight",
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"]({ 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 })
}}
/>
}
)
}
}
export default Campaigns