Files
P42_UI/app/views/common/support/SupportView.js
T
2025-08-27 07:03:09 +00:00

359 lines
13 KiB
JavaScript

class SupportView extends EICDomContent {
TICKET_STATUSES = [
{id: 0, label: '', severity: ''},
{id: 1, label: 'new', severity: 'info'},
{id: 2, label: 'opened', severity: 'warning'},
{id: 3, label: 'opened', severity: 'warning'},
{id: 4, label: 'closed', severity: 'success'},
{id: 5, label: '(state5)', severity: 'danger'},
{id: 6, label: '(state6)', severity: 'danger'},
{id: 7, label: 'opened', severity: 'warning'},
]
constructor() {
super();
Object.assign(this, app.helpers.basicDialogs)
}
DOMContentLoaded(options) {
this.components = ui.eicfy(this.el)
for(let model in options.models) this[model] = options.models[model];
this.gridTickets = new DataGrid(this.find('.support-tickets .list [eicdatagrid]'), {
headers: [
{ label: 'Ticket', filter: 'text', sortable: true},
{ label: 'Modified', type: 'dateTime', sortable: true},
{ label: 'Status', type: 'markup', filter: 'list', sortable: true},
]
});
this.btTicketsRefresh = new Button(null, {icon: 'icon-refresh', severity: 'primary', rounded: true, size: 'xsmall', hint: 'Refresh the list'})
this.btTicketsRefresh.addEventListener('click', this.refreshTicketsList.bind(this));
this.gridTickets.el.querySelector('header .cell.actions').append(this.btTicketsRefresh.el);
this.gridTickets.onRowClick = this.onTicketSelect.bind(this);
this.gridConsole = new DataGrid(this.find('.support-console [eicdatagrid]'), {
headers: [
{ label: 'Time', filter: 'text', sortable: true},
{ label: 'Level', filter: 'list', sortable: true},
{ label: 'Error', filter: 'text', sortable: true},
{ label: 'Url', filter: 'text', sortable: true},
],
actions: [],
height: '30vh'
});
this.gridConsole.onRowClick = this.showErrDetails.bind(this)
this.ticketDetail = this.find('.support-tickets .inspector');
ui.hide(this.ticketDetail);
this.btReply = this.find('.support-tickets .inspector footer .cta button');
this.btReply.addEventListener('click', this.onTicketReplyOpen.bind(this))
this.attachments = this.components.find(item => item.el.name == 'attachments')
this.attachments.activeEditor = BinaryFileContentSelector;
this.replyForm = new Form(this.find('.support-tickets .inspector footer .reply-form'));
ui.hide(this.replyForm.el);
this.replyForm.el.querySelector('button.send').addEventListener('click', this.onTicketReply.bind(this))
this.replyForm.el.querySelector('button.cancel').addEventListener('click', this.onTicketReplyCancel.bind(this))
this.btIssueNew = this.components.find(item => item.el.classList.contains('create'));
this.btIssueNew.addEventListener('click', this.onTicketCreate.bind(this));
this.refreshConsole();
this.refreshTicketsList()
}
DOMContentFocused() {
if(!this.refreshTo) this.refreshTo = setTimeout(this.checkUp2Date.bind(this),500)
this.refreshConsole();
}
DOMContentBlured() {
if(this.refreshTo) {
clearTimeout(this.refreshTo)
this.refreshTo = null
}
}
refreshTicketsList(event=null){
if(event){
event.stopPropagation();
event.preventDefault();
}
this.gridTickets.loading = true;
this.tickets.list()
.then(this.refreshTickets.bind(this));
}
refreshTickets(payload) {
this.gridTickets.loading = false;
this.gridTickets.clear();
let tickets = payload.reverse();
if(tickets.length > 0) {
for(let ticket of tickets) {
let status = this.TICKET_STATUSES.find(item => item.id == (ticket.state.id || 0))
this.gridTickets.addRow(ticket.id, [
`${ticket.title} (#${ticket.number})`,
ticket.state.lastActivity,
`<span eicchip xsmall ${status.severity}>${status.label}</span>`,
], true);
}
this.gridTickets.filter()
this.tickets.get(tickets[0].id)
.then(this.refreshInspector.bind(this));
}
}
clearInspector() {
ui.show(this.ticketDetail, 'grid');
this.ticketDetail.classList.remove('loading');
this.ticketDetail.querySelector('header h1').innerHTML = ''
this.ticketDetail.querySelector('header h2').innerHTML = ''
this.ticketDetail.querySelector('section').innerHTML = '';
this.replyForm.el.querySelector('textarea').innerHTML = '';
this.attachments.clear();
}
freezeInspector() {
ui.show(this.ticketDetail, 'grid');
this.ticketDetail.classList.add('loading');
}
refreshInspector(payload) {
let ticket = payload.ticket;
let articles = payload.articles.reverse();
this.clearInspector();
let content = this.ticketDetail.querySelector('section');
this.ticketDetail.querySelector('header h1').innerHTML = `${ticket.title} (#${ticket.number})`;
this.ticketDetail.querySelector('header h2').innerHTML = ticket.typeofissue || 'General issue';
this.replyForm.el.querySelector('button.send').dataset.id = ticket.id;
let lastDate = null;
for(let article of articles) {
let message = article.body
.split("\n").join('<br/>')
.split(' -> ').join(' &#8594; ')
let currentDate = ui.format.date( article.created )
if(lastDate != currentDate ) {
content.append(ui.create(`<div class="center" small>${currentDate}</div>`))
lastDate = currentDate;
}
switch(article.sender) {
case 'Agent':
case 'System':
content.append(ui.create(`<div class="cols-1 left sender"><span eicchip primary xsmall><i class="icon-user"></i><span>EIC support wrote</span></span></div>`))
break;
case 'Customer':
content.append(ui.create(`<div class="cols-1 right sender"><span eicchip success xsmall><i class="icon-user"></i><span>You wrote</span></span></div>`))
break;
}
let thread = ui.create(`<div class="content"><div class="subject">${article.subject || ''}</div><div class="message">${message}</div><div class="attachments"></div></div>`)
content.append(thread)
for(let file of article.attachments) {
let button = new Button(null, {
label: file.filename,
severity: 'info',
size: 'xsmall',
rounded: true,
icon: 'icon-attachment'
});
button.el.dataset.ticket = ticket.id;
button.el.dataset.article = article.id;
button.el.dataset.attachment = file.id;
button.el.addEventListener('click', this.onFileOpen.bind(this));
thread.querySelector('.attachments').append(button.el)
}
}
}
refreshConsole() {
this.gridConsole.clear();
for(let err of app.latestErrors) {
this.gridConsole.addRow(err.errId,
[ err.timestamp,
`<span class="${err.level.toLowerCase()}">${err.level}</span>`,
err.message,
err.url,
]
);
}
}
checkUp2Date() {
let mustRefresh = false
if(app.latestErrors.lengh != this.gridConsole.length) mustRefresh=true
else {
for(let err of app.latestErrors) {
if(!this.gridConsole.getRowById(err.errId)) { // err not in grid (the opposite should not happen)
mustRefresh = true
break
}
}
}
if(mustRefresh) this.refreshConsole()
this.refreshTo = setTimeout(this.checkUp2Date.bind(this),500)
}
onFileOpen(event) {
event.stopPropagation();
event.preventDefault();
let ticketId = event.currentTarget.dataset.ticket;
let articleId = event.currentTarget.dataset.article;
let attachmentId = event.currentTarget.dataset.attachment;
this.tickets.download(ticketId, articleId, attachmentId);
}
onTicketSelect(target) {
this.closeReplyForm();
this.freezeInspector();
this.tickets.get(target.dataset.id)
.then(this.refreshInspector.bind(this));
}
async onTicketCreate(event) {
event.stopPropagation();
event.preventDefault();
let ticket = await this.openDialog(
await this.loadContent(
'common/support/dialogs/SupportIssueFormDialog',
{ title: 'Report an issue' },
{
mode: 'create',
models: { tickets: this.tickets }
}
)
);
if(ticket) {
this.tickets.list()
.then(this.refreshTickets.bind(this));
}
}
closeReplyForm() {
ui.show(this.ticketDetail.querySelector('footer .cta'), 'grid');
ui.hide(this.ticketDetail.querySelector('footer .reply-form'));
}
openReplyForm() {
ui.hide(this.ticketDetail.querySelector('footer .cta'));
ui.show(this.ticketDetail.querySelector('footer .reply-form'), 'grid');
}
onTicketReplyOpen(event) {
event.stopPropagation();
event.preventDefault();
this.openReplyForm();
}
async onTicketReply(event) {
event.stopPropagation();
event.preventDefault();
let ticketId = event.currentTarget.dataset.id;
if(this.replyForm.validate()) {
let payload = this.replyForm.value;
payload.attachments = this.attachments.selectedItems.map(item => { return { filename: item.label, "mime-type": item.type, data: item.content} })
let size = JSON.stringify(payload).length
let limit = 1024 * 1024 * 5;
if(size < limit) {
this.tickets.update(ticketId, payload)
.then( this.onReplySuccess.bind(this, ticketId))
} else {
ui.growl.append('Your message is exceeding the allowed 5Mb size limit. Please consider removing files or reduce file size', 'danger')
}
}
}
onReplySuccess(id, payload) {
this.replyForm.el.querySelector('textarea').value = '';
this.attachments.clear();
this.closeReplyForm();
this.tickets.list().then( this.refreshTickets.bind(this));
this.tickets.get(id).then(this.refreshInspector.bind(this));
}
onTicketReplyCancel(event) {
event.stopPropagation();
event.preventDefault();
this.closeReplyForm();
}
async showErrDetails(row, event) {
let err = app.latestErrors.find(err=>err.errId==row.dataset['id'])
if(!err) return
let trace = '<div class="trace">'
for(let step of err.stacktrace) {
if(step[2].length<2) continue
trace += `<div class="traceline">${step[0]} @ ${step[1]} ${step[2].join(':')}</div>`
}
trace += '</div>'
let messageData = ''
if(Array.isArray(err.messageData)) {
messageData = '<div class="msgdata">'+err.messageData.map(x=>JSON.stringify(x)).join('</div><div class="msgdata">')+'</div>'
} else if(err.messageData) messageData = '<div class="msgdata">'+err.messageData+'</div>'
let result = await this.confirmDialog({
title: 'Error Details',
message: `<div eicdatagrid class="console-error-detail" footer="hidden" header="hidden"></div>`,
cancelLabel: 'Close',
okLabel: '',
severity: 'primary',
muted: true,
okPromise:() => { },
contentLoaded: (el => {
el.style['max-width'] = '80vw'
el.style['max-height'] = '50vw'
this.gridEntry = new DataGrid(el.querySelector('.support-entryGrid'), {
headers: [
{ label: ''},
{ label: ''}
],
height: 'auto'
});
this.gridEntry.addRow('time', ['Time', err.timestamp])
this.gridEntry.addRow('level', ['Level', `<span class="${err.level.toLowerCase()}">${err.level}</span>`])
this.gridEntry.addRow('message', ['Message', `<span class="${err.level.toLowerCase()}">${err.message}</span><br>${messageData}`])
this.gridEntry.addRow('user', ['User UID', err.user.identity.uuid])
this.gridEntry.addRow('user', ['User Roles', err.user.roles.join(', ')])
this.gridEntry.addRow('url', ['Url', err.url])
this.gridEntry.addRow('stacktrace', ['Trace', trace])
}).bind(this),
})
}
}
app.registerClass('SupportView',SupportView);