En esta entrada explicaré como crear una vista ASP.net como la que se muestra a continuación, en la que se muestra la lista de las 10 marcas más vendidas más un gráfico de tipo pastel. Para ello utilizo jQueryFlot sobre la plantilla wrapbootstrap-ace comentada en entradas anteriores.
En este caso se tratará de una página “estática”, que recibirá los datos a través del Model. En una entrada posterior utilizaremos json para poder actualizar los datos (y el gráfico) de la vista en función de, por ejemplo, el rango temporal que elija el usuario (este año, últimos 30 días, etc.).

Vista SQL CRM_Ventas
Partimos de una vista de SQL Server existente, cuya de definición es la siguiente:
CREATE VIEW [dbo].[CRM_Ventas]
WITH SCHEMABINDING
AS
SELECT TOP (100) PERCENT
Comercial as Zona,
[Clasif_ neumático] as Gama,
Marca,
c.Año,
c.Mes,
c.[Nº cliente] as IdCliente,
c.[Nombre cliente] as NombreCliente,
cast(SUM(c.[Imp_ ventas]) as decimal(7,2)) as Importe,
cast(SUM(c.[Cdad_ ventas]) as integer) as Cantidad
FROM dbo.[Cuadro mando Neumalia] AS c
GROUP BY Comercial, [Clasif_ neumático], Marca, c.Año, c.Mes, c.[Nº cliente], c.[Nombre cliente]
HAVING ([Clasif_ neumático] <> '')
Crear el modelo Venta
Utilizamos Code First desde base de datos para generar la entidad Venta a partir de la vista anterior, siguiendo los siguientes pasos:
- En la carpta Models –> Agregar / Nuevo elemento…
- ADO.Net Entity Data Model, con nombre = Ventas.
- Code First desde base de datos.
- Seleccionar la conexión de base de datos que nos interese.
- Seleccionar la vista CRM_Ventas
Debe quedarnos algo así:
namespace NeumaliaCRM.Models
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Spatial;
[Table("CRM_Ventas")]
public partial class Venta
{
[Key]
[Column(Order = 0)]
[StringLength(10)]
public string Zona { get; set; }
[Key]
[Column(Order = 1)]
[StringLength(30)]
public string Gama { get; set; }
[Key]
[Column(Order = 2)]
[StringLength(10)]
public string Marca { get; set; }
[Key]
[Column(Order = 3)]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Año { get; set; }
[Key]
[Column(Order = 4)]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Mes { get; set; }
[Key]
[Column(Order = 5)]
[StringLength(20)]
public string IdCliente { get; set; }
[Key]
[Column(Order = 6)]
[StringLength(30)]
public string NombreCliente { get; set; }
public decimal? Importe { get; set; }
public int? Cantidad { get; set; }
}
}
Crear el viewmodel TopVentasViewModel
La vista mostrará el top ventas por marca (tal como se muestra en la figura) y también el top ventas por cliente. Crearemos un viewmodel con la siguiente definición:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
namespace NeumaliaCRM.ViewModels
{
public class TopVentasViewModel
{
public TopVentasViewModel(IList<VentaClienteViewModel> topClientes, IList<VentaMarcaViewModel> topMarcas)
{
TopClientes = topClientes;
TopMarcas = topMarcas;
}
public virtual IList<VentaClienteViewModel> TopClientes { get; set; }
public virtual IList<VentaMarcaViewModel> TopMarcas { get; set; }
}
public class VentaClienteViewModel
{
public string NombreCliente { get; set; }
public decimal? Importe { get; set; }
public int? Cantidad { get; set; }
}
public class VentaMarcaViewModel
{
public string Marca { get; set; }
public string Gama { get; set; }
public decimal? Importe { get; set; }
public int? Cantidad { get; set; }
}
}
Añadir el método Top al controlador VentasController
//************//
// Top ventas //
//************//
public ActionResult Top(int? año = 2014)
{
var currentUserId = User.Identity.GetUserId();
var currentUser = _userManager.FindById(User.Identity.GetUserId());
var currentUserZonas = currentUser.Zonas.Select(z => z.ZonaId);
var topClientes = _ctx.Ventas
.Where(v => v.Año == año)
.Where(v => currentUserZonas.Contains(v.Zona)) // Incluir sólo los de las zonas del comercial logado
.GroupBy(v => v.IdCliente)
.Select(v => new VentaClienteViewModel()
{
NombreCliente = v.FirstOrDefault().NombreCliente,
Importe = v.Sum(g => g.Importe),
Cantidad = v.Sum(g => g.Cantidad)
})
.OrderByDescending(c => c.Importe)
.Take(10)
.ToList();
//////////////////////
// Ventas por marca //
//////////////////////
var ventasPorMarca = _ctx.Ventas
.Where(v => v.Año == año)
.Where(v => currentUserZonas.Contains(v.Zona)) // Incluir sólo los de las zonas del comercial logado
.GroupBy(v => v.Marca)
.Select(v => new VentaMarcaViewModel()
{
Marca = v.FirstOrDefault().Marca,
Gama = v.FirstOrDefault().Gama,
Importe = v.Sum(g => g.Importe),
Cantidad = v.Sum(g => g.Cantidad)
})
.OrderByDescending(c => c.Importe);
// Top 10 ventas por marca
var topVentasPorMarca = ventasPorMarca
.Take(10) // Top 10
.ToList();
// Resto: consolidar como uno sólo
var restoTopVentasPorMarca = ventasPorMarca
.Skip(10)
.Select(v => new { Marca = "Resto", v.Importe, v.Cantidad })
.GroupBy(v => v.Marca) // Agrupar el resto
.Select(v => new VentaMarcaViewModel()
{
Marca = "Resto",
Gama = "",
Importe = v.Sum(g => g.Importe),
Cantidad = v.Sum(g => g.Cantidad)
})
.ToList() // Lista con un único el elemento, el consolidado
.FirstOrDefault(); // Coge el primer (y único elemento).
// Añadir a top ventas el consolidado con el resto.
if (restoTopVentasPorMarca != null)
topVentasPorMarca.Add(restoTopVentasPorMarca);
return View(new TopVentasViewModel(
topClientes.ToList(),
topVentasPorMarca.ToList())
);
}
Crear la vista Ventas/Top
En mi caso he creado 2 vistas en la carpeta Views/Ventas: Top, y una parcial, TopMarcas (podría haberse hecho con una sola, pero en mi caso me sirve para luego crear “clones” de TopMarcas) con la siguiente definición para cada una de ellas. Obsérvese que la función $.plot requiere que se le pasen los datos en formato json, como una lista de objetos con las propiedades label y data; con código de servidor recorremos la colección Model.TopMarcas y a continuación utilizamos el helper @Html.Raw(Json.Encode(…)) para transformar el IList pasado desde el controller a un objeto json.
Views/Ventas/Top
@model NeumaliaCRM.ViewModels.TopVentasViewModel
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
@Scripts.Render("~/bundles/wrapbootstrap-ace/js/flot")
<script type="text/javascript">
jQuery(function ($) {
$('.easy-pie-chart.percentage').each(function () {
var $box = $(this).closest('.infobox');
var barColor = $(this).data('color') || (!$box.hasClass('infobox-dark') ? $box.css('color') : 'rgba(255,255,255,0.95)');
var trackColor = barColor == 'rgba(255,255,255,0.95)' ? 'rgba(255,255,255,0.25)' : '#E2E2E2';
var size = parseInt($(this).data('size')) || 50;
$(this).easyPieChart({
barColor: barColor,
trackColor: trackColor,
scaleColor: false,
lineCap: 'butt',
lineWidth: parseInt(size / 10),
animate: /msie\s*(8|7|6)/.test(navigator.userAgent.toLowerCase()) ? false : 1000,
size: size
});
})
$('.sparkline').each(function () {
var $box = $(this).closest('.infobox');
var barColor = !$box.hasClass('infobox-dark') ? $box.css('color') : '#FFF';
$(this).sparkline('html', { tagValuesAttribute: 'data-values', type: 'bar', barColor: barColor, chartRangeMin: $(this).data('min') || 0 });
});
var placeholder = $('#piechart-placeholder').css({ 'width': '90%', 'min-height': '150px' });
/** Recoger datos y dibujar **/
@{
var topMarcasJsonForPieChart = new List<object>();
foreach (var m in Model.TopMarcas)
{
topMarcasJsonForPieChart.Add(new { label = m.Marca, data = m.Importe });
}
}
var data = @Html.Raw(Json.Encode(topMarcasJsonForPieChart))
function drawPieChart(placeholder, data, position) {
$.plot(placeholder,
data,
{
series: {
pie: {
show: true,
tilt: 0.8,
highlight: {
opacity: 0.25
},
stroke: {
color: '#fff',
width: 2
},
startAngle: 2
}
},
legend: {
show: true,
position: position || "ne",
labelBoxBorderColor: null,
margin: [-30, 15]
},
grid: {
hoverable: true,
clickable: true
}
})
}
drawPieChart(placeholder, data);
/**
we saved the drawing function and the data to redraw with different position later when switching to RTL mode dynamically
so that's not needed actually.
*/
placeholder.data('chart', data);
placeholder.data('draw', drawPieChart);
var $tooltip = $("<div class='tooltip top in'><div class='tooltip-inner'></div></div>").hide().appendTo('body');
var previousPoint = null;
placeholder.on('plothover', function (event, pos, item) {
if (item) {
if (previousPoint != item.seriesIndex) {
previousPoint = item.seriesIndex;
var tip = item.series['label'] + " : " + item.series['percent'] + '%';
$tooltip.show().children(0).text(tip);
}
$tooltip.css({ top: pos.pageY + 10, left: pos.pageX + 10 });
} else {
$tooltip.hide();
previousPoint = null;
}
});
$('#recent-box [data-rel="tooltip"]').tooltip({ placement: tooltip_placement });
function tooltip_placement(context, source) {
var $source = $(source);
var $parent = $source.closest('.tab-content')
var off1 = $parent.offset();
var w1 = $parent.width();
var off2 = $source.offset();
var w2 = $source.width();
if (parseInt(off2.left) < parseInt(off1.left) + parseInt(w1 / 2)) return 'right';
return 'left';
}
$('.dialogs,.comments').slimScroll({
height: '300px'
});
})
</script>
</head>
<body>
@Html.Partial("TopClientes", Model.TopClientes)
@Html.Partial("TopMarcas", Model.TopMarcas)
</body>
</html>
Views/Ventas/TopMarcas
@model IEnumerable<NeumaliaCRM.ViewModels.VentaMarcaViewModel>
<div class="col-sm-5">
<div class="widget-box">
<div class="widget-header widget-header-flat widget-header-small">
<h5>
<i class="icon-signal"></i>
Top 10 marcas
</h5>
<div class="widget-toolbar no-border">
<button class="btn btn-minier btn-primary dropdown-toggle" data-toggle="dropdown">
Este año
<i class="icon-angle-down icon-on-right bigger-110"></i>
</button>
<ul class="dropdown-menu pull-right dropdown-125 dropdown-lighter dropdown-caret">
<li class="active">
<a href="#" class="blue">
<i class="icon-caret-right bigger-110"> </i>
Este año
</a>
</li>
<li>
<a href="#">
<i class="icon-caret-right bigger-110 invisible"> </i>
Año anterior
</a>
</li>
<li>
<a href="#">
<i class="icon-caret-right bigger-110 invisible"> </i>
Este mes
</a>
</li>
<li>
<a href="#">
<i class="icon-caret-right bigger-110 invisible"> </i>
Siempre
</a>
</li>
</ul>
</div>
</div>
<div class="widget-body">
<div class="widget-main">
<div>
<table class="table">
<tr>
<th>
@Html.DisplayNameFor(model => model.Marca)
</th>
<th>
@Html.DisplayNameFor(model => model.Gama)
</th>
<th>
@Html.DisplayNameFor(model => model.Importe)
</th>
<th>
@Html.DisplayNameFor(model => model.Cantidad)
</th>
<th></th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Marca)
</td>
<td>
@Html.DisplayFor(modelItem => item.Gama)
</td>
<td>
@Html.DisplayFor(modelItem => item.Importe)
</td>
<td>
@Html.DisplayFor(modelItem => item.Cantidad)
</td>
</tr>
}
</table>
</div>
<div id="piechart-placeholder"></div>
<div class="hr hr8 hr-double"></div>
<div class="clearfix">
<div class="grid3">
<span class="grey">
<i class="icon-facebook-sign icon-2x blue"></i>
likes
</span>
<h4 class="bigger pull-right">1,255</h4>
</div>
<div class="grid3">
<span class="grey">
<i class="icon-twitter-sign icon-2x purple"></i>
tweets
</span>
<h4 class="bigger pull-right">941</h4>
</div>
<div class="grid3">
<span class="grey">
<i class="icon-pinterest-sign icon-2x red"></i>
pins
</span>
<h4 class="bigger pull-right">1,050</h4>
</div>
</div>
</div><!-- /widget-main -->
</div><!-- /widget-body -->
</div><!-- /widget-box -->
</div>