One thing I find routinely difficult is creating and updating estimates and invoices for clients, with fluid dates, on top of managing emails and trello boards. This is a hacky and simple attempt to build an invoice generator. Currently it uses only client-side javascript to create an invoice from a list of deliverables, but could easily grow into a larger applications with an API and authentification + authorization. Eventually, it could be used to manage multiple projects and create trello boards and lists based on the input deliverables.
A basic data model for a single invoice should look something like this:
{
"name":"IO",
"type": "creative technology",
"clientCo":"Google",
"clientRep":"Ron Swanson",
"clientEmail":"ronaldjsawnsong@protonmail.com",
"currentDate":"03/01/2018",
"startDate":"04/01/2018",
"endDate":"12/31/2018",
"hours":650,
"rate":50,
"currency":"USD",
"status":"hold",
"deliverables":[
{
"deliverable":"Design Research",
"date_start":"04/16/2018",
"date_end":"07/31/2018",
"deliverable_time":500,
"deliverable_items":[
"Discovery",
"Mockups",
"Prototypes",
"Iteration"
]
},
{
"deliverable":"User Experience",
"date_start":"08/01/2018",
"date_end":"09/01/2018",
"deliverable_time":100,
"deliverable_items":[
"Personas",
"Testing",
"Analytics"
]
},
{
"deliverable":"Support",
"date_start":"09/01/2018",
"date_end":"11/01/2018",
"deliverable_time":50,
"deliverable_items":[
"Cross-Compatability",
"Emergent Issues"
]
}
],
"estimate":32500,
};
Pretty simple. We create an object that includes general project information, dates, and an array of deliverables that each has an array of deliverable items and an estimate of time. From there, it isn't difficult to create a rough quote.
To input the data, we can use a form. Currently, the form passes data to inline functions, but it should be straightforward to refactor to POST
to an API.
A basic html form, with many fields:
<form class="form">
<div class="row">
<div class="col-xs-12">
<div class="row">
<div class="col-xs-4">
<p class="field">
<input class="text-input client_co" name="client_co" type="text" placeholder="Client Company" autocomplete='organization'></input>
</p>
</div>
<div class="col-xs-4">
<p class="field">
<input class="text-input client_rep" name="client_rep" type="text" placeholder="Client Representative"></input>
</p>
</div>
<div class="col-xs-4">
<p class="field">
<input class="text-input client_email" name="client_email" type="email" placeholder="Client Email" autocomplete='email'></input>
</p>
</div>
</div>
<div class="row">
<div class="col-xs-6">
<p class="field">
<input class="text-input project_type" name="project_type" type="text" placeholder="Project Type"></input>
</p>
</div>
<div class="col-xs-6">
<p class="field">
<input class="text-input project_title" name="project_title" type="text" placeholder="Project Title"></input>
</p>
</div>
</div>
<div class="row">
<div class="col-xs-3">
<p class="field">
<input class="text-input date_curr" name="date_curr" type="text" placeholder="Current Date"></input>
</p>
</div>
<div class="col-xs-3">
<p class="field">
<input class="text-input date_start" name="date_start" type="text" placeholder="Project Start Date"></input>
</p>
</div>
<div class="col-xs-3">
<p class="field">
<input class="text-input date_end" name="date_end" type="text" placeholder="Project End Date"></input>
</p>
</div>
<div class="col-xs-3">
<p class="field">
<input class="text-input rate" name="rate" type="number" placeholder="Rate"></input>
</p>
</div>
</div>
<div class="services">
<div class="row" style="border:1px solid #000;">
<div class="col-xs-12">
<a href="javascript:void(0);" class="add_deliverable-add" title="Add field"><i class="fa fa-plus-circle"></i> Add Deliverable</a>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<p class="field">
<input style="padding: 10px;" class="button" type="submit" onclick="return saveData();" value="Generate"></input>
<select style="float:right">
<option>USD</option>
<option>EU</option>
</select>
</p>
</div>
</div>
</form>
And some javascript to handle the submission and save it to local storage:
function saveData(id) {
var project_type = $(".project_type").val();
var project_title = $(".project_title").val();
var client_co = $(".client_co").val();
var client_email = $(".client_email").val();
var client_rep = $(".client_rep").val();
var date_curr = $(".date_curr").val();
var date_start = $(".date_start").val();
var date_end = $(".date_end").val();
var rate = $(".rate").val();
var json = {};
var keyout = [];
$('.deliverables').each(function(i, obj){
var items = $('input[name^=deliverable]', this).map(function(idx, elem) {
var cost = $(elem).closest('.row').find(".item_cost").val();
var datestart = $(elem).closest('.row').find(".item_date_start").val();
var dateend = $(elem).closest('.row').find(".item_date_end").val();
json.deliverable = $(elem).closest('.row').find(".deliverable-service").val();
json.date_start = datestart;
json.date_end = dateend;
json.deliverable_time = parseInt(cost);
return $(elem).val();
}).get();
json.deliverable_items = items;
keyout.push(json);
json = {} // clear temporary object
event.preventDefault();
});
var project = {};
project.name = project_title;
project.type = project_type;
project.clientCo = client_co;
project.clientRep = client_rep;
project.clientEmail = client_email;
project.currentDate = date_curr;
project.startDate = date_start;
project.endDate = date_end;
project.hours = 0;
project.rate = parseInt(rate);
project.currency = "USD";
project.deliverables = keyout;
for (key in keyout) {
project.hours += keyout[key].deliverable_cost;
$(".delivery").append('<div class="unit"><div class="title">'+ JSON.stringify(keyout[key].deliverable) +'</div><div class="items"></div></div>');
}
project.estimate = project.rate * project.hours;
storeObject(project);
return false;
}
function storeObject(data) {
console.log("storing data");
localStorage.setItem("cookie", JSON.stringify(data));
}
As well as some additional javascript for adding deliverables and items on the fly:
var maxItemField = 10; //Input fields increment limitation
var addItemButton = $('.add_deliverable'); //Add button selector
var itemWrapper = $('.deliverables'); //Input field wrapper
// console.log(itemWrapper);
var itemFieldHTML = '<div class="field"><input class="text-input" type="text" class="deliverable" name="deliverable" value="" placeholder="Deliverable Item"/><a href="javascript:void(0);" class="remove_item_button" title="Remove field" style="position: absolute; right: 20px; top: 10%;"><i class="fa fa-minus-circle"></i></a><a href="javascript:void(0);" class="add_deliverable" title="Add field" style="position: absolute; right: 0; top: 10%;"><i class="fa fa-plus-circle"></i></a></p></div></div></div>'; //New input field html
var x = 1; //Initial field counter is 1
$(document).on("click", ".deliverables .add_deliverable", function(e){ //Once add button is clicked
var itemWrapper = $(e.target).closest('.deliverables');
var count = $(e.target).closest('.deliverables').children().length;
if(count <= 11){ //Check maximum number of input fields
count++; //Increment field counter
$(itemWrapper).append(itemFieldHTML); // Add field html
}
});
$(document).on('click', '.deliverables .remove_item_button', function(e){ //Once remove button is clicked
e.preventDefault();
$(e.target).closest('.field').remove(); //Remove field html
x--; //Decrement field counter
});
$(window).keydown(function(event){
if(event.keyCode == 13) {
if ($(event.target).is('input')) {
event.preventDefault();
$(event.target).siblings(".add_deliverable").click();
$(event.target).closest(".field").next().find("input").focus();
}
}
});
var maxServiceField = 10; //Input fields increment limitation
var addServiceButton = $('.add_deliverable-add'); //Add button selector
var serviceWrapper = $('.services'); //Input field wrapper
var serviceFieldHTML = '<div class="row service-item" style="border:1px solid #000;"><div class="col-xs-12"><p class="field"><a href="javascript:void(0);" class="remove_service_button" title="Add field"><i class="fa fa-minus-circle"></i></a></p></div><div class="col-xs-12"><div class="row"><div class="col-xs-4"><p class="field"><input class="text-input item_date_start" name="item_date_start" type="text" value="" placeholder="Item Start Date"></input></p></div><div class="col-xs-4"><p class="field"><input class="text-input item_date_end" name="item_date_end" type="text" placeholder="Item End Date"></input></p></div><div class="col-xs-4"><p class="field"><input class="text-input item_cost" id="item_cost" name="item_cost" type="number" placeholder="Time Estimate"></input></p></div></div></div><div class="col-xs-12"><p class="field"><input class="text-input deliverable-service" type="text" id="deliverable-service" name="deliverable-service" value="" placeholder="Deliverable"/></p></div><div class="col-xs-12 deliverables"><p class="field"><input class="text-input deliverable" type="text" id="deliverable" class="deliverable" name="deliverable" value="" placeholder="Deliverable Item"/><a href="javascript:void(0);" class="add_deliverable" title="Add field" style="position: absolute; right: 0; top: 10%;"><i class="fa fa-plus-circle"></i></a></p></div></div>'
var x = 1; //Initial field counter is 1
$(document).on("click",".add_deliverable-add", function(){ //Once add button is clicked
if(x < maxServiceField){ //Check maximum number of input fields
x++; //Increment field counter
if ($('.service-item').length >= 1) {
$(serviceWrapper).find('.service-item').last().after(serviceFieldHTML); // Add field html
} else {
$(serviceWrapper).prepend(serviceFieldHTML); // Add field html
}
}
});
$(document).on('click', '.services .remove_service_button', function(e){ //Once remove button is clicked
e.preventDefault();
$(this).parent().parent().parent().remove(); //Remove field html
x--; //Decrement field counter
});
And some datepicker
init functions to handle the many date fields:
$( ".date_curr" ).datepicker({
onSelect: function(dateText) {
$(this).parent().addClass('active');
}
});
$( ".date_start" ).datepicker({
onSelect: function(dateText) {
$(this).parent().addClass('active');
}
});
$( ".date_end" ).datepicker({
onSelect: function(dateText) {
$(this).parent().addClass('active');
}
});
$(document).on("focus", ".item_date_start", function() {
$(this).datepicker("refresh").datepicker(); // reinit for each individual deliverable
});
$(document).on("focus", ".item_date_end", function() {
$(this).datepicker("refresh").datepicker(); // reinit for each individual deliverable
});
A rough html page, built with printing (specifically to PDF) in mind:
<div class="page">
<div class="letterhead" id="hed">
<div class="logo"></div>
<p class="block">
<span class="">FreelanceIsFun</span>
<span class="inline"><b><span class="project-title"></span></b><span class="project-type"></span></span>
<span class="date-curr"></span>
</p>
</div>
<div id="proj-hed">
<div class="clienthead">
<span class="client-co"></span><span class="client-rep"></span><span class="client-email"></span>
</div>
</div>
<div id="timeline">
<div class="large-dates">
<div class="lt">
<span class="date-start"></span>
</div>
<div class="rt">
<span class="date-end"></span>
</div>
</div>
</div>
<div id="ship">
<div class="overview"></div>
<div class="page-break"></div>
<div class="delivery"></div>
</div>
<div class="page-break"></div>
<div id="bill">
<div class="breakdown"></div>
<div class="quote">
<span class="estimate"></span>
</div>
</div>
</div>
And a function to fill the data:
function fillData(data) {
$(".client-co").html(data.clientCo);
$(".client-rep").html(data.clientRep);
$(".client-email").html(data.clientEmail);
$('.project-title').html(data.name);
$('.project-type').html(data.type);
$('.date-curr').html(data.currentData);
$('.date-start').html(ripdate(data.startDate));
$('.date-end').html(ripdate(data.endDate));
$(".estimate").html(data.estimate);
for (item in data.deliverables) {
var htmlstring = "";
for (key in data.deliverables[item].deliverable_items) {
htmlstring += '<ul class="deliv-items">' +data.deliverables[item].deliverable_items[key] + '</ul>';
}
$(".overview").append('<div class="unit"><div class="title"><h3>'+ data.deliverables[item].deliverable +'</h3></div></div>');
$(".breakdown").append('<div class="title"><h3>'+ data.deliverables[item].deliverable_time +'</h3></div>');
$(".delivery").append('<div class="unit"><div class="title"><h3>'+ data.deliverables[item].deliverable +'</h3></div><div class="items">'+ htmlstring +'</div></div>');
}
}
function retrieveObject() {
var retrieve = JSON.parse(localStorage.getItem("cookie"));
fillData(retrieve);
}
That covers most everything needed to quickly submit and parse the form data. It's still inconvenient to fill out the form each time, so an API that would allow for storing and editing projects could be quite helpful. Saving the object as a bootleg cookie with localStorage
helps slightly, but an accidental reload could destroy a form full of data. Another solution could be to repopulate the form with any saved data, but that's still a bit annoying.
Eventually, I'd like to develop a REST API to use (maybe Django's REST framework, maybe Express) and migrate to a more robust front end framework. React is intimidating, so possibly backbone or vue. I love how powerful Django is, and an ideal situation would be to accomplish all of this via Django.