diff --git a/icons/stats.png b/icons/stats.png new file mode 100644 index 0000000..797bd9e Binary files /dev/null and b/icons/stats.png differ diff --git a/index-dev.html b/index-dev.html index 6a68040..8445f47 100644 --- a/index-dev.html +++ b/index-dev.html @@ -1,47 +1,48 @@ Cost Calculator +
diff --git a/js/costcalc_export.jsx b/js/costcalc_export.jsx index b63aef4..370f1d3 100644 --- a/js/costcalc_export.jsx +++ b/js/costcalc_export.jsx @@ -1,368 +1,372 @@ "use strict"; // This code manages the export part of the engine class ManageExport extends React.Component { constructor(props) { super(props); this.make_export = this.make_export.bind(this); this.make_copy = this.make_copy.bind(this); this.rmvempty = this.rmvempty.bind(this); this.hide = this.hide.bind(this); this.colsdef=["Category","Provider","Name","Comments","Options","Cost"]; this.colconv="Cost"; this.state={ output:'', disp:false, rmvempty:false, typexp:"", ConvEnable:this.props.conv.Enable, }; this.cols=this.colsdef.slice(0); this.colsconv=this.cols.slice(0); this.colsconv.push(this.colconv); } render() { if(this.props.conv.Enable){ this.cols=this.colsconv; }else{ this.cols=this.colsdef;} let opt=""; let output=""; if (this.state.disp){ opt=this.options_btn(); output=this.make_output(this.props.data.data); } return (
{this.export_btn()}
{output}
{opt}
); } export_btn(){ return(
) } options_btn(opt){ let Namermv=""; if (this.state.rmvempty){ Namermv="Display Empty Lines"; }else { Namermv="Remove Empty Lines"; } return(
); } rmvempty(){ this.setState({rmvempty:!this.state.rmvempty}); // this.make_export(this.typexp) } hide(){ this.setState({disp:false}) } make_copy(){ this.fnSelect("export-output"); document.execCommand("copy"); this.fnDeSelect() alert("Copied"); + Stats.RecordEvent('Export',"clipboard",0); } make_output(rdata){ const data=this.read_export(rdata,this.state.typexp); const hcol=this.cols; + //Generate a stat export + Stats.RecordEvent('Export',this.state.typexp,0); + switch(this.state.typexp){ case 'html': return this.htmlout(hcol,data); case 'htmlsrc': return this.htmlsrcout(hcol,data); case 'mark': return this.markout(hcol,data); case 'csv': return this.csvout(hcol,data); } } make_export(typ){ this.setState({typexp:typ}); this.setState({disp:true}); } htmlout(hcol,data){ let disps=''; let Convcol=null; let movecol=3; if(projectduration>1) disps='s'; if(this.props.conv.Enable){ movecol=4; Convcol={ConvCurrency(this.props.data.total)}; } return(
{this.makecol(hcol,'html',true)} {this.htmltable(data)} {Convcol}
{projectname} {projectduration} year{disps} Total Cost {this.props.data.total}
); } htmlsrcout(hcol,data){ let disps=''; if(projectduration>1) disps='s'; return(

                     <table>
<thead>
{this.makecol(hcol,'htmlsrc',true)} </thead>
<tbody>
{this.htmlsrctable(data)} <tr >
<td>{projectname}</td> <td>{projectduration} year{disps}</td> <td colSpan={hcol.length-3} align="right"><strong>Total Cost</strong></td><td align="center"><strong> {this.props.data.total}</strong></td>
</tr>
</tbody>
</table>
); } markout(hcol,data){ let disps=''; if(projectduration>1) disps='s'; const Head=Array.from({length: this.cols.length}, (v, k) => "---"); const col=Array.from({length: hcol.length-3}, (v, k) => "| "); return(

                     |{this.makecol(hcol,'mark',true)}
|{this.makecol(Head,'mark')}
{this.marktable(data)} |{projectname}|{projectduration} year{disps}{col} Total Cost |{this.props.data.total}|
); } csvout(hcol,data){ let disps=''; if(projectduration>1) disps='s'; const col=Array.from({length: hcol.length-3}, (v, k) => ","); return(

                     {this.makecol(hcol,'csv',true)}
{this.csvtable(data)} {projectname},{projectduration} year{disps}{col} Total Cost ,{this.props.data.total},
); } makecol(cols,style,head){ let children = []; switch(style) { case 'html': for (let j = 0; j < cols.length; j++) { if(head){ children.push({cols[j]}); }else { children.push({cols[j]}); } } return children; case 'htmlsrc': for (let j = 0; j < cols.length; j++) { if(head){ children.push(<th> {cols[j]} </th>); }else { children.push(<td> {cols[j]} </td>); } } return children; case 'mark': for (let j = 0; j < cols.length; j++) { children.push( {cols[j]}|); } return children case 'csv': for (let j = 0; j < cols.length; j++) { children.push( {cols[j]},); } return children; } } htmltable(data){ var items=[]; for(let i=0;i{children}); } return items; } htmlsrctable(data){ var items=[]; for(let i=0;i<tr> {children} </tr>
); } return items; } marktable(data){ var items=[]; for(let i=0;i|{children}
); } return items; } csvtable(data){ var items=[]; for(let i=0;i{children}
); } return items; } read_export(rawexport,n) { var output = []; for (let cat = 0; cat < rawexport.length; cat++) { const state = rawexport[cat]; for (let mod = 0; mod < state.length; mod++) { if((!this.state.rmvempty) || (state[mod].Provider!=='')){ let tmp ={}; tmp={ Category:state[mod].Category, Provider: state[mod].Provider, Name: state[mod].Name, Comments: state[mod].Comments, Options: this.read_options(state[mod].ExportCmp), Cost: state[mod].Cost, }; if(this.props.conv.Enable) tmp["Cost2"]=ConvCurrency(state[mod].Cost); output.push(tmp); }}} return output; } read_options(Options){ let output=[]; for (let i = 0; i < Options.length; i++) { switch(this.state.typexp) { case 'html': output.push({Options[i].Name} : {Options[i].Value} ); break; case 'htmlsrc': output.push(<b>{Options[i].Name}</b> : <i>{Options[i].Value}</i> ); break; case 'mark': output.push(__{Options[i].Name}__ : *{Options[i].Value}* ); break; } if(i< Options.length-1){ output.push(
) } } return output } fnSelect(objId){ this.fnDeSelect(); if (document.selection){ var range = document.body.createTextRange(); range.moveToElementText(document.getElementById(objId)); range.select(); }else if (window.getSelection){ var range = document.createRange(); range.selectNode(document.getElementById(objId)); window.getSelection().addRange(range); } } fnDeSelect(){ if (document.selection) document.selection.empty(); else if (window.getSelection) window.getSelection().removeAllRanges(); } } diff --git a/js/costcalc_main.jsx b/js/costcalc_main.jsx index 15a50ac..41eb1c1 100644 --- a/js/costcalc_main.jsx +++ b/js/costcalc_main.jsx @@ -1,1667 +1,1676 @@ "use strict"; var projectname=''; var projectduration=0; // Functions Tools // --------------------- // --------------------- //function loop for react js function Repeat(props) { let items = []; for (let i = 0; i < props.numTimes; i++) { items.push(props.children(i)); } return
{items}
; } //convert string to numeric function tonumeric (value) { return parseFloat( value.toString().replace(/[^0-9\.]+/g, '') ); } // Covert numeric to money string function tomoney(numeric,currency) { if (typeof numeric == 'string') { numeric = parseFloat(numeric); } let strcur=''; if(currency===undefined){ strcur=MainData.Currency; } else { strcur=currency; } return numeric.toFixed(0).replace(/(\d)(?=(\d{3})+\.)/g, '$1,') + ' '+strcur; } // return the sum of an array function sum(obj) { const val=Object.values(obj); var total = 0; for (var i = 0; i < val.length; i++) { total = total + tonumeric(val[i]); } return total; } // Comapare two obj return true is similar Object.compare = function (obj1, obj2) { //Loop through properties in object 1 for (var p in obj1) { //Check property exists on both objects if (obj1.hasOwnProperty(p) !== obj2.hasOwnProperty(p)) return false; switch (typeof (obj1[p])) { //Deep compare objects case 'object': if (!Object.compare(obj1[p], obj2[p])) return false; break; //Compare function code case 'function': if (typeof (obj2[p]) == 'undefined' || (p != 'compare' && obj1[p].toString() != obj2[p].toString())) return false; break; //Compare values default: if (obj1[p] != obj2[p]) return false; } } //Check object 2 for any extra properties for (var p in obj2) { if (typeof (obj1[p]) == 'undefined') return false; } return true; }; // Generate a random int function randomint(not){ var rnd; do { rnd=Math.floor(Math.random() * 100); var cont=false; for (let i = 0; i < not.length ; i++) { if (not[i]===rnd){ cont=true; } } } while(cont); return rnd; } // Inputs Definition // --------------------- // --------------------- // Display the amount selector class AmountInput extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); } handleChange(e) { this.props.onChange(e.target.value); } componentDidMount() { $('[data-toggle="tooltip"]').tooltip(); } componentDidUpdate() { $('[data-toggle="tooltip"]').tooltip(); } componentWillUnmount() { $('[data-toggle="tooltip"]').tooltip('dispose'); } render() { const value = this.props.value; let label=null; if(this.props.name!=null && this.props.name!==""){ label=; } return (
{label} {this.props.name} : {value} {this.props.unit}
); } } // Display a select input box class SelectorInput extends React.Component { constructor(props) { super(props); // this.state={listoptions:this.makelist(props.options)}; this.handleChange = this.handleChange.bind(this); } rate(i){ return i; } makelist(data){ var listoptions=[]; for (var i = 0; i < data.length; i++) { listoptions.push(); } return listoptions; } handleChange(select) { this.props.onChange(select.target.value); } makerate(){ if (this.props.rate!=null && this.props.rate!==""){ return(
Rate : {this.props.rate} {this.props.unit}
);} } maketitle(title){ const maxstr=20 if (title.length>maxstr){ title=title.substr(0,maxstr)+"..."; } return title; } componentDidMount() { $('[data-toggle="tooltip"]').tooltip(); } componentDidUpdate() { $('[data-toggle="tooltip"]').tooltip(); } componentWillUnmount() { $('[data-toggle="tooltip"]').tooltip('dispose'); } render() { let label=null; if(this.props.name!=null && this.props.name!==""){ label=
; } return (
{label} {this.makerate()}
) } } // Make the know more button class MakeknowmoreInput extends React.Component { constructor(props) { super(props); this.state={btnsize:20} } componentDidMount() { $('[data-toggle="tooltip"]').tooltip(); } componentDidUpdate() { $('[data-toggle="tooltip"]').tooltip(); } componentWillUnmount() { $('[data-toggle="tooltip"]').tooltip('dispose'); } render() { const data = this.props.data; if (((data.Url !== '') )) { // if (data.Url.length==1){ // return( // } url={data.Url[0].Url} // id="btn-plugin-knowmore" // class="btn-primary btn-sm" tips={"Know more about " + data.Name}/> // ); // }else { return (} options={data.Url} id="btn-plugin-knowmore" class="btn-primary btn-sm" tips={"Know more about " + data.Name}/>); //} } else{ return null } } } // Display a checkbox class CheckboxInput extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state={checked:this.props.defaults}; } handleChange() { this.setState({checked: !this.state.checked}); this.props.onChange(!this.state.checked); } render() { return (
); } } // Display a btn with link class ButtonHrefInput extends React.Component { constructor(props) { super(props); } componentDidMount() { $('[data-toggle="tooltip"]').tooltip(); } componentDidUpdate() { $('[data-toggle="tooltip"]').tooltip(); } render() { return ( {this.props.name} ); } } // Button with validation popup class ButtonInputWpop extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state={target:"Modal"+this.props.idp}; } handleChange() { const out={n:this.props.n,target:this.state.target}; this.props.onClick(out); } componentDidMount() { $('[data-toggle="tooltip"]').tooltip(); } componentDidUpdate() { $('[data-toggle="tooltip"]').tooltip(); } componentWillUnmount() { $('[data-toggle="tooltip"]').tooltip('dispose'); } render() { return ( ); } } // Display a button class ButtonInput extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); } handleChange() { this.props.onClick(this.props.n); } componentDidMount() { $('[data-toggle="tooltip"]').tooltip(); } componentDidUpdate() { $('[data-toggle="tooltip"]').tooltip(); } componentWillUnmount() { $('[data-toggle="tooltip"]').tooltip('dispose'); } render() { return ( ); } } // Display a menu class MenuInput extends React.Component { constructor(props) { super(props); this.state={listoptions:this.makelist(props.options)}; } componentDidMount() { $('[data-toggle="tooltip"]').tooltip(); } componentDidUpdate() { $('[data-toggle="tooltip"]').tooltip(); } componentWillUnmount() { $('[data-toggle="tooltip"]').tooltip('dispose'); } makelist(data){ var listoptions=[]; for (var i = 0; i < data.length; i++) { listoptions.push({data[i].Name}); } return listoptions; } render() { return (
{this.state.listoptions}
); } } // Text input box class TxtInput extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); } handleChange(e) { this.props.onChange(e.target.value); } componentDidMount() { $('[data-toggle="tooltip"]').tooltip(); } componentDidUpdate() { $('[data-toggle="tooltip"]').tooltip(); } componentWillUnmount() { $('[data-toggle="tooltip"]').tooltip('dispose'); } render() { let info=null ; if(this.props.info != null && this.props.info !== "") info={this.props.info} ; return (
{this.props.Prepend}
{info}
{this.props.InvalidMessage}
); } } // Outputs definition // --------------------- // --------------------- // Display the cost output box class CostOutput extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); } handleChange() { this.props.onCostChange(this.props.display); } componentDidMount() { $('[data-toggle="tooltip"]').tooltip(); } componentDidUpdate() { $('[data-toggle="tooltip"]').tooltip(); } render() { const classN="form-control "+this.props.class; return (
); } } // Display a text box for display function Textoutput(props){ return(
{props.text}
); } // Plugins definition // --------------------- // --------------------- class AmountRatesCost extends React.Component { constructor(props) { super(props); this.handleAmountChange = this.handleAmountChange.bind(this); this.handleRateChange = this.handleRateChange.bind(this); this.state={ Amount : 1, SelectRate : 0 , Rate : this.props.data.Rates[Object.keys(this.props.data.Rates)[0]], Adaptive:false, }; if(typeof this.props.data.Adaptive!=='undefined' && this.props.data.Adaptive===true){ this.state.Adaptive=true; } this.make_export(); } handleAmountChange(amount) { this.setState({Amount: amount}); } handleRateChange(select) { this.setState({SelectRate: select}); this.setState({Rate: this.props.data.Rates[Object.keys(this.props.data.Rates)[select]]}); } make_export(){ this.export=[ {Name:"Amount",Value:this.state.Amount+" "+this.props.data.AmountUnit}, {Name:this.props.data.RateName,Value:Object.keys(this.props.data.Rates)[this.state.SelectRate]} ]; this.props.export(this.export); } componentDidUpdate(){ this.makecost(this.state.Amount,this.state.Rate); this.make_export(); } render() { let Amount_min; let Amount_max; let Amount_stp; if(this.state.Adaptive){ Amount_min=this.props.data.AmountMin[this.state.SelectRate]; Amount_max=this.props.data.AmountMax[this.state.SelectRate]; Amount_stp=this.props.data.AmountStep[this.state.SelectRate]; } else { Amount_min=this.props.data.AmountMin; Amount_max=this.props.data.AmountMax; Amount_stp=this.props.data.AmountStep; } if(this.state.Amount>Amount_max){ this.state.Amount=Amount_max; } if(this.state.Amount
); } makecost(amount,rate) { let free; if(this.state.Adaptive){ free=this.props.data.AmountFree[this.state.SelectRate]; } else { free=this.props.data.AmountFree; } let total = (amount - free) * rate; if(this.props.data.ByYear) total=total*projectduration; total=tomoney(total); this.props.onCostChange(this.props.n,total); return total; } } class CategoryAmountRatesCost extends React.Component { constructor(props) { super(props); this.handleCatChange = this.handleCatChange.bind(this); this.handleAmountChange = this.handleAmountChange.bind(this); this.handleRateChange = this.handleRateChange.bind(this); this.state={ SelectCat : 0, Cat : this.props.data.Cat[Object.keys(this.props.data.Cat)[0]], Amount : 1, SelectRate : 0 , Rate : this.props.data.Rates[Object.keys(this.props.data.Rates)[0]], Adaptive: false, }; if(typeof this.props.data.Adaptive!=='undefined' && this.props.data.Adaptive===true){ this.state.Adaptive=true; } this.make_export(); } handleAmountChange(amount) { this.setState({Amount: amount}); } handleRateChange(select) { this.setState({SelectRate: select}); this.setState({Rate: this.props.data.Rates[Object.keys(this.props.data.Rates)[select]]}); } handleCatChange(select) { this.setState({SelectCat: select}); this.setState({Cat: this.props.data.Cat[Object.keys(this.props.data.Cat)[select]]}); } make_export(){ this.export=[ {Name:this.props.data.CatName,Value:Object.keys(this.props.data.Cat)[this.state.SelectCat]}, {Name:"Amount",Value:this.state.Amount+" "+this.props.data.AmountUnit}, {Name:this.props.data.RateName,Value:Object.keys(this.props.data.Rates)[this.state.SelectRate]} ]; this.props.export(this.export); } componentDidUpdate(){ this.makecost(this.state.Cat,this.state.Amount,this.state.Rate); this.make_export(); } render() { let Amount_min; let Amount_max; let Amount_stp; if(this.state.Adaptive){ Amount_min=this.props.data.AmountMin[this.state.SelectRate]; Amount_max=this.props.data.AmountMax[this.state.SelectRate]; Amount_stp=this.props.data.AmountStep[this.state.SelectRate]; } else { Amount_min=this.props.data.AmountMin; Amount_max=this.props.data.AmountMax; Amount_stp=this.props.data.AmountStep; } if(this.state.Amount>Amount_max){ this.state.Amount=Amount_max; } if(this.state.Amount
); } makecost(cat,amount,rate) { let free; if(this.state.Adaptive){ free=this.props.data.AmountFree[this.state.SelectRate]; } else { free=this.props.data.AmountFree; } var total=cat+(amount-free)*rate; if(this.props.data.ByYear) total=total*projectduration; total=tomoney(total); this.props.onCostChange(this.props.n,total); return total; } } class CategoryCost extends React.Component { constructor(props) { super(props); this.handleCatChange = this.handleCatChange.bind(this); this.state={SelectCat : 0, Cat : this.props.data.Cat[Object.keys(this.props.data.Cat)[0]] }; this.make_export(); } handleCatChange(select) { this.setState({SelectCat: select}); this.setState({Cat: this.props.data.Cat[Object.keys(this.props.data.Cat)[select]]}); } make_export(){ this.export=[ {Name:this.props.data.CatName,Value:Object.keys(this.props.data.Cat)[this.state.SelectCat]}, ]; this.props.export(this.export); } componentDidUpdate(){ this.makecost(this.state.Cat); this.make_export(); } render() { return (
); } makecost(cat) { var total=cat; if(this.props.data.ByYear) total=total*projectduration; total=tomoney(total); this.props.onCostChange(this.props.n,total); return total; } } class NoneSelect extends React.Component { constructor(props) { super(props); this.export=[] } render() { const Cost=tomoney(0); this.props.onCostChange(this.props.n,Cost); this.props.export(this.export); return (
Please select a provider in the list.
); } } class UserCost extends React.Component { constructor(props) { super(props); this.handleCostChange = this.handleCostChange.bind(this); this.handleProviderChange = this.handleProviderChange.bind(this); this.handleServiceChange = this.handleServiceChange.bind(this); this.handleYearChange = this.handleYearChange.bind(this); this.handleConvMoneyChange = this.handleConvMoneyChange.bind(this); this.state={ total:0, value:0, ProviderError:true, ServiceError:true, ByYear:false, }; this.export=[]; } handleYearChange(state){ this.setState({ByYear: state}); this.props.handlebyYearChange(state); } makecost(byYear,amount){ let total=amount; if(byYear) total=amount*projectduration; // this.setState({total:total}); this.props.onCostChange(this.props.n,tomoney(total)); } handleCostChange(value){ this.setState({value:value}); } handleProviderChange(txt){ this.props.handleProviderChange(txt); if(txt ===''){ this.setState({ProviderError: true}); } else { this.setState({ProviderError: false}); } } handleServiceChange(txt){ this.props.handleServiceChange(txt); if(txt ===''){ this.setState({ServiceError: true}); } else { this.setState({ServiceError: false}); } } handleConvMoneyChange(conv){ this.setState({conv:conv}); } componentDidUpdate(){ this.makecost(this.state.ByYear,this.state.value); // this.make_export(); } classtxt(error){ if(error){ return "is-invalid"; } else { return "is-valid"; } } render() { let Costname="Cost"; if(this.state.ByYear) Costname="Cost by year"; this.props.export(this.export); return (
); } } // Combine plugins // --------------------- // --------------------- // Manages what's display inside a plugin : provider selector, select the components... class ProviderPluginsSelector extends React.Component { constructor(props) { super(props); this.handleCostChange = this.handleCostChange.bind(this); this.handleProviderChange = this.handleProviderChange.bind(this); this.handleCommentChange = this.handleCommentChange.bind(this); this.handleAddPlugin = this.handleAddPlugin.bind(this); this.handleRmvPlugin = this.handleRmvPlugin.bind(this); this.handleProviderChangetxt = this.handleProviderChangetxt.bind(this); this.handleServiceChangetxt = this.handleServiceChangetxt.bind(this); this.handlebyYearChange = this.handlebyYearChange.bind(this); this.make_exportcmp = this.make_exportcmp.bind(this); this.make_export = this.make_export.bind(this); this.state={ selected:0, keys:this.ProvidersName(props.data), n:1, cost:0, comments:"", Provider:"", Name:"", manualname:false, manbyyear:false, show_plus:false, exportcmp:"", }; } handleCostChange(n,e) { if (this.state.cost !== e ) { this.setState({cost: e}); this.props.handleCostChange(n,e); } } handleProviderChange(select){ this.setState({selected:select}); if (select>0){ this.setState({show_plus:true}); }else { this.setState({show_plus:false}); } this.state.Provider=this.props.data.Data[select].Provider; this.state.Name=this.props.data.Data[select].Name; this.props.handleCostChange(this.props.n,this.state.cost); + // Send a provider even when provider change + Stats.RecordEvent('Provider',this.state.Provider,0); + } componentDidUpdate(){ this.props.handleCostChange(this.props.n,this.state.cost); this.make_export(); } make_exportcmp(data){ this.state.exportcmp=data } make_export(){ const out={ Category:this.props.data.Name, Provider:this.state.Provider, Name:this.state.Name, Comments:this.state.comments, ExportCmp:this.state.exportcmp, Cost:this.state.cost, }; this.props.export(out,this.props.n) } handleCommentChange(com){ this.setState({comments:com}); } handleAddPlugin(n){ this.props.handleAddPlugin(n); } handleRmvPlugin(n){ this.props.handleRmvPlugin(n); } // The 3 nexts function are for user input management handleProviderChangetxt(txt){ this.setState({Provider:txt}); } handleServiceChangetxt(txt){ this.setState({Name:txt}); } handlebyYearChange(state){ this.setState({manbyyear:state}); } // Manage extra display info for a selected provider extrainfo(Cdata){ let Extra_inf=""; let Extra_infUrl=""; if ( typeof Cdata.ExtraInfoUrl !=='undefined' && Cdata.ExtraInfoUrl !==''){ Extra_infUrl=

To know more

; } if ( typeof Cdata.ExtraInfo !=='undefined' && Cdata.ExtraInfo !==''){ Extra_inf=
  {Cdata.ExtraInfo} {Extra_infUrl}
; } return(Extra_inf); } render() { const selected=this.state.selected; this.state.manualname=false; this.state.keys=this.ProvidersName(this.props.data); const Cmp=this.cmp2string(this.cmpdata(selected).Style); const Cdata=this.cmpdata(selected); const id=this.props.data.Name.replace(/\s/g,'')+this.props.n; return(
{this.extrainfo(Cdata)}
); } cmpdata(select){ let out=this.props.data.Data[select]; if (this.state.manualname){ out.Name=this.state.Name; out.ByYear=this.state.manbyyear; if ( this.state.Provider ==='') { this.state.keys[select] = 'Please provide a Provider'; }else { this.state.keys[select]=this.state.Provider; } } return out; } // return the correct style fct from the str input cmp2string(str){ switch (str) { case "AmountRatesCost" : return AmountRatesCost; case "CategoryCost" : return CategoryCost; case "CategoryAmountRatesCost" : return CategoryAmountRatesCost; case "NoneSelect":return NoneSelect; case "UserCost":{this.state.manualname=true; return UserCost;} } } ProvidersName(main){ const data = main.Data; var providers=[]; for (var i = 0; i < data.length; i++) { providers.push(data[i].Provider); } return providers; } } // Displays the header of a plugin (button +- name cost ...) class ModuleHeader extends React.Component{ constructor(props) { super(props); this.handleAddPlugin = this.handleAddPlugin.bind(this); this.handleRmvPlugin = this.handleRmvPlugin.bind(this); } handleAddPlugin(n){ this.props.handleAddPlugin(n); } handleRmvPlugin(n){ this.props.handleRmvPlugin(n); } byyear(by){ if(by){ return({projectduration}
years
); } else { return(); } } render() { let minus=null; let plus=null; let convout=null; if (this.props.show_minus){ minus=} onClick={this.handleRmvPlugin} n={this.props.n} tips="Remove this line" idp={this.props.id} info={this.props.data.Name}/>; } if (this.props.show_plus){ plus= } onClick={this.handleAddPlugin} n={this.props.n} tips={"Add a new "+this.props.data.Name}/>; } if (this.props.conv.Enable) { convout=; } return(
{plus}
{minus}
{/*
*/} {/*
*/} {/*
*/} {/*
*/}
{this.makeinfo(this.props.keys,this.props.selected,this.props.Cdata)}
{this.props.comments}
{convout}
{this.byyear(this.props.Cdata.ByYear)}
); } + btnClick(){ + // Send a category even when someone click on the category btn + Stats.RecordEvent('Category',this.props.data.Name,this.props.n); + } makeinfo(keys,selected,Cdata){ let name=Cdata.Name; if ( name ===''&&keys[selected]===''){ name='Please provide a Provider'; return ({name}); }else if(keys[selected]==='None'){ return ({name}); }else{ return ({keys[selected]} : {name}); } } } //displays one kind plugin it manages the add and removes option class ManagePlugins extends React.Component{ constructor(props) { super(props); this.handleCostChange = this.handleCostChange.bind(this); this.handleAddPlugin = this.handleAddPlugin.bind(this); this.handleRmvPlugin = this.handleRmvPlugin.bind(this); this.make_exportplug=this.make_exportplug.bind(this); this.make_export = this.make_export.bind(this); this.state={ displayed:[], varsum:{}, plugins:[], export:[], }; this.state.displayed.push(randomint(this.state.displayed)); } handleRmvPlugin(n){ $('#'+n.target).modal('hide'); var tmp=this.state.displayed; tmp.splice(n.n,1); this.setState({displayed:tmp}); this.handleCostChange(n.n,0); } handleAddPlugin(n){ var tmp=this.state.displayed; tmp.splice(n+1,0,randomint(this.state.displayed)); this.setState({displayed:tmp}); } handleCostChange(n,cost) { this.state.varsum[n]=cost; this.props.handleCostChange(this.props.n,sum(this.state.varsum)); } make_exportplug(data,n) { this.state.export[n] = data; this.make_export() } make_export(){ if (this.state.export.length === this.give_n()) { this.props.export(this.state.export, this.props.n) } } give_id(index){ return this.state.displayed[index] } give_n(){ const disp=this.state.displayed; return disp.length } componentDidUpdate(){ this.make_export(); } render() { let show_minus = false; if (this.give_n()>1) { show_minus=true; } this.make_export(); return(
{(index) => }
);} } // displays all the plugins defined in the Maindata class PluginsMain extends React.Component { constructor(props) { super(props); this.handleCostChange = this.handleCostChange.bind(this); this.make_exportplug=this.make_exportplug.bind(this); this.make_export = this.make_export.bind(this); this.state={ varsum:{}, export:[], }; } handleCostChange(name,e) { this.state.varsum[name]=e; this.props.TotalCost(sum(this.state.varsum)); } make_exportplug(data,n) { this.state.export[n] = data; this.make_export(); } make_export(){ if (this.state.export.length === this.props.data.length) { this.props.export(this.state.export); } } render() { return(
Line controls
Category
Provider information
Cost
{(index) => }
); } } // MAIN // --------------------- // --------------------- class Main extends React.Component { constructor(props) { Money_GetRates(); super(props); this.handleCostChange = this.handleCostChange.bind(this); this.make_exportmain = this.make_exportmain.bind(this); this.handleNameChange = this.handleNameChange.bind(this); this.handleDurationChange = this.handleDurationChange.bind(this); this.handleConvMoneyChange = this.handleConvMoneyChange.bind(this); + this.btnClick=this.btnClick.bind(this); this.state= { total: 0, export: [], exportmain:[], name:'', duration:MainData.DefaultDuration, conv:{Enable:false,Cur:''}, }; projectduration=this.state.duration; this.init=true; } - + componentDidUpdate(){ + _paq.push(['enableLinkTracking']); + } handleCostChange(total) { if (this.state.total !== total){ this.setState({total:total}); } } make_exportmain(idata) { const tmp=JSON.parse(JSON.stringify(idata)); let disp=false; if(!this.init){ if(!Object.compare(tmp,this.state.exportmain.data)){ disp=true; } - - } if((this.init)||(disp)){ this.setState({exportmain: {data: tmp, total: tomoney(this.state.total)}}); this.init=false; } - } handleNameChange(name){ this.setState({name:name}); projectname=name; - } handleDurationChange(d){ this.setState({duration:d}); projectduration=d; } handleConvMoneyChange(conv){ this.setState({conv:conv}); } + btnClick(name,value){ + Stats.RecordEvent('Options',name,value); + } render() { return(
+ + {this.page_head()}
{this.project_info()} {this.final_cost(this.state.conv)} {this.howto()}
{this.page_foot()}
- ); } project_info(){ return(
); } // Display the total cost final_cost(conv){ let disps=''; if(projectduration>1) disps='s'; let convout=''; if (conv.Enable) convout=; return(
{/**/}

Total Cost for {projectduration} year{disps}

{convout}
); } // Define the head (top) of the page page_head(){ let helpbtn=null; let imglogo=null; if (MainData.HelpUrl!=null && MainData.HelpUrl!=="") - helpbtn= I need help with my DMP; + helpbtn= this.btnClick("helpbtn",0)}> +  I need help with my DMP; if (MainData.InstLogo!=null && MainData.InstLogo!=="") imglogo=; return( ); } //Define the foot (bottom) of the page page_foot(){ return( ); } // Define the howto (user guide) howto(){ let curconv=null; if(Money_Enable) curconv=
Change Currency

If you need another currency than {MainData.Currency} you can add an extra currency by selecting in the Change Currency menu

Actual rate is automatically applied using openexchangerates

; return(

HOWTO

Project Name and Duration

The Project name is only used for you.

Project Duration is used for subscription services charged by year : the yearly cost will be multiplied by the duration of the project.

{curconv}
Categories
This tool is divided by categories (for example Activate storage). Click on the category name, and it will expand.
Providers

Providers can be chosen from the Select a provider box. You can then tune your settings for this provider to fit your needs.

If the provider you want is not present, you can add it manually with Provide your own provider and then enter your provider/service and cost.

Add or Remove Line

If you want to add a new line use the } tips={"Add a new category"} onClick={this.fctnull}/> button.

You can also remove a line with } tips={"Remove this line"} onClick={this.fctnull}/> button.

To know more about
Some extra information about the category or the provider can be obtained with the } tips={"Know more"} onClick={this.fctnull}/> button.
Comments your input
Comments are for your own usage, you can use it for remembering what each section is and for a nice export.
Export
You can export your work into different formats :
HTML : This format can be used in any wordprocessing software (such as Microsoft Word or Libreoffice).
HTML Source code, Markdown, and CSV formats are also possible.
Click on the in order to copy your work into your clipboard. A simple Paste will transfer your work into any software.
); } // Function use by the howto btn to move the page move2howto(){ - $('html,body').animate({ - scrollTop: $("#howto").offset().top}, - 'slow'); + Stats.RecordEvent('Options',"howtobtn",0); + $('html,body').animate({scrollTop: $("#howto").offset().top},'slow'); } fctnull(){} } //Main Declaration // --------------------- // --------------------- ReactDOM.render(
,document.getElementById('root')); - - -$(function () { - $('[data-toggle="tooltip"]').tooltip() -}); - +// Display the stats popup after 10s +if (Stats.Enable) {setTimeout(function(){$('#PopupStats').modal('show')}, 10000);} +//Enable tooltip +$('[data-toggle="tooltip"]').tooltip(); diff --git a/js/costcalc_stats.jsx b/js/costcalc_stats.jsx new file mode 100644 index 0000000..9b32a63 --- /dev/null +++ b/js/costcalc_stats.jsx @@ -0,0 +1,221 @@ +"use strict"; +var _paq = window._paq || []; + +var Stats = { + + Enable: false, + Engine: '', + + + StatsInitMatomo: function (PIWIK_URL, IDSITE) { + +// accurately measure the time spent on the last pageview of a visit + _paq.push(['enableHeartBeatTimer']); + _paq.push(['setGenerationTimeMs', 0]); + _paq.push(['setDocumentTitle', 'MainPage']); + // require user consent before processing data + _paq.push(['requireConsent']); + _paq.push(['trackPageView']); + _paq.push(['enableLinkTracking']); + const u = "https://" + PIWIK_URL + "/"; + _paq.push(['setTrackerUrl', u + 'piwik.php']); + _paq.push(['setSiteId', IDSITE]); + const d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0]; + g.type = 'text/javascript'; + g.async = true; + g.defer = true; + g.src = u + 'piwik.js'; + s.parentNode.insertBefore(g, s); + + }, + + InitStat: function () { + const engine = MainData.StatsEngine; + const enable = MainData.UseStats; + if (enable) { + this.Enable = true; + this.Engine = engine; + switch (engine) { + case "matomo": + this.StatsInitMatomo(MainData.StatsURL, MainData.StatsID); + break; + + } + } + }, + + RecordEvent: function(action,name,value){ + if (this.Enable) { + switch (this.Engine) { + case "matomo": + _paq.push(['trackEvent', action, name, value]); + break; + } + } + }, + + ConsentOn: function(){ + console.log("Statistics Enabled"); + if (this.Enable) { + switch (this.Engine) { + case "matomo": + _paq.push(['rememberConsentGiven']); + break; + } + } + + }, + ConsentOff: function(){ + console.log("Statistics Disabled"); + if (this.Enable) { + switch (this.Engine) { + case "matomo": + _paq.push(['forgetConsentGiven']); + break; + } + } + }, + +}; + +class PopupStats extends React.Component { + constructor(props) { + super(props); + + } + + render() { + return ( + + ); + } + + knowmore() { + return ( +
+
+
+

+ +

+
+ +
+
+ We are recording the generic usage of the tools such as : +
    +
  • Number of visits
  • +
  • Time spend within the tool
  • +
  • Categories used
  • +
  • What kind of tools is used (Example : export)
  • +
+ What we are never recording : +
    +
  • Your IP address
  • +
  • The costs and options you enter
  • +
  • Manual inputs
  • +
+
+
+
+ +
+
+

+ +

+
+
+
+ {this.KmTools()} +
+
+
+
+
+

+ +

+
+
+
+ We are developing this tool and would like to know how you use it, in particular which options and categories you are using most in order to improve and update the tools for your needs. +
+
+
+
+
+

+ +

+
+
+
+ You can contact us Here +
+
+
+
+ ) + } + + KmTools() { + switch (Stats.Engine) { + case "matomo": + return(
+

We are using a self-hosted version of Matomo

+

The data are only stored on our servers at {MainData.StatsURL}

+

The data are only for research purpose and will not be shared my any 3rd party.

+
); + + } + } + +} + + +Stats.InitStat(); diff --git a/js/data.js b/js/data.js index 545c8f1..d5e9473 100644 --- a/js/data.js +++ b/js/data.js @@ -1,481 +1,486 @@ // Providers // ---------------------------------------------------- // ---------------------------------------------------- // Storage const NasEpfl = { Style: "AmountRatesCost", Provider : "EPFL-VPSI", Name:'NAS', Url : [ {Name:'VPSI-Website',Url:'https://support.epfl.ch/help/epfl?id=epfl_service_status&service=49a363acdb34c700ef64731b8c96191f'}, {Name:'SV-IT Storage Website',Url:'https://sv-it.epfl.ch/stockage'} ], ExtraInfo:"The first TB is free", ByYear:true, Adaptive:false, AmountName: "Amount", AmountUnit: "TB", AmountMin : 1, AmountMax : 500, AmountStep : 1, AmountFree:1, AmountFreeCumulative:false, RateName : 'Performance', Rates : { 'Collaborative': 165, 'On-line archive': 110, 'Raw': 55 }, RateUnit : "CHF / TB" }; const SwitchEpfl = { Style : 'CategoryCost', Provider : "Switch-EPFL", Name:'Online Storage', ByYear:true, Url : [ {Name:'Switch Website',Url:'https://drive.switch.ch/'} ], CatName:'Options', Cat:{ 'Cloud Based max 50GB':0, }, CatUnit:'CHF', }; const GoogleDriveEdu = { Style : 'CategoryCost', Provider : "Google Drive Educ", Name:'Online Storage', ByYear:true, Adaptive:false, ExtraInfo : "Google Storage is not recommended as the data are stored outside of Switzerland", ExtraInfoUrl :"https://support.epfl.ch/kb_view_customer.do?sysparm_article=KB0012829", Url : [ {Name :'Google Education Page',Url:'https://edu.google.com/?modal_active=none'} ], CatName:'Options', Cat:{ 'Cloud Based illimited':0, }, CatUnit:'CHF', }; const Dropbox_perso = { Style : 'CategoryCost', Provider : "Dropbox Personal", Name:'Online Storage', ByYear:true, ExtraInfo : "Dropbox is not recommended as the data are stored outside of Switzerland", ExtraInfoUrl :"https://support.epfl.ch/kb_view_customer.do?sysparm_article=KB0012882", Url : [ {Name :'Dropbox',Url:'https://www.dropbox.com/plans?trigger=nr'} ], CatName:'Plan', Cat:{ 'Personal Free 2Go':0, 'Personal Plus 1TB':112, 'Personal Pro 2TB':226, }, CatUnit:'CHF', }; const Dropbox_team = { Style : 'AmountRatesCost', Provider : "Dropbox for Team", Name:'Online Storage', ByYear:true, ExtraInfo : "Dropbox is not recommended as the data are stored outside of Switzerland", ExtraInfoUrl :"https://support.epfl.ch/kb_view_customer.do?sysparm_article=KB0012882", Url : [ {Name :'Dropbox',Url:'https://www.dropbox.com/plans?trigger=nr'} ], Adaptive:false, AmountName: "Number of Users", AmountUnit: "Users", AmountMin : 1, AmountMax : 500, AmountStep : 1, AmountFree:0, AmountFreeCumulative:false, RateName : 'Plan', Rates : { 'Standard': 136, 'Advanced': 204, }, RateUnit : "CHF / User" }; // ELN const SLIMSEpfl = { Style:'CategoryAmountRatesCost', Provider : "EPFL-SV-IT", Name:'SLIMS', ByYear:true, Url : [ {Name:'SLIMS on SV-IT Website',Url:'https://sv-it.epfl.ch/lims'}, {Name: 'Genohm (SLIMS Company)',Url:'https://www.genohm.com/'} ], CatName:'PI Status', Cat:{ 'Full Professor':3000, 'Associate Professor':2000, 'Tenure Track Assistant Professor or Core Facility':1000 }, CatUnit:'CHF', Adaptive:false, AmountName: "Storage", AmountUnit: "TB", AmountMin : 1, AmountMax : 100, AmountStep : 1, AmountFree:0, AmountFreeCumulative:false, RateName: 'ELN Storage', Rates : { 'Stored on EPFL Server': 165, }, RateUnit : "CHF / TB" }; const ELNEpfl = { Style : 'CategoryCost', Provider : "ELN-EPFL", Name:'ELN', ByYear:true, Url : [ {Name:'ELN Website',Url:'https://eln.epfl.ch/'} ], CatName:'Options', Cat:{ 'Free for EPFL community':0, }, CatUnit:'CHF', }; const Rspace = { Style : 'CategoryCost', Provider : "Rspace community", Name:'Rspace', ByYear:true, Url : [ {Name:'Rspace Website',Url:'https://www.researchspace.com/'} ], CatName:'Options', Cat:{ 'Cloud Based unlimited storage and user':0, }, CatUnit:'CHF', }; const Benchling = { Style : 'CategoryCost', Provider : "Benchling", Name:'ELN', ByYear:true, ExtraInfo : "The first 10GB are free", ExtraInfoUrl :"", Url : [ {Name :'Benchling website',Url:'https://benchling.com/academic'} ], CatName:'Options', Cat:{ 'Cloud Based 10GB':0, }, CatUnit:'CHF', }; // Database const MysqlEpfl = { Style : 'CategoryCost', Provider : "EPFL-VPSI", Name:'MySql', ByYear:true, Adaptive:false, Url : [ {Name:'EPFL VPSI ',Url:'https://support.epfl.ch/epfl?id=epfl_service_status&service=eb026fa0db34c700ef64731b8c96198e'} ], CatName:'Options', Cat:{ 'MySQL max 2GB':0, }, CatUnit:'CHF', }; // Repository const Zenodo = { Style : 'CategoryCost', Provider : "Zenodo-CERN", Name:'Zenodo', ByYear:false, Adaptive:false, Url : [ {Name:'Zenodo Website',Url:'https://www.zenodo.org/'}, {Name:'About Zenodo',Url:'http://about.zenodo.org/'}, ], CatName:'Options', Cat:{ 'Max 50GB per Dataset':0, }, CatUnit:'CHF', }; const C4science = { Style : 'CategoryCost', Provider : "EPFL-SCITAS", Name:'C4Science', Url : [ {Name:'C4Science Website',Url:'https://www.c4science.ch/'} ], ByYear:true, Adaptive:false, ExtraInfo:'C4Science is the repository recommended by EPFL for code repository', CatName:'Options', Cat:{ 'Free for text file':0, }, CatUnit:'CHF', }; const Github = { Style: "AmountRatesCost", Provider : "GitHub", Name:'GitHub', Url : [ {Name:'Github Website Pricing',Url:'https://github.com/pricing'} ], AmountName: "Number of user", AmountUnit: "User(s)", Adaptive:true, ByYear:true, AmountMin : [1,1,5,10], AmountMax : [100,1,100,100], AmountStep : [1,1,1,1], AmountFree:[0,0,0,0], AmountFreeCumulative:false, RateName:'Plan', Rates:{ 'OpenSource project':0, 'Developer (for one user)':81.6, 'Team (min 5 users)':104.9, 'Business Cloud':244.7 }, RateUnit:'CHF / Users', }; const Bitbucket= { Style: "AmountRatesCost", Provider : "Bitbucket", Name:'BitBucket', Url : [ {Name:'Bitbucket Website Pricing',Url:'https://bitbucket.org/product/pricing'} ], AmountName: "Number of user", AmountUnit: "User(s)", Adaptive:true, ByYear:true, AmountMin : [1,5,5], AmountMax : [5,100,100], AmountStep : [1,1,1], AmountFree:[0,0,0], AmountFreeCumulative:false, RateName:'Plan', Rates:{ 'Free (up to 5 users)':0, 'Standard for growing teams (min 5 users)':24, 'Premium for large teams (min 5 users)':60, }, RateUnit:'CHF / Users', }; const Gitlab= { Style: "AmountRatesCost", Provider : "Gitlab", Name:'Gitlab', Url : [ {Name:'Gitlab Website Pricing',Url:'https://about.gitlab.com/pricing/'} ], AmountName: "Number of user", AmountUnit: "User(s)", AmountMin : 1, AmountMax : 100, AmountStep : 1, AmountFree:0, Adaptive:false, AmountFreeCumulative:false, ByYear:true, RateName:'Plan', Rates:{ 'Core Self Hosted':0, 'Free Cloud Based' : 0, 'Starter Self Hosted':48, 'Bronze Cloud based':48, 'Premium Self Hosted':228, 'Silver Cloud Based':228 }, RateUnit:'CHF / Users', }; const Figshare = { Style : 'CategoryCost', Provider : "FigShare", Name:'Figshare', ByYear:false, Adaptive:false, Url : [ {Name:'Figshare website',Url:'https://figshare.com/'}, {Name:'Figshare Pricing',Url:'https://www.g2crowd.com/products/figshare/pricing'} ], ExtraInfo:'The costs of enabling access to research data under an SNSF grant are eligible. The data archives (data repositories) have to meet the FAIR principles.', ExtraInfoUrl:'http://www.snf.ch/SiteCollectionDocuments/snsf-general-implementation-regulations-for-the-funding-regulations-e.pdf#page=14', CatName:'Options', Cat:{ 'Free 1GB':0, '10GB':96, '15GB':132, '20GB':180 }, CatUnit:'CHF', }; const Dryad = { Style : 'CategoryAmountRatesCost', Provider : "Dryad", Name:'Dyrad', Url : [ {Name:'Dryad Website Pricing',Url:'https://Datadryad.org/pages/payment'} ], ByYear:false, ExtraInfo:'The costs of enabling access to research data under an SNSF grant are eligible. The data archives (data repositories) have to meet the FAIR principles.', ExtraInfoUrl:'http://www.snf.ch/SiteCollectionDocuments/snsf-general-implementation-regulations-for-the-funding-regulations-e.pdf#page=14', CatName:'Options', Cat:{ 'up to 20GB if DPC covered':0, 'up to 20GB if no DPC covered':120 }, CatUnit:'CHF', AmountName: "Storage", AmountUnit: "GB", Adaptive:false, AmountMin : 20, AmountMax : 100, AmountStep : 10, AmountFree:20, AmountFreeCumulative:false, RateName: 'Storage', Rates : { 'Extra Storage': 50, }, RateUnit : "CHF / GB" }; // System variable definition // ---------------------------------------------------- // ---------------------------------------------------- const NoneSelected={ Style: 'NoneSelect', Provider:'None', Name:'Select a Provider', Url:'', ByYear:false, }; const UserCostSelect={ Style : 'UserCost', Provider:'Manual Provider', Name:'', Url:'', ByYear:false, }; // Categories definition // ---------------------------------------------------- // ---------------------------------------------------- const storage={ Name : 'Active Storage', Icon : 'storage.png', Url : [ {Name:'EPFL RDM',Url:'https://researchData.epfl.ch/work-with-Data/storage/'}, {Name:'Comparison of file synchronization software',Url:'https://en.wikipedia.org/wiki/Comparison_of_file_synchronization_software'} ], Data :[NoneSelected, NasEpfl, SwitchEpfl, GoogleDriveEdu, Dropbox_perso, Dropbox_team, UserCostSelect, ] }; const ELN={ Name : 'Electronic LabBook', Icon : 'eln.png', Url : [ {Name: 'EPFL RDM',Url:'https://researchData.epfl.ch/work-with-Data/active-Data-management/'} ], Data :[NoneSelected, SLIMSEpfl, ELNEpfl, Rspace, Benchling, UserCostSelect, ] }; const Database={ Name : 'Database', Icon : 'database.png', Url : '', Data :[NoneSelected, MysqlEpfl, UserCostSelect, ] }; const datarepository={ Name : 'Data Repository', Icon : 'drepos.png', Url : [ {Name:'EPFL RDM WebPage',Url:'https://researchData.epfl.ch/publish-preserve/'} ], Data :[ NoneSelected, Zenodo, Figshare, Dryad, UserCostSelect, ] }; const coderepository={ Name : 'Code Repository', Icon : 'crepos.png', Url : '', Data :[ NoneSelected, C4science, Github, Bitbucket, Gitlab, UserCostSelect, ] }; // Combine Categories // ---------------------------------------------------- // ---------------------------------------------------- const MainData={ InstName:'EPFL Library', InstLogo:'logo.png', InstLogoWidth:200, Updated:'25/06/2019', HelpUrl:'mailto:researchdata@epfl.ch', Currency:'CHF', OEXRApi:'cd8d785bdb6646b0a7e4c0eba5a74199', Conv:['EUR','USD','GBP'], - Version : 'v2.1', + UseStats:true, + StatsEngine:'matomo', + StatsURL:'costcalc.epfl.ch/matomo/', + StatsID:'1', + StatsContact:'mailto:researchdata@epfl.ch', + Version : 'v2.0', DefaultDuration:1, Data:[storage,ELN,Database,datarepository,coderepository], };