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