2015-10-14 16:15:39 +02:00
|
|
|
@extends('header')
|
2015-03-16 22:45:25 +01:00
|
|
|
|
|
|
|
@section('head')
|
|
|
|
@parent
|
|
|
|
|
2015-12-07 14:34:55 +01:00
|
|
|
@include('money_script')
|
2015-04-01 21:57:02 +02:00
|
|
|
<script src="{!! asset('js/d3.min.js') !!}" type="text/javascript"></script>
|
2015-03-16 22:45:25 +01:00
|
|
|
|
|
|
|
<style type="text/css">
|
|
|
|
|
|
|
|
#tooltip {
|
|
|
|
position: absolute;
|
|
|
|
width: 200px;
|
|
|
|
height: auto;
|
|
|
|
padding: 10px 10px 2px 10px;
|
|
|
|
background-color: #F6F6F6;
|
|
|
|
-webkit-border-radius: 10px;
|
|
|
|
-moz-border-radius: 10px;
|
|
|
|
border-radius: 10px;
|
|
|
|
-webkit-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
|
|
|
|
-moz-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
|
|
|
|
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
|
|
|
|
}
|
|
|
|
|
|
|
|
.no-pointer-events {
|
|
|
|
pointer-events: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
</style>
|
|
|
|
@stop
|
|
|
|
|
|
|
|
@section('content')
|
|
|
|
@parent
|
2015-10-14 16:15:39 +02:00
|
|
|
@include('accounts.nav', ['selected' => ACCOUNT_DATA_VISUALIZATIONS, 'advanced' => true])
|
2015-03-16 22:45:25 +01:00
|
|
|
|
|
|
|
<div id="tooltip" class="hidden">
|
|
|
|
<p>
|
|
|
|
<strong><span id="tooltipTitle"></span></strong>
|
|
|
|
<a class="pull-right" href="#" target="_blank">View</a>
|
|
|
|
</p>
|
|
|
|
<p>Total <span id="tooltipTotal" class="pull-right"></span></p>
|
|
|
|
<p>Balance <span id="tooltipBalance" class="pull-right"></span></p>
|
|
|
|
<p>Age <span id="tooltipAge" class="pull-right"></span></p>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<form class="form-inline" role="form">
|
|
|
|
Group By
|
2015-04-30 19:54:19 +02:00
|
|
|
<select id="groupBySelect" class="form-control" onchange="update()" style="background-color:white !important">
|
2015-03-16 22:45:25 +01:00
|
|
|
<option>Clients</option>
|
|
|
|
<option>Invoices</option>
|
|
|
|
<option>Products</option>
|
|
|
|
</select>
|
2015-04-01 21:57:02 +02:00
|
|
|
<b>{!! $message !!}</b>
|
2015-03-16 22:45:25 +01:00
|
|
|
</form>
|
|
|
|
|
|
|
|
<p> </p>
|
|
|
|
|
|
|
|
<div class="svg-div"/>
|
|
|
|
|
|
|
|
<script type="text/javascript">
|
|
|
|
|
|
|
|
// store data as JSON
|
2015-04-01 21:57:02 +02:00
|
|
|
var data = {!! $clients !!};
|
2015-03-16 22:45:25 +01:00
|
|
|
|
|
|
|
_.each(data, function(client) {
|
|
|
|
_.each(client.invoices, function(invoice) {
|
|
|
|
_.each(invoice.invoice_items, function(invoice_item) {
|
|
|
|
invoice_item.invoice = invoice;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// pre-process the possible groupings (clients, invoices and products)
|
|
|
|
var clients = data.concat();
|
|
|
|
var invoices = _.flatten(_.pluck(clients, 'invoices'));
|
|
|
|
|
|
|
|
// remove quotes and recurring invoices
|
|
|
|
invoices = _.filter(invoices, function(invoice) {
|
2015-08-11 16:38:36 +02:00
|
|
|
return !parseInt(invoice.is_quote) && !invoice.is_recurring;
|
2015-03-16 22:45:25 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
var products = _.flatten(_.pluck(invoices, 'invoice_items'));
|
|
|
|
products = d3.nest()
|
|
|
|
.key(function(d) { return d.product_key; })
|
|
|
|
.sortKeys(d3.ascending)
|
|
|
|
.rollup(function(d) { return {
|
|
|
|
amount: d3.sum(d, function(g) {
|
|
|
|
return g.qty * g.cost;
|
|
|
|
}),
|
|
|
|
paid: d3.sum(d, function(g) {
|
|
|
|
return g.invoice && g.invoice.invoice_status_id == 5 ? (g.qty * g.cost) : 0;
|
|
|
|
}),
|
|
|
|
age: d3.mean(d, function(g) {
|
|
|
|
return calculateInvoiceAge(g.invoice) || null;
|
|
|
|
}),
|
|
|
|
}})
|
|
|
|
.entries(products);
|
|
|
|
|
|
|
|
// create standardized display properties
|
|
|
|
_.each(clients, function(client) {
|
|
|
|
client.displayName = getClientDisplayName(client);
|
|
|
|
client.displayTotal = +client.paid_to_date + +client.balance;
|
|
|
|
client.displayBalance = +client.balance;
|
|
|
|
client.displayPercent = (+client.paid_to_date / (+client.paid_to_date + +client.balance)).toFixed(2);
|
|
|
|
var oldestInvoice = _.max(client.invoices, function(invoice) { return calculateInvoiceAge(invoice) });
|
|
|
|
client.displayAge = oldestInvoice ? calculateInvoiceAge(oldestInvoice) : -1;
|
|
|
|
});
|
|
|
|
|
|
|
|
_.each(invoices, function(invoice) {
|
|
|
|
invoice.displayName = invoice.invoice_number;
|
|
|
|
invoice.displayTotal = +invoice.amount;
|
|
|
|
invoice.displayBalance = +invoice.balance;
|
|
|
|
invoice.displayPercent = (+invoice.amount - +invoice.balance) / +invoice.amount;
|
|
|
|
invoice.displayAge = calculateInvoiceAge(invoice);
|
|
|
|
});
|
|
|
|
|
|
|
|
_.each(products, function(product) {
|
|
|
|
product.displayName = product.key;
|
|
|
|
product.displayTotal = product.values.amount;
|
|
|
|
product.displayBalance = product.values.amount - product.values.paid;
|
|
|
|
product.displayPercent = (product.values.paid / product.values.amount).toFixed(2);
|
|
|
|
product.displayAge = product.values.age;
|
|
|
|
});
|
|
|
|
|
|
|
|
//console.log(JSON.stringify(clients));
|
|
|
|
//console.log(JSON.stringify(invoices));
|
|
|
|
//console.log(JSON.stringify(products));
|
|
|
|
|
2016-02-17 21:42:31 +01:00
|
|
|
var arc = d3.svg.arc()
|
|
|
|
.innerRadius(function(d) { return d.r })
|
2016-02-27 21:53:02 +01:00
|
|
|
.outerRadius(function(d) { return d.r - 8 })
|
2016-02-17 21:42:31 +01:00
|
|
|
.startAngle(0);
|
|
|
|
|
|
|
|
var fullArc = d3.svg.arc()
|
2016-02-27 21:51:22 +01:00
|
|
|
.innerRadius(function(d) { return d.r - 1 })
|
2016-02-27 21:53:02 +01:00
|
|
|
.outerRadius(function(d) { return d.r - 7 })
|
2016-02-17 21:42:31 +01:00
|
|
|
.startAngle(0)
|
|
|
|
.endAngle(2 * Math.PI);
|
2015-03-16 22:45:25 +01:00
|
|
|
|
2015-10-16 07:32:02 +02:00
|
|
|
var diameter = 800,
|
2015-03-16 22:45:25 +01:00
|
|
|
format = d3.format(",d");
|
|
|
|
//color = d3.scale.category10();
|
|
|
|
|
|
|
|
var color = d3.scale.linear()
|
|
|
|
.domain([0, 100])
|
|
|
|
.range(["yellow", "red"]);
|
|
|
|
|
|
|
|
var bubble = d3.layout.pack()
|
|
|
|
.sort(null)
|
|
|
|
.size([diameter, diameter])
|
|
|
|
.value(function(d) { return Math.max(30, d.displayTotal) })
|
|
|
|
.padding(12);
|
|
|
|
|
|
|
|
var svg = d3.select(".svg-div").append("svg")
|
2015-10-14 16:15:39 +02:00
|
|
|
.attr("width", "100%")
|
2015-03-16 22:45:25 +01:00
|
|
|
.attr("height", "1142px")
|
|
|
|
.attr("class", "bubble");
|
|
|
|
|
|
|
|
svg.append("rect")
|
|
|
|
.attr("stroke-width", "1")
|
|
|
|
.attr("stroke", "rgb(150,150,150)")
|
2015-10-14 16:15:39 +02:00
|
|
|
.attr("width", "99%")
|
2015-03-16 22:45:25 +01:00
|
|
|
.attr("height", "100%")
|
|
|
|
.attr("fill", "white");
|
|
|
|
|
|
|
|
function update() {
|
|
|
|
|
|
|
|
var data = {};
|
|
|
|
var groupBy = $('#groupBySelect').val().toLowerCase();
|
|
|
|
data.children = window[groupBy];
|
|
|
|
|
|
|
|
data = bubble.nodes(data).filter(function(d) {
|
|
|
|
return !d.children && d.displayTotal && d.displayName;
|
2015-10-16 07:32:02 +02:00
|
|
|
});
|
2015-03-16 22:45:25 +01:00
|
|
|
|
|
|
|
var selection = svg.selectAll(".node")
|
|
|
|
.data(data, function(d) { return d.displayName; });
|
|
|
|
|
|
|
|
var node = selection.enter().append("g")
|
|
|
|
.attr("class", "node")
|
|
|
|
.attr("transform", function(d) { return "translate(" + (d.x+20) + "," + (d.y+20) + ")"; });
|
|
|
|
|
|
|
|
var visibleTooltip = false;
|
|
|
|
node.on("mousemove", function(d) {
|
|
|
|
if (!visibleTooltip || visibleTooltip != d.displayName) {
|
|
|
|
d3.select("#tooltip")
|
|
|
|
.classed("hidden", false)
|
2015-10-14 16:15:39 +02:00
|
|
|
.style("left", (d3.event.offsetX + 40) + "px")
|
|
|
|
.style("top", (d3.event.offsetY + 40) + "px");
|
2015-03-16 22:45:25 +01:00
|
|
|
visibleTooltip = d.displayName;
|
2015-10-14 16:15:39 +02:00
|
|
|
//console.log(d3.event);
|
2015-03-16 22:45:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
d3.select("#tooltipTitle").text(truncate(d.displayName, 18));
|
|
|
|
d3.select("#tooltipTotal").text(formatMoney(d.displayTotal));
|
|
|
|
d3.select("#tooltipBalance").text(formatMoney(d.displayBalance));
|
|
|
|
d3.select("#tooltipAge").text(pluralize('? day', parseInt(Math.max(0, d.displayAge))));
|
|
|
|
|
|
|
|
if (groupBy == "products" || !d.public_id) {
|
|
|
|
d3.select("#tooltip a").classed("hidden", true);
|
|
|
|
} else {
|
|
|
|
d3.select("#tooltip a").classed("hidden", false);
|
|
|
|
d3.select("#tooltip a").attr("href", "/" + groupBy + "/" + d.public_id);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
svg.on("click", function() {
|
|
|
|
visibleTooltip = false;
|
|
|
|
d3.select("#tooltip")
|
|
|
|
.classed("hidden", true);
|
|
|
|
});
|
|
|
|
|
|
|
|
node.append("circle")
|
|
|
|
.attr("fill", "#ffffff")
|
|
|
|
.attr("r", function(d) { return d.r });
|
|
|
|
|
|
|
|
node.append("path")
|
|
|
|
.each(function(d) { d.endAngle = 0; })
|
|
|
|
.attr("class", "no-pointer-events")
|
|
|
|
.attr("class", "animate-fade")
|
|
|
|
.attr("d", fullArc)
|
|
|
|
.style("fill", function(d, i) { return 'white'; });
|
|
|
|
|
|
|
|
node.append("text")
|
|
|
|
.attr("dy", ".3em")
|
|
|
|
.attr("class", "no-pointer-events")
|
|
|
|
.style("text-anchor", "middle")
|
|
|
|
.text(function(d) { return d.displayName; });
|
|
|
|
|
|
|
|
node.append("path")
|
|
|
|
.each(function(d) { d.endAngle = 0; })
|
|
|
|
.attr("class", "no-pointer-events")
|
|
|
|
.attr("class", "animate-grow")
|
|
|
|
.attr("d", arc)
|
2016-02-17 21:32:26 +01:00
|
|
|
.style("fill", function(d, i) { return '#2e9e49'; });
|
2015-03-16 22:45:25 +01:00
|
|
|
|
|
|
|
d3.selectAll("path.animate-grow")
|
|
|
|
.transition()
|
|
|
|
.delay(function(d, i) { return (Math.random() * 500) })
|
|
|
|
.duration(1000)
|
|
|
|
.call(arcTween, 5);
|
|
|
|
|
|
|
|
d3.selectAll("path.animate-fade")
|
|
|
|
.transition()
|
|
|
|
.duration(1000)
|
|
|
|
.style("fill", function(d, i) {
|
2016-05-10 11:20:53 +02:00
|
|
|
return 'red';
|
2015-03-16 22:45:25 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
selection.exit().remove();
|
|
|
|
}
|
|
|
|
|
|
|
|
update();
|
|
|
|
|
|
|
|
// http://bl.ocks.org/mbostock/5100636
|
|
|
|
function arcTween(transition, newAngle) {
|
|
|
|
transition.attrTween("d", function(d) {
|
|
|
|
var interpolate = d3.interpolate( 0, 360 * d.displayPercent * Math.PI/180 );
|
|
|
|
return function(t) {
|
|
|
|
d.endAngle = interpolate(t);
|
|
|
|
return arc(d);
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function calculateInvoiceAge(invoice) {
|
|
|
|
if (!invoice || invoice.invoice_status_id == 5) {
|
|
|
|
return -1;
|
|
|
|
}
|
2015-10-20 19:12:34 +02:00
|
|
|
var dayInSeconds = 1000*60*60*24;
|
2016-04-19 04:35:18 +02:00
|
|
|
@if (Auth::user()->account->hasFeature(FEATURE_REPORTS))
|
2015-10-20 19:12:34 +02:00
|
|
|
var date = convertToJsDate(invoice.created_at);
|
|
|
|
@else
|
|
|
|
var date = new Date().getTime() - (dayInSeconds * Math.random() * 100);
|
|
|
|
@endif
|
|
|
|
|
|
|
|
return parseInt((new Date().getTime() - date) / dayInSeconds);
|
2015-03-16 22:45:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function convertToJsDate(isoDate) {
|
|
|
|
if (!isoDate) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
var t = isoDate.split(/[- :]/);
|
|
|
|
return new Date(t[0], t[1]-1, t[2], t[3], t[4], t[5]);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pluralize(string, count) {
|
|
|
|
string = string.replace('?', count);
|
|
|
|
if (count !== 1) {
|
|
|
|
string += 's';
|
|
|
|
}
|
|
|
|
return string;
|
|
|
|
};
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
@stop
|