json-object-editor
Version:
JOE the Json Object Editor | Platform Edition
505 lines (471 loc) • 23 kB
JavaScript
const { priority } = require("../fields/core");
var project = function(){return{
title : '${name}',
//info:'The main productiivity tool of JOE allow for tasks and projects to be assigned tracked and monitored as they pass through statuses and workflows. Use the <b>projectboard</b> app for a more focused view.',
info:"Within a project you can manage team members and goals, assign a task to a team member, etc.",
listView:{
title: function(proj){
return '<joe-fright>${RUN[_joe.SERVER.User.Render.cubes;${members};]}</joe-fright>'+
((proj.group && '<joe-subtext>'+_joe.getDataItemProp(proj.group,'group')+'</joe-subtext>') ||'')+
'<joe-title>${name}</joe-title>'
+'<joe-subtitle>${info}</joe-subtitle>';
},
listWindowTitle: 'Projects'
},
itemMenu:function(item){
var menu = [
{
/*name:'<joe-schema-icon title="standard report">'+_joe.schemas.report.menuicon+'</joe-schema-icon>',*/
name:_joe.schemas.report.menuicon,//+'<div class="joe-subtext">report</div>',
action:'window.open(\'/API/plugin/reportbuilder/standard?itemid='+item._id+'\')'}
];
var subtasks = _joe.Data.task.where({$and:[{project:item._id},{complete:{$in:[false,'false',undefined]}}]});
if(subtasks.length){
menu.push({name:'<div style="line-height:1;"><big>'+subtasks.length+'</big> <div class="joe-subtext">tasks</div>',
action:'goJoe(_joe.Data.task,{schema:\'task\',subset:\''+item.name+'\'})'});
}
return menu;
},
stripeColor:function(item){
if(item.priority && item.priority < 100){
return {
title:`P${item.priority}`,
color:_joe.Colors.priority[item.priority]
};
}
},
hideNumbers:true,
sorter:[{display:'priority',field:'priority'},'name','status'],
// Curated summary for agents
summary:{
description:'Container for organizing work, teams, and timelines.',
purpose:'Projects group tasks, people, and timelines. A project typically has one status and group, many members (users), and many tags. Tasks reference their owning project. Use projects for planning, tracking, and reporting across related tasks.',
labelField:'name',
defaultSort:{ field:'joeUpdated', dir:'desc' },
searchableFields:['name','info','description','problem_statement','_id'],
allowedSorts:['priority','status','name','joeUpdated','created','start_dt','end_dt'],
relationships:{
outbound:[
{ field:'status', targetSchema:'status', cardinality:'one' },
{ field:'group', targetSchema:'group', cardinality:'one' },
{ field:'members', targetSchema:'user', cardinality:'many' },
{ field:'tags', targetSchema:'tag', cardinality:'many' },
{ field:'aligned_goals', targetSchema:'goal', cardinality:'many' }
],
inbound:{ graphRef:'server/relationships.graph.json' }
},
joeManagedFields:['created','joeUpdated'],
fields:[
{ name:'_id', type:'string', required:true },
{ name:'itemtype', type:'string', required:true, const:'project' },
{ name:'name', type:'string', required:true },
{ name:'info', type:'string' },
{ name:'description', type:'string' },
{ name:'problem_statement', type:'string' },
{ name:'status', type:'string', isReference:true, targetSchema:'status' },
{ name:'group', type:'string', isReference:true, targetSchema:'group' },
{ name:'members', type:'string', isArray:true, isReference:true, targetSchema:'user' },
{ name:'tags', type:'string', isArray:true, isReference:true, targetSchema:'tag' },
{ name:'aligned_goals', type:'string', isArray:true, isReference:true, targetSchema:'goal' },
{ name:'goals', type:'objectList' },
{ name:'start_dt', type:'string', format:'date-time' },
{ name:'end_dt', type:'string', format:'date-time' },
{ name:'phases', type:'objectList' },
{ name:'subtasks', type:'objectList' },
{ name:'considerations_list', type:'objectList' },
{ name:'deliverables', type:'objectList' },
{ name:'key_features', type:'objectList' },
{ name:'marketing_tactics', type:'objectList' },
{ name:'links', type:'objectList' },
{ name:'pricing', type:'objectList' },
{ name:'files', type:'string', isArray:true },
{ name:'priority', type:'number', enumValues:[1,2,3,1000] },
{ name:'complete', type:'boolean' },
{ name:'joeUpdated', type:'string', format:'date-time', required:true },
{ name:'created', type:'string', format:'date-time', required:true }
]
},
fields:function(){
var fields=[
{sidebar_start:'left'},
{section_start:'ai'},
'objectChat',
'listConversations',
{name:'ai_summary',display:'AI Summary',type:'rendering',comment:'Summarize the project in a few sentences.',
'ai':{
label:'summarize with ai',
prompt:'Summarize the project in a few sentences.'
}
},
{section_end:'ai'},
{sidebar_end:'left'},
{section_start:'overview'},
'name',
{extend:'info',specs:{comment:'What\'s the problem solved or reason for this project?'}},
'description',
{name:'highlights',type:'objectList',
properties:['name'
],hideHeadings:true},
{section_end:'overview'},
{section_start:'objective'},
{name:'problem_statement', display:"Problem Statement",type:'wysiwyg'},
{name:'aligned_goals',display:'Aligned Goals',comment:'how does this latter up to bigger goals?',
type:'group',
//values:'goal',
values:function(i){
if(!i.group){
return _joe.Data.goal;
}else{
return _joe.Data.goal.where({group:i.group});
}
},
icon:'goal',blank:true},
{name:'goals',type:'objectList',
template:function(obj,subobj){
return '<joe-title >${name}</joe-title>'
+'<joe-subtext>${metric}</joe-subtext>';
},
properties:['name',{name:'metric',width:'40%'}]},
{section_end:'objective'},
{section_start:'timeline'},
{name:'start_dt',display:'kickoff',type:'date', native:true,width:'50%'},
{name:'end_dt',display:'completion',type:'date', native:true,width:'50%'},
{name:'phases',type:'objectList',
template:function(obj,subobj){
//var done = (subobj.sub_complete)?'joe-strike':'';
var t =
'<joe-title >${name} '+(subobj.hasOwnProperty('id') && '['+subobj.id+']'||'')+'</joe-title>'
+((subobj.start_date || subobj.end_date) && '<joe-subtext>${start_date} - ${end_date}</joe-subtext>'||'');
return t;
},
properties:[
{name:'name'},
{name:'id',width:'120px'},
{name:'start_date', display:'start', type:'date', native:true,width:'80px'},
{name:'end_date', display:'end', type:'date',native:true,width:'80px'}
],
hideHeadings:true},
{name:'subtasks',comment:'steps to be completed at the PO level',type:'objectList',
template:function(obj,subobj){
var done = (subobj.sub_complete)?'joe-strike':'';
return `<joe-full-right>${subobj.sub_duedate || ''} ${(subobj.milestone && `<b>[${subobj.milestone}]</b>`)||''}</joe-full-right>`
+'<joe-title class="'+done+'">${name}</joe-title>';
},
properties:[
{name:'name'},
{name:'milestone',width:'100px'},
{name:'sub_duedate', display:'due', type:'date',width:'50px',native:true},
{name:'sub_complete',display:'done',type:'boolean',width:'50px'}
]
},
{section_end:'timeline'},
{section_start:'thoughts',collapsed:function(i){
return !(i.notes || (i.considerations_list && i.considerations_list.length))
}},
{name:'considerations_list',display:'list',type:'objectList',properties:[
'name',
{name:'type',type:'select', width:'120px',values:['resource','dependency','assumption','risk','question','learning','opportunity','other','']}
]},
{label:'notes'},
{name:'notes',type:'wysiwyg',label:false},
//'notes',
{name:'addnote',label:false, type:'create',schema:'note'},
'references',
{section_end:'thoughts'},
{section_start:'deliverables',collapsed:function(i){
return !((i.key_features && i.key_features.length) || (i.deliverables && i.deliverables.length))
}},
{name:'deliverables',type:'objectList',properties:[
{name:'name'},
{name:'milestone',width:'100px',
// autocomplete:{text:true,properties:{
// }},values:function(p){
// return ['2021.1','2021.2'];
// }
}
]},
{name:'key_features',display:'key features',type:'objectList',properties:[
{name:'name'},
{name:'status',type:'select',values:['mvp','stretch','contingency','backlog','in-progress','complete',''],width:'100px'},
{name:'effort',type:'number',width:'60px'},
{name:'impact',type:'number',width:'60px'},
{name:'milestone',width:'100px'}
]},
{section_end:'deliverables'},
{section_start:'marketing',collapsed:function(i){
return !(i.marketing_tactics && i.marketing_tactics.length)
}},
{name:'marketing_tactics',display:'Communication Ideas',type:'objectList',properties:[
{name:'name'},
{name:'responsible',width:'200px'}
]},
{name:'links',type:'objectList',properties:[
{name:'name',width:'240px'},
{name:'url'}
]},
{section_end:'marketing'},
{section_start:'team',collapsed:function(i){
return (i.group && (i.members && i.members.length))
}},
'group',
{extend:'members',specs:{width:'50%'}},
'people',
{label:'comments'},
'user_comments',
{section_end:'team'},
{section_start:'cost',collapsed:function(i){
return !((i.pricing && i.pricing.length))
}},
{name:'pricing',type:'objectList',
template:'<joe-title>${line_item}</joe-title>',
properties:[
{name:'line_item',display:'line item'},
{name:'amount',type:'number',width:"120px"},
{name:'quantity',type:'number',width:"100px"},
{name:'paid',type:'date',width:"120px"},
{name:'invoice',type:'number',width:"100px"},
]
},
{section_end:'cost'},
// {name:'other',type:'subobject',value:{name:'no name'}},
{sidebar_start:'right'},
'status',
'priority',
'reports',
{name:'complete',display:'complete',type:'boolean',label:'completed projects are hidden'},
{section_start:'tasks', collapsed:true},
{name:'addtask',label:false, type:'create',schema:'task',
overwrites:function(item){return {project:item._id};}
},
'tasks',
{section_end:'tasks'},
{section_start:'tags', collapsed:function(i){
return (i.tags || i.tags.length)
}},
'tags',
{section_end:'tags'},
/*
{section_start:'related',collapsed:function(proj){
if((proj.notes && proj.notes.length) || (proj.references && proj.references.length)){
return false;
}
return true;
}},
{section_end:'related'},
*/
{section_start:'files',collapsed:function(item){
return !(item.files && item.files.length);
}},
{name:'files',type:'uploader',allowmultiple:true, height:'300px',comment:'drag files here to upload', onConfirm:_joe.SERVER.Plugins.awsFileUpload},
{section_end:'files'},
{section_start:'access',collapsed:true},
'_protected',
{section_end:'access'},
{sidebar_end:'right'},
{section_start:'charts'},
{name:'task_breakdown',display:'task breakdown',type:'content',run:function(item){
var html ='<div id="task_breakdown_holder"></div>'
html+='<script>_joe.schemas.project.methods.renderTaskBreakdown("'+item._id+'");</script>';
return html;
}},
{section_end:'charts'},
{section_start:'system',collapsed:true},
'_id','created','itemtype',
{section_end:'system'},
]
return fields;
},
subsets:function(){
var sets = [
{name:'My Projects',default:true,filter:{complete:{$nin:[true]},members:{$in:[_joe.User._id]}}},
{name:'Completed',filter:{complete:{$in:[true]}}},
{group_start:'groups',collapsed:false}
];
_joe.Data.group.sortBy('name').map(function(g){
sets.push({name:g.name,filter:{group:g._id}});
})
sets.push({group_end:'groups'});
return sets;
},
filters:function(){
var colors = [{},
{stripecolor:'#cc4500'},{stripecolor:'#FFee33'},{stripecolor:'#acf'}]
var stats = [].concat(
_joe.Filter.Options.status({group:'statuses',collapsed:true}),
_joe.Filter.Options.tags({group:'tags',collapsed:true}),
);
stats.push({group_start:'goals',collapsed:true})
_joe.Data.goal.sortBy('!priority','code').map(function(g){
let color;
stats.push({
name:(g.code||g.name),
filter:{aligned_goals:{$in:[g._id]}},
stripecolor:colors[(g.priority|| 0)].stripecolor
});
})
stats.push({name:'none',filter:{aligned_goals:{$size: 0}}});
stats.push({group_end:'goals'});
return stats;
},
idprop : "_id",
bgColor:function(item){
if(!item.status){
return '';
}
var status = _joe.getDataItem(item.status,'status');
return {title:status.name,color:status.color}
},
methods:{
renderTaskBreakdown:function(projectid,target){
var target = target ||'#task_breakdown_holder';
var project = _joe.getDataItem(projectid,'project');
capp && capp.Chart.byStatus('task',null,{
delay:true,
target:target,
filter:{project:projectid,complete:{$nin:['true',true]}},
onclick:function (d, i) {
goJoe(_joe.getDataset('task')||[],{schema:'task',subset:project.name});
},
});
}
},
onDemandExpander:true,
itemExpander:function(item){
var html = '';
if(item.goals && item.goals.length){
html += '<joe-title>Goals</joe-title>';
item.goals.map(function(goal){
html+='<joe-itemmenu-section class="padded"><joe-title>'+goal.name+'</joe-title>\
<joe-subtitle>'+goal.metric+'</joe-subtitle></joe-itemmenu-section>';
})
html+='';
}
var tasks = _joe.Data.task
.where({$and:[{project:item._id},{complete:{$in:[false,'false',undefined]}}]})
.sortBy('project_phase,priority');
html+='<joe-title>'+tasks.length+' Tasks</joe-title>';
var sectionname = null;
var newname;
tasks.map(function(task){
if(sectionname != task.project_phase){
if(sectionname !== null){
html+='</joe-itemmenu-section>';
}
sectionname = task.project_phase;
html+='<joe-itemmenu-section data-secname="'+sectionname+'"> <joe-subtitle>'+(item.phases.where({id:sectionname})[0]||{name:'---'}).name+'</joe-subtitle>';
}
html+=_joe.renderFieldListItem(task,'<joe-title>${name}</joe-title><joe-subtitle>${info}</joe-subtitle>\
<joe-fright>P${priority}</joe-fright>','task');
//item,contentTemplate,schema,specs
});
if(sectionname !== null){
html+='</joe-itemmenu-section>';
}
return html;
},
menuicon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="-256 -256 1536 1536"><path d="M896 193H640v-66c0-34.2-27.8-62-62-62H446c-34.2 0-62 27.8-62 62v66H128c-35.3 0-64 28.7-64 64v512c0 35.3 28.7 64 64 64h768c35.3 0 64-28.7 64-64V257c0-35.3-28.7-64-64-64zm-448-48c0-8.8 7.2-16 16-16h96c8.8 0 16 7.2 16 16v48H448v-48zm448 368H576v64H448v-64H128V257h64v192h640V257h64v256z"></path></svg>',
report:{
name:'Project Standard',
id:'standard',
template:function(item,template_data){
var temp = "<report-section>\
<report-section-label>Details</report-section-label>\
${if ('${this.PROJECT.description}')}\
<report-section-sublabel>Description</report-section-sublabel>\
${this.PROJECT.description}\
${end if}\
<report-section-sublabel>Goals</report-section-sublabel>\
${foreach ${item} in ${this.PROJECT.goals}}\
<joe-title>${item.name}</joe-title>\
<joe-subtitle>${item.metric}</joe-subtitle>\
${end foreach}\
</report-section>\
<report-section>\
<report-section-label>Charts</report-section-label>";
var complete=0;
var incomplete=0;
var tasks = JOE.Cache.search({itemtype:'task',project:item._id});
var users ={};
var status = {};
tasks.map(function(task){
if(task.complete){
complete++;
}else if(task.status && task.status != undefined){
incomplete++;
status[task.status] = status[task.status] || 0;
status[task.status]++;
}else{
incomplete++;
status['none'] = status['none'] || 0;
status['none']++;
}
})
temp+='<report-section-sublabel>'+Math.round(complete/tasks.length*100)+'% of '+tasks.length+' tasks completed. </report-section-sublabel>';
temp+="<div id='chart'></div>\
<script>\
var chart = c3.generate({\
bindto: '#chart',\
legend: {\
position: 'right'\
},\
data: {\
columns: [\
['complete "+complete+"', "+complete+"],"
for(var s in status){
var sts = JOE.Cache.findByID('status',s)||{name:'no status'};
temp+= "['"+sts.name+" "+status[s]+" ',"+status[s]+"],";
}
temp+=" ],\
type : 'pie'\
}\
});\
</script>\
</report-section>\
<report-section name='tasks'>\
<a name='tasks'></a>\
${script}\
var tasks = JOE.Cache.search({itemtype:'task',complete:{$in:[false,'false']},project:'${this.PROJECT._id}'});\
tasks.sortBy('priority');\
var project = JOE.Cache.findByID('project','${this.PROJECT._id}');\
var h = '<report-section-label>'+tasks.length+' Tasks</report-section-label>';\
function printTask(task){\
var ht = '';\
var action = '/API/plugin/reportbuilder/standard?itemid='+task._id;\
ht+='<a class=\"report-content-item\" href=\"'+action+'\">"
+"<joe-title>'+task.name+'</joe-title><joe-subtitle>'+task.info+'</joe-subtitle></a>';\
return ht;\
}\
if(project.phases && project.phases.length){\
project.phases.map(function(phase){\
h+='<report-section-sublabel>'+phase.name+' '+(phase.id && ('['+phase.id+']') || '')+'</report-section-sublabel>';\
tasks.map(function(task){ \
if(task.project_phase == phase.id){\
h+=printTask(task);\
}\
});\
});\
h += '<report-section-sublabel>unphased</report-section-sublabel>';\
tasks.map(function(task){ \
if(task.project_phase !== 0 && !task.project_phase){\
h+=printTask(task);\
}\
});\
}\
else{\
var priority = '';\
tasks.sortBy('priority').map(function(task){ \
if(task.priority != priority){\
h+='<report-section-sublabel>priority '+task.priority+'</report-section-sublabel>';\
priority = task.priority;\
}\
h+=printTask(task);\
});\
}\
return h;\
${end script}\
</report-section>";
return temp;
}
}
}};
module.exports = project();