410 lines
15 KiB
JavaScript
410 lines
15 KiB
JavaScript
/**
|
|
* Short Proposal Form
|
|
*
|
|
* @version 2023
|
|
* @author Michael Fallise
|
|
*
|
|
* Options:
|
|
* * modeAc
|
|
*
|
|
*/
|
|
class SubmissionShortForm2023View extends EICDomContent {
|
|
|
|
options = {
|
|
mode: 'read',
|
|
autoSaveInterval: 15 * 1000,
|
|
validationInterval: 2 * 1000,
|
|
}
|
|
|
|
chunks = [];
|
|
|
|
DOMContentLoaded(options) {
|
|
this.components = ui.eicfy(this.el)
|
|
for(let model in options.models) this[model] = options.models[model];
|
|
|
|
this.tabs = new Tab();
|
|
this.tabs.addTabs(this.findAll('.tabs-extended menu li'), this.findAll('.content'))
|
|
|
|
this.form = new Form(this.find('.form'));
|
|
|
|
this.pic = options.profile.pic;
|
|
this.number = options.profile.number;
|
|
this.members.list(this.pic,this.number).then(this.fillContributors.bind(this))
|
|
this.mode = options.profile.mode || this.options.mode;
|
|
this.checkConcurrency(options.profile.mode || this.options.mode);
|
|
}
|
|
|
|
async checkConcurrency(mode) {
|
|
if(mode == 'edit') {
|
|
let diff = (new Date()).getTime() - (new Date(this.submission.data.updated.date)).getTime()
|
|
let created = (new Date()).getTime() - (new Date(this.submission.data.created.date)).getTime()
|
|
if(diff < this.options.autoSaveInterval && created > this.options.autoSaveInterval) {
|
|
// someone is editing right now
|
|
let goReadOnly = await this.openDialog(
|
|
await this.loadContent(
|
|
'projects/submissions/dialogs/SubmissionFormConcurrencyDialog',
|
|
{ title: 'Someone is already at work' }, {
|
|
contributor: this.submission.data.updated.contributor
|
|
}
|
|
)
|
|
);
|
|
|
|
mode = 'read';
|
|
|
|
if(!goReadOnly) {
|
|
this.mode = 'edit';
|
|
this.unload();
|
|
}
|
|
}
|
|
}
|
|
|
|
this.fill(mode);
|
|
}
|
|
|
|
DOMContentRemoved() {
|
|
if(!this.loaded) return;
|
|
if(this.autoSave) {
|
|
clearInterval(this.autoSave)
|
|
this.autoSave = null;
|
|
};
|
|
if(this.validationCheck) {
|
|
clearInterval(this.validationCheck);
|
|
this.validationCheck = null;
|
|
}
|
|
if((this.mode == 'edit')) this.save();
|
|
|
|
for(let chunk of this.chunks) { chunk.view.DOMContentRemoved(); }
|
|
}
|
|
|
|
DOMContentBlured() {
|
|
if(!this.loaded) return;
|
|
if(this.autoSave) {
|
|
clearInterval(this.autoSave);
|
|
this.autoSave = null;
|
|
}
|
|
if(this.validationCheck) {
|
|
clearInterval(this.validationCheck);
|
|
this.validationCheck = null;
|
|
}
|
|
}
|
|
|
|
DOMContentFocused(options) {
|
|
if(!this.loaded) return;
|
|
|
|
//By Nike : if options, means rerouted, then new models, then switch to fresh models !
|
|
if(options) {
|
|
for(let model in options.models) this[model] = options.models[model];
|
|
}
|
|
|
|
//By Nike: propagate DOMContentFocused to tabs. Caution before setupMode which calls fill, which must have fresh model.data
|
|
for(let chunk of this.chunks) chunk.view.DOMContentFocused(options)
|
|
|
|
if(
|
|
options &&
|
|
options.profile &&
|
|
options.profile.mode &&
|
|
options.profile.mode != this.mode
|
|
) this.setupMode(options.profile.mode);
|
|
|
|
if(!this.autoSave && this.mode == 'edit') {
|
|
this.autoSave = setInterval(this.save.bind(this), this.options.autoSaveInterval);
|
|
}
|
|
if(!this.validationCheck && this.mode == 'edit') {
|
|
this.validationCheck = setInterval(this.validate.bind(this), this.options.validationCheck);
|
|
}
|
|
}
|
|
|
|
fill(mode) {
|
|
let myTabsPath = 'projects/submissions/2023/tabs/'
|
|
|
|
this.find('.tools .created .date').innerText = (new Date(this.submission.data.created.date)).toLocaleString("en-DE");
|
|
this.find('.tools .created .contributor').innerText = `${this.submission.data.created.contributor.firstname} ${this.submission.data.created.contributor.lastname}`;
|
|
|
|
this.refreshUpdated(this.submission.data.updated);
|
|
|
|
let queue = [
|
|
this.loadContent( myTabsPath+'SubmissionShortForm2023GeneralView', {}, { model: this.submission } )
|
|
.then( view => this.appendTab('general', view)),
|
|
this.loadContent( myTabsPath+'SubmissionShortForm2023CompanyView', {}, { model: this.submission } )
|
|
.then( view => this.appendTab('company', view)),
|
|
this.loadContent( myTabsPath+'SubmissionShortForm2023TeamView', {}, { model: this.submission, team: this.team, pic: this.pic, number: this.number } )
|
|
.then( view => this.appendTab('team', view)),
|
|
this.loadContent( myTabsPath+'SubmissionShortForm2023ProblemView', {}, { model: this.submission } )
|
|
.then( view => this.appendTab('problem', view)),
|
|
this.loadContent( myTabsPath+'SubmissionShortForm2023SolutionView', {}, { model: this.submission } )
|
|
.then( view => this.appendTab('solution', view)),
|
|
this.loadContent( myTabsPath+'SubmissionShortForm2023CompetitionView', {}, { model: this.submission } )
|
|
.then( view => this.appendTab('competition', view)),
|
|
this.loadContent( myTabsPath+'SubmissionShortForm2023FundingView', {}, { model: this.submission } )
|
|
.then( view => this.appendTab('funding', view)),
|
|
this.loadContent( myTabsPath+'SubmissionShortForm2023ImpactView', {}, { model: this.submission } )
|
|
.then( view => this.appendTab('impacts', view)),
|
|
this.loadContent( myTabsPath+'SubmissionShortForm2023DocumentsView', {}, { model: this.submission } )
|
|
.then( view => {
|
|
this.appendTab('documents', view);
|
|
view.el.addEventListener('save', this.save.bind(this) )
|
|
}),
|
|
this.loadContent( myTabsPath+'SubmissionShortForm2023ConsentView', {}, { model: this.submission, team: this.team, pic: this.pic, number: this.number } )
|
|
.then( view => this.appendTab('consent', view))
|
|
];
|
|
|
|
if(['draft', 'frozen'].includes(this.submission.data.status)) {
|
|
queue.push(
|
|
this.loadContent( myTabsPath+'SubmissionShortForm2023DisclaimerView', {}, { model: this.submission } )
|
|
.then( view => {
|
|
this.appendTab('status', view);
|
|
this.statusTab = view;
|
|
view.el.addEventListener('submit', this.submit.bind(this));
|
|
})
|
|
)
|
|
} else {
|
|
queue.push(
|
|
this.loadContent( myTabsPath+'SubmissionShortForm2023StatusView', {}, { model: this.submission } )
|
|
.then( view => {
|
|
this.appendTab('status', view);
|
|
this.statusTab = view;
|
|
view.el.addEventListener('withdraw', this.withdraw.bind(this));
|
|
view.el.addEventListener('clone', this.clone.bind(this));
|
|
view.el.addEventListener('complain', this.complain.bind(this));
|
|
})
|
|
)
|
|
}
|
|
|
|
Promise.allSettled(queue)
|
|
.then((results) => {
|
|
this.setupMode(mode);
|
|
this.loaded = true;
|
|
});
|
|
}
|
|
|
|
setupMode(mode) {
|
|
|
|
this.mode = mode;
|
|
|
|
for(let chunk of this.chunks) {
|
|
chunk.view.options.mode = mode;
|
|
chunk.view.fill(mode);
|
|
}
|
|
|
|
if(this.mode == 'read' && this.autoSave) {
|
|
clearInterval(this.autoSave);
|
|
this.autoSave = null
|
|
}
|
|
|
|
if(this.mode == 'read' && this.validationCheck) {
|
|
clearInterval(this.validationCheck);
|
|
this.validationCheck = null
|
|
}
|
|
|
|
if(this.mode == 'edit' && !this.autoSave) {
|
|
this.autoSave = setInterval(this.save.bind(this), this.options.autoSaveInterval);
|
|
this.save(); // perform a save on startup to mark editing start
|
|
}
|
|
|
|
if(this.mode == 'edit' && !this.validationCheck) {
|
|
this.validationCheck = setInterval(this.validate.bind(this), this.options.validationInterval);
|
|
}
|
|
|
|
if(!['draft', 'frozen'].includes(this.submission.data.status)) {
|
|
this.tabs.selectByIndex(this.tabs.triggers.length - 2);
|
|
}
|
|
}
|
|
|
|
appendTab(target, view) {
|
|
let chunk = {target: target, view: view};
|
|
this.chunks.push( chunk );
|
|
this.find(`.content[data-part="${target}"]`).append(view.el);
|
|
return chunk;
|
|
}
|
|
|
|
fillContributors(data) {
|
|
let metrics = { contributors: 0, pending: 0 }
|
|
this.find('.contributors').innerHTML = '';
|
|
for(let item of data) {
|
|
if(item.status == 'active') metrics.contributors++;
|
|
if(item.status == 'pending') metrics.pending++;
|
|
}
|
|
this.find('.contributors').innerHTML = '';
|
|
if(metrics.contributors > 0) {
|
|
let chip = new Chip(null, {label: 'contributors', severity: 'primary', badge: metrics.contributors});
|
|
chip.el.addEventListener('click', this.onContributorsEdit.bind(this))
|
|
this.find('.contributors').append(chip.el);
|
|
}
|
|
if(metrics.pending > 0) {
|
|
let chip = new Chip(null, {label: 'pending requests', severity: 'warning', badge: metrics.pending})
|
|
chip.el.addEventListener('click', this.onContributorsEdit.bind(this))
|
|
this.find('.contributors').append(chip.el);
|
|
}
|
|
}
|
|
|
|
async onContributorsEdit(event) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
|
|
await this.openDialog(
|
|
await this.loadContent(
|
|
'projects/submissions/dialogs/SubmissionFormAccessDialog',
|
|
{ title: 'Contributors to this proposal' }, {
|
|
pic: this.pic,
|
|
number: this.number,
|
|
models: {
|
|
proposal: this.members,
|
|
organisation: new ApplicantMembersModel(['list'])
|
|
}
|
|
}
|
|
)
|
|
);
|
|
|
|
this.members.list(this.pic,this.number)
|
|
.then(this.fillContributors.bind(this));
|
|
|
|
}
|
|
|
|
validate() {
|
|
let valid = true;
|
|
|
|
for(let chunk of this.chunks) {
|
|
let report = chunk.view.validate(true);
|
|
let badge = this.components.find(item => item.el.hasAttribute('eicbadge') && item.el.parentElement.dataset.target == chunk.target)
|
|
badge.value = report.issues;
|
|
|
|
valid = valid && report.valid;
|
|
}
|
|
this.statusTab.switchSubmit(!valid);
|
|
|
|
return valid;
|
|
}
|
|
|
|
refreshUpdated(data) {
|
|
this.find('.tools .updated .date').innerText = (new Date(data.date)).toLocaleString("en-DE");
|
|
this.find('.tools .updated .contributor').innerText = `${data.contributor.firstname} ${data.contributor.lastname}`;
|
|
}
|
|
|
|
save() {
|
|
this.validate();
|
|
this.submission.save(this.pic, this.number, this.form.value)
|
|
.then(
|
|
async payload => {
|
|
this.refreshUpdated(payload.updated);
|
|
this.dispatchUpdate();
|
|
},
|
|
async error => {
|
|
if(error.code == 504) {
|
|
ui.growl('Auto save encountered some issue.', 'danger');
|
|
this.setupMode('read');
|
|
} else {
|
|
ui.growl.append(error.displayMessage, 'danger');
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
async submit(event) {
|
|
if(this.validate()) {
|
|
if(this.autoSave) {
|
|
clearInterval(this.autoSave)
|
|
this.autoSave = null;
|
|
};
|
|
if(this.validationCheck) {
|
|
clearInterval(this.validationCheck);
|
|
this.validationCheck = null;
|
|
}
|
|
this.submission.submit(this.pic, this.number, this.form.value)
|
|
.then(async payload => {
|
|
|
|
this.dispatchUpdate();
|
|
this.mode = 'read'; // exiting edit mode, avoiding save on close
|
|
ui.growl.append('Your proposal has been successfully submitted', 'success');
|
|
this.unload();
|
|
|
|
},
|
|
() => {
|
|
if(!this.autoSave && this.mode == 'edit') {
|
|
this.autoSave = setInterval(this.save.bind(this), this.options.autoSaveInterval);
|
|
}
|
|
if(!this.validationCheck && this.mode == 'edit') {
|
|
this.validationCheck = setInterval(this.validate.bind(this), this.options.validationCheck);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async withdraw(event) {
|
|
this.submission.withdraw(this.pic, this.number, this.form.value)
|
|
.then(async payload => {
|
|
this.dispatchUpdate();
|
|
ui.growl.append('Your proposal has been successfully withdrawn', 'success');
|
|
this.unload();
|
|
|
|
});
|
|
}
|
|
|
|
async clone(event) {
|
|
this.submission.clone(this.pic, this.number)
|
|
.then(
|
|
async payload => {
|
|
this.dispatchUpdate();
|
|
ui.growl.append('A new proposal is available', 'success');
|
|
this.unload();
|
|
},
|
|
async () => {
|
|
ui.growl.append('Sorry, you don\'t have the rights to clone this proposal', 'danger')
|
|
}
|
|
);
|
|
}
|
|
|
|
async complain(event) {
|
|
|
|
let complaint = await this.openDialog(
|
|
await this.loadContent(
|
|
'projects/submissions/dialogs/SubmissionFormComplaintDialog',
|
|
{ title: 'File a complaint about the evaluation' }, {
|
|
pic: this.pic,
|
|
number: this.number,
|
|
proposal: this.submission
|
|
}
|
|
)
|
|
);
|
|
|
|
if(complaint) this.unload();
|
|
}
|
|
|
|
|
|
|
|
DOMContentResized() {
|
|
super.DOMContentResized();
|
|
|
|
let device = this.el.getAttribute('device');
|
|
let tabs = this.find('.tabs-extended');
|
|
|
|
switch(device) {
|
|
case 'desktop':
|
|
ui.show(tabs);
|
|
let current = tabs.querySelector('.tab-selected');
|
|
let part = current.getAttribute('data-target');
|
|
this.findAll('.content[data-part]').forEach(content => { if(content.getAttribute('data-part') != part) ui.hide(content) });
|
|
break;
|
|
default:
|
|
ui.hide(tabs);
|
|
this.findAll('.content[data-part]').forEach(content => { ui.show(content) })
|
|
break;
|
|
}
|
|
}
|
|
|
|
dispatchUpdate() {
|
|
/*
|
|
// MFA: if ever needed to perform local updates
|
|
let data = {
|
|
number: this.submission.data.proposal.number,
|
|
acronym: this.submission.data.proposal.acronym,
|
|
status: this.submission.data.status,
|
|
version: this.submission.data.version
|
|
}
|
|
*/
|
|
let data = null;
|
|
|
|
app.events.trigger('proposal_updated', data);
|
|
}
|
|
}
|
|
|
|
app.registerClass('SubmissionShortForm2023View', SubmissionShortForm2023View); |