Files
P42_UI/app/models/comms/mailings/MailingsModel.js
T
2025-08-27 07:03:09 +00:00

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);