427 lines
20 KiB
JavaScript
427 lines
20 KiB
JavaScript
class MailingsModel extends EICModel {
|
|
|
|
constructor(privileges) {
|
|
super('/mailing', privileges)
|
|
this.ffs = new FakeFileSystem()
|
|
Object.assign(this, app.helpers.validators)
|
|
}
|
|
|
|
async search(query, status=[]) {
|
|
if(!this.hasPrivilege('search')) return( new Promise((resolve, reject) => reject()))
|
|
let endpoint = this.getApiEndpoint('search')
|
|
let payload = {
|
|
query: query,
|
|
status: status,
|
|
}
|
|
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method, payload) // Separate filesystem info from true mailings
|
|
.then( async serverData => serverData.payload)
|
|
.then( data => { // And feed ffs
|
|
this.ffs.loadStructure(
|
|
data.ffs,
|
|
data.mailings.map(item=>(
|
|
{
|
|
fullPath: (item.path+'/'+item.name).replace(/\/+/g, '/'),
|
|
object : item
|
|
}
|
|
))
|
|
)
|
|
return(data.mailings)
|
|
})
|
|
)
|
|
}
|
|
|
|
async get(mid) {
|
|
if(!this.hasPrivilege('read')) return( new Promise((resolve, reject) => reject()))
|
|
let endpoint = this.getApiEndpoint('read')
|
|
endpoint.uri = endpoint.uri.replace('{mid}', mid)
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method)
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async getRecipients(mid){
|
|
if(!this.hasPrivilege('read')) return( new Promise((resolve, reject) => reject())) // if can read mailing => can read recipients
|
|
let endpoint = this.getApiEndpoint('read', '/mailing/{mid}/recipients')
|
|
endpoint.uri = endpoint.uri.replace('{mid}', mid)
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method)
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async getBounces(mid){
|
|
if(!this.hasPrivilege('read')) return( new Promise((resolve, reject) => reject())) // if can read mailing => can read bounces
|
|
let endpoint = this.getApiEndpoint('read', '/mailing/{mid}/bounces')
|
|
endpoint.uri = endpoint.uri.replace('{mid}', mid)
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method)
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async save(mailingInfo) {
|
|
// Local copy where we'll remove display (non-save-able) mailingInfo stuff, and join-in ext stuff into the request.
|
|
let localMmailingInfo = JSON.parse(JSON.stringify(mailingInfo))
|
|
|
|
// template struct => templateId
|
|
if(localMmailingInfo.template && localMmailingInfo.template.id){
|
|
localMmailingInfo.templateId = localMmailingInfo.template.id
|
|
delete(localMmailingInfo.template)
|
|
}
|
|
|
|
delete(localMmailingInfo.imports)
|
|
delete(localMmailingInfo.sources)
|
|
delete(localMmailingInfo.statusHistory)
|
|
delete(localMmailingInfo.kpis)
|
|
delete(localMmailingInfo.nbSources)
|
|
delete(localMmailingInfo.nbRecipients)
|
|
|
|
let endpoint = this.getApiEndpoint('save')
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method, localMmailingInfo)
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async delete(mid) {
|
|
if(!this.hasPrivilege('delete')) return( new Promise((resolve, reject) => reject()))
|
|
let endpoint = this.getApiEndpoint('delete')
|
|
endpoint.uri = endpoint.uri.replace('{mid}', mid)
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method)
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async schedule(mid, scheduleDate) {
|
|
if(!this.hasPrivilege('schedule')) return( new Promise((resolve, reject) => reject()))
|
|
let endpoint = this.getApiEndpoint('schedule')
|
|
endpoint.uri = endpoint.uri.replace('{mid}', mid)
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method, { scheduleDate: scheduleDate })
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async test(mid, templateId, recipientEmail, mappings={}) {
|
|
if((!this.hasPrivilege('edit')) && (!this.hasPrivilege('approve')) && (!this.hasPrivilege('reject'))) return( new Promise((resolve, reject) => reject()))
|
|
let endpoint = this.getApiEndpoint('test')
|
|
endpoint.uri = endpoint.uri.replace('{mid}', mid)
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method, {
|
|
templateId: templateId,
|
|
recipientEmail: recipientEmail,
|
|
parameters: mappings
|
|
})
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async getImports(mid) {
|
|
let endpoint = this.getApiEndpoint('list', '/mailing/{mid}/imports')
|
|
endpoint.uri = endpoint.uri.replace('{mid}', mid)
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method)
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async saveImport(mid, data) {
|
|
data.refresh = false
|
|
let endpoint = this.getApiEndpoint('save', '/mailing/{mid}/imports')
|
|
endpoint.uri = endpoint.uri.replace('{mid}', mid)
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method, data)
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async saveExclusion(mid, data) {
|
|
data.refresh = false
|
|
data.exclusionList = true
|
|
data.availableColumns = data.availableColumns.filter(item=>item.isEmail)
|
|
const emailIdx = data.availableColumns[0].value
|
|
data.data = data.data.map(row=>row[emailIdx])
|
|
let endpoint = this.getApiEndpoint('save', '/mailing/{mid}/imports')
|
|
endpoint.uri = endpoint.uri.replace('{mid}', mid)
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method, data)
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async refreshImport(mid, data, importId) {
|
|
if(data.sourceType=='Excel') return({})
|
|
data.refresh = true
|
|
data.importId = importId
|
|
let endpoint = this.getApiEndpoint('save', '/mailing/{mid}/imports')
|
|
endpoint.uri = endpoint.uri.replace('{mid}', mid)
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method, data)
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async readImport(mid, iid) {
|
|
let endpoint = this.getApiEndpoint('read', '/mailing/{mid}/imports/{iid}')
|
|
endpoint.uri = endpoint.uri.replace('{mid}', mid).replace('{iid}', iid)
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method)
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async deleteImport(mid, id) {
|
|
let endpoint = this.getApiEndpoint('delete', '/mailing/{mid}/imports')
|
|
endpoint.uri = endpoint.uri.replace('{mid}', mid)
|
|
return (
|
|
this.request(endpoint.uri, endpoint.method, { id: id })
|
|
.then( async serverData => serverData.payload)
|
|
)
|
|
}
|
|
|
|
async getReadableFolders() {
|
|
try {
|
|
const endpoint = this.getApiEndpoint('list', '/mailing/folders')
|
|
const payload = { path: '/', withFiles: true }
|
|
|
|
const serverData = await this.request(endpoint.uri, endpoint.method, payload)
|
|
return serverData.payload || {}
|
|
} catch (err) {
|
|
console.error("Error in getReadableFolders", err)
|
|
return null
|
|
}
|
|
}
|
|
|
|
async makeDir(path=null, dirName){
|
|
let ffs = this.ffs
|
|
if(path && (!ffs.pathExists(path))) return(Promise.resolve(null))
|
|
if(!path) path=ffs.currentPath
|
|
dirName = dirName.replace(/\//g,'').replace(/"/g,'').replace('\\','').replace(/\./g,'')
|
|
|
|
let endpoint = this.getApiEndpoint('add', '/mailing/folders')
|
|
let uri = endpoint.uri
|
|
return (
|
|
this.request(uri, endpoint.method, {
|
|
appId: 2,
|
|
path: path,
|
|
newFolder: dirName
|
|
})
|
|
.then( async serverData => {
|
|
return(serverData.payload)
|
|
})
|
|
)
|
|
}
|
|
|
|
async removeDir(appId, path = null) {
|
|
let ffs = this.ffs
|
|
if (!path) return Promise.resolve(null)
|
|
|
|
path = ffs.normalizePath(path)
|
|
|
|
let endpoint = this.getApiEndpoint('delete', '/mailing/folders')
|
|
let uri = endpoint.uri
|
|
|
|
|
|
return this.request(uri, endpoint.method, {
|
|
appId: appId,
|
|
path: path
|
|
}).then(async serverData => {
|
|
return serverData.payload
|
|
})
|
|
}
|
|
|
|
|
|
/********************************* Data helpers **************************************/
|
|
getStatusLabel() {
|
|
return({
|
|
created : 'Created',
|
|
draft : 'Draft',
|
|
submitted : app.User.roles.includes('MAIL_Reviewer') ? 'To review' : 'Being reviewed',
|
|
approved : 'Approved',
|
|
rejected : 'Rejected',
|
|
scheduled : 'Scheduled',
|
|
sent : 'Sent',
|
|
})
|
|
}
|
|
|
|
findDupes(allImports){
|
|
let dupes = {} //Key email, value: array of where it is found
|
|
let allEmails = []
|
|
for(let oneImport of allImports){
|
|
const emailCol = oneImport.availableColumns.find(item => item.isEmail)
|
|
if(!emailCol) continue
|
|
for(let[rownb, row] of oneImport.data.entries()){
|
|
let inAllEmails = allEmails.find(item => ( item.email == row[emailCol.value]))
|
|
if(inAllEmails) { // that's a dupe
|
|
if(!(row[emailCol.value] in dupes)) { // First time we come across this one, add the original entry
|
|
dupes[row[emailCol.value]] = [{
|
|
sourceName: inAllEmails.sourceName,
|
|
sourceType: inAllEmails.sourceType,
|
|
rownb: inAllEmails.rownb,
|
|
}]
|
|
}
|
|
dupes[row[emailCol.value]].push({ // Add the dupe
|
|
sourceName: oneImport.sourceName,
|
|
sourceType: oneImport.sourceType,
|
|
rownb: rownb,
|
|
})
|
|
} else {
|
|
allEmails.push({
|
|
email: row[emailCol.value],
|
|
sourceName: oneImport.sourceName,
|
|
sourceType : oneImport.sourceType,
|
|
rownb: rownb,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return(dupes)
|
|
}
|
|
|
|
getLatestStatus(mailingInfo, status=null){
|
|
if(!mailingInfo || !mailingInfo.statusHistory || (mailingInfo.statusHistory.length<1)) return('')
|
|
const latest = mailingInfo.statusHistory.reduce((max, obj) =>
|
|
(obj.dateTime > max.dateTime) && ((!status) || (status==obj.value)) ? obj : max
|
|
)
|
|
return( (!status) ? latest : (status == latest.value) ? latest : null )
|
|
}
|
|
|
|
getStepActivations(mailing, templateTokens){
|
|
const stepActivations = { }
|
|
stepActivations['start'] = {
|
|
disabled: (!this.hasPrivilege('edit')) || ((mailing.status != 'created') && (mailing.status != 'draft')),
|
|
severity: (mailing.status != 'created') ? 'success' : 'warning',
|
|
done: (mailing.name && mailing.path)
|
|
}
|
|
stepActivations['template'] = {
|
|
disabled: (!this.hasPrivilege('edit')) || (mailing.status != 'draft'),
|
|
severity: (!mailing.template.id) ? (mailing.status != 'draft') ? 'secondary' : 'warning' : 'success',
|
|
done: (mailing.template.id)
|
|
}
|
|
stepActivations['recipients'] = {
|
|
disabled: (!this.hasPrivilege('edit')) || (mailing.status != 'draft'), // || (!stepActivations.template.done),
|
|
severity: (mailing.nbRecipients == 0) ? ((mailing.status != 'draft') || (!mailing.template.id)) ? 'secondary' : 'warning' : 'success',
|
|
done: (mailing.nbRecipients > 0)
|
|
}
|
|
stepActivations['mappings'] = {
|
|
disabled: (!this.hasPrivilege('edit')) || (mailing.status != 'draft') || (!stepActivations.template.done) || (!stepActivations.recipients.done),
|
|
severity: (stepActivations.template.done && stepActivations.recipients.done) ? (this.TotalNbMapMissing(mailing, templateTokens) > 0) ? 'warning': 'success' : 'secondary',
|
|
done: (stepActivations['template'].done && stepActivations['recipients'].done && (this.TotalNbMapMissing(mailing, templateTokens) == 0))
|
|
}
|
|
stepActivations['approval'] = {
|
|
disabled: ((!this.hasPrivilege('edit')) && (!this.hasPrivilege('approve')) && (!this.hasPrivilege('reject'))) || (!(['draft','submitted'].includes(mailing.status))) || (!stepActivations.mappings.done) ,
|
|
severity: stepActivations.mappings.done ? ((mailing.status == 'approved') || (mailing.status == 'scheduled')) ? 'success' : 'warning' : 'secondary',
|
|
done: (mailing.status == 'approved')
|
|
}
|
|
stepActivations['schedule'] = {
|
|
disabled: (!this.hasPrivilege('schedule')) || ((mailing.status!='approved') && (mailing.status!='scheduled')),
|
|
severity: (mailing.status!='scheduled') ? (mailing.status!='approved') ? 'secondary' : 'warning' : 'success',
|
|
done: (mailing.status=='scheduled')
|
|
}
|
|
return(stepActivations)
|
|
}
|
|
|
|
nbMapMissingPerSrc(mailing, templateTokens){
|
|
let nbMissing = {}
|
|
mailing.sources.forEach(src => {
|
|
let srcMappings = mailing.mappings.find(mp => mp. sourceId==src.id)
|
|
if(srcMappings) {
|
|
nbMissing[src.id] = (templateTokens.length - Object.keys(srcMappings.mappings).length)
|
|
} else {
|
|
nbMissing[src.id] = templateTokens.length
|
|
}
|
|
})
|
|
return(nbMissing)
|
|
}
|
|
|
|
TotalNbMapMissing(mailing, templateTokens){
|
|
const missingPerSrc = this.nbMapMissingPerSrc(mailing, templateTokens)
|
|
return(Object.keys(missingPerSrc).reduce((acc,key) => acc+=missingPerSrc[key],0))
|
|
}
|
|
|
|
getReviewBlocks(mailing){
|
|
const latestSubmitted = this.getLatestStatus(mailing, 'submitted')
|
|
const latestRejected = this.getLatestStatus(mailing, 'rejected')
|
|
const latestApproved = this.getLatestStatus(mailing, 'approved')
|
|
let blocks = []
|
|
let contents = {}
|
|
if(!app.User.roles.includes('MAIL_Reviewer')){
|
|
// Only Editors & Revieers can sen test mails
|
|
if(app.User.roles.includes('MAIL_Editor')) blocks.push('testPane')
|
|
blocks.push('revieweePane')
|
|
switch(mailing.status){
|
|
case 'submitted': blocks.push('revieweeOngoing'); break
|
|
case 'approved': blocks.push('revieweeApproved')
|
|
contents['approUser1'] = `${latestApproved.changedBy.firstname} ${latestApproved.changedBy.lastname}`
|
|
contents['approDate1'] = (new Intl.DateTimeFormat("fr-FR", {timeZone: "Europe/Paris", dateStyle:'medium', timeStyle:'medium'}))
|
|
.format(new Date(latestApproved.dateTime))
|
|
break
|
|
case 'draft': if((latestRejected) && // no rejection ever
|
|
// Rejection and no approval afterwards (possible because might be draft again after unscheduled)
|
|
((!latestApproved) || (latestApproved.dateTime<latestRejected.dateTime))){
|
|
blocks.push('revieweeRejected')
|
|
contents['rejectionUser1'] = `${latestRejected.changedBy.firstname} ${latestRejected.changedBy.lastname}`
|
|
contents['rejectionDate1'] = (new Intl.DateTimeFormat("fr-FR", {timeZone: "Europe/Paris", dateStyle:'medium', timeStyle:'medium'}))
|
|
.format(new Date(latestRejected.dateTime))
|
|
contents['rejectionReason1'] = (latestRejected.meta && latestRejected.meta.reason) ? latestRejected.meta.reason : ''
|
|
} else if(app.User.roles.includes('MAIL_Editor')) {
|
|
blocks.push('revieweeRequest')
|
|
}
|
|
break
|
|
}
|
|
} else {
|
|
blocks.push('testPane')
|
|
|
|
switch(mailing.status){
|
|
case 'submitted': blocks.push('reviewerPane')
|
|
blocks.push('reviewerChoice')
|
|
contents['requestUser2'] =`${latestSubmitted.changedBy.firstname} ${latestSubmitted.changedBy.lastname}`
|
|
contents['requestDate2'] =(new Intl.DateTimeFormat("fr-FR", {timeZone: "Europe/Paris", dateStyle:'medium', timeStyle:'medium'}))
|
|
.format(new Date(latestSubmitted.dateTime))
|
|
contents['reviewComments2'] = (latestSubmitted.meta && latestSubmitted.meta.comments) ? latestSubmitted.meta.comments : ''
|
|
break
|
|
case 'approved': blocks.push('reviewerPane')
|
|
blocks.push('reviewerApproved')
|
|
contents['approUser2'] = `${latestApproved.changedBy.firstname} ${latestApproved.changedBy.lastname}`
|
|
contents['approDate2'] = (new Intl.DateTimeFormat("fr-FR", {timeZone: "Europe/Paris", dateStyle:'medium', timeStyle:'medium'}))
|
|
.format(new Date(latestApproved.dateTime))
|
|
break
|
|
case 'draft': if(app.User.roles.includes('MAIL_Editor')) {
|
|
// For those who are both editor & reviewer, let them ask themselves (or other reviewer)
|
|
blocks.push('revieweePane')
|
|
blocks.push('revieweeRequest')
|
|
}
|
|
if((latestRejected) &&
|
|
// Rejection and no approval afterwards (possible because might be draft again after unscheduled)
|
|
((!latestApproved) || (latestApproved.dateTime<latestRejected.dateTime))){
|
|
blocks.push('reviewerPane')
|
|
blocks.push('reviewerRejected')
|
|
contents['rejectionUser2'] = `${latestRejected.changedBy.firstname} ${latestRejected.changedBy.lastname}`
|
|
contents['rejectionDate2'] = (new Intl.DateTimeFormat("fr-FR", {timeZone: "Europe/Paris", dateStyle:'medium', timeStyle:'medium'}))
|
|
.format(new Date(latestRejected.dateTime))
|
|
contents['rejectionReason2'] = (latestRejected.meta && latestRejected.meta.reason) ? latestRejected.meta.reason : ''
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return([blocks, contents])
|
|
}
|
|
|
|
canImport(){
|
|
return(true)
|
|
}
|
|
|
|
canImportExclusion(){
|
|
return(true)
|
|
}
|
|
|
|
canFetch(){
|
|
return(app.User.identity.uuid=='steinic')
|
|
}
|
|
}
|
|
|
|
app.registerClass('MailingsModel', MailingsModel); |