miércoles, mayo 21, 2014

jQueryFlot en ASP.net (II) + AJAX

Siguiendo con la entrada anterior, supongamos ahora que queremos actualizar los datos de la página en función de, por ejemplo, que se seleccione un rango de fechas u otro. Y clara está, esta actualización queremos hacerla sin tener que recargar la página, es decir, mediante AJAX. Tendremos que hacer las siguientes modificaciones:

  • Nueva función TopMarcasJson en el controlador VentasController que retorne un objeto de tipo JsonResult que será consumido directamente por la función $.plot desde la vista
  • Modificar la función Top del controlador VentasController para que pase la colección TopMarcas a la vista (será la vista la que vaya a buscar los datos del modelo con la función anterior).
  • Modificar la vista Ventas/Top para que la función $.plot tome los datos mediante AJAX.

Nueva función TopMarcasJson en VentasController

        /// <summary>
///
Retorna JSON que se consume mediante AJAX desde la función $.plot de la vista Ventas/Top
/// </summary>
/// <returns></returns>
[HttpGet]
public JsonResult TopMarcasJson(int año)
{
var currentUserId = User.Identity.GetUserId();
var currentUser = _userManager.FindById(User.Identity.GetUserId());
var currentUserZonas = currentUser.Zonas.Select(z => z.ZonaId);

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);

// Convertir a Json de tipo array de {label, data}
return Json(
topVentasPorMarca.Select(v =>
new { label = v.Marca, data = v.Importe }),
JsonRequestBehavior.AllowGet
);
}

Modificar la función Top de VentasController


Ya no hará falta pasar la colección TopMarcas desde esta función, por lo que podemos suprimir el fragmento de código que hace referencia a eso y retornar la vista vacía (sin modelo).


Modificar script de la vista Ventas/Top


Debemos sustituir el fragmento bajo /** Recoger datos y dibujar **/ por el nuevo:

        function drawPieChart(placeholder, data0, position) {
$.getJSON(
"TopMarcasJson", function (data) {
//succes - data loaded, now use plot:
$.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);
Obsérvese cómo se llama a la función $.getJSON y el retorno obtenido se le pasa a $.plot.

jQueryFlot en ASP.net

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.).

image

 

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:



  1. En la carpta Models –> Agregar / Nuevo elemento…

  2. ADO.Net Entity Data Model, con nombre = Ventas.

  3. Code First desde base de datos.

  4. Seleccionar la conexión de base de datos que nos interese.

  5. 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">&nbsp;</i>
Este año
</a>
</
li>

<
li>
<
a href="#">
<
i class="icon-caret-right bigger-110 invisible">&nbsp;</i>
Año anterior
</a>
</
li>

<
li>
<
a href="#">
<
i class="icon-caret-right bigger-110 invisible">&nbsp;</i>
Este mes
</a>
</
li>

<
li>
<
a href="#">
<
i class="icon-caret-right bigger-110 invisible">&nbsp;</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>
&nbsp; 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>
&nbsp; 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>
&nbsp; pins
</span>
<
h4 class="bigger pull-right">1,050</h4>
</
div>
</
div>
</
div><!-- /widget-main -->
</div><!-- /widget-body -->
</div><!-- /widget-box -->
</div>

 

martes, mayo 20, 2014

Mini-CRM en ASP.Net (IV) “En construcción!”

Añadiendo la entidad VentasPorMarca

Los comerciales necesitan el cuadro de ventas por marca, en el que aparezcan cantidad e importe del año en curso y del anterior. Mis datos están en una tabla en la que cada línea es una agrupación de ventas por ejercicio, mes, zona y marca. Lo primero que necesito es una vista en que se muestren las ventas por zona y marca, del año en curso y del anterior. La creo en la base de datos así:

CREATE VIEW [dbo].[CRM_VentasPorMarcaYZona]
WITH SCHEMABINDING
AS
SELECT TOP
(100) PERCENT zm.[Clasif_ neumático] as Gama, zm.Marca, zm.Comercial as Zona, vPrev.i AS ImporteAnt, vPrev.c AS CantidadAnt, vCurr.i AS ImporteAct
,
vCurr.c AS
CantidadAct
FROM (SELECT Marca, [Clasif_ neumático],
Comercial
FROM dbo.[Cuadro mando Neumalia] AS
c
GROUP BY [Clasif_ neumático], Marca,
Comercial
HAVING ([Clasif_ neumático] <> '')) AS zm
LEFT OUTER JOIN
(
SELECT Año, Marca AS m, Comercial AS z, SUM([Imp_ ventas]) AS i, SUM(cast([Cdad_ ventas] as integer)) AS
c
FROM dbo.
[Cuadro mando Neumalia]
GROUP BY Año, Marca,
Comercial
HAVING (Año = DATEPART(year, GETDATE()) - 1)) AS vPrev ON zm.Marca = vPrev.m AND zm.Comercial = vPrev.z
LEFT OUTER JOIN
(
SELECT Año, Marca AS m, Comercial AS z, SUM([Imp_ ventas]) AS i, SUM(cast([Cdad_ ventas] as integer)) AS
c
FROM dbo.[Cuadro mando Neumalia] AS
[Cuadro mando Neumalia_1]
GROUP BY Año, Marca,
Comercial
HAVING (Año = DATEPART(year, GETDATE()))) AS vCurr ON zm.Marca = vCurr.m AND zm.Comercial = vCurr.
z
ORDER BY zm.Marca

A continuación creo la clase VentasPorMarca:

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_VentasPorMarcaYZona"
)]
public partial class
VentasPorMarca
{
[
Key
]
[
Column
(Order = 0)]
[
StringLength
(30)]
public string Gama { get; set
; }

[
Key
]
[
Column
(Order = 1)]
[
StringLength
(10)]
public string Marca { get; set
; }

[
Key
]
[
Column
(Order = 2)]
[
StringLength
(10)]
public string Zona { get; set
; }

public decimal? ImporteAnt { get; set
; }

public int? CantidadAnt { get; set
; }

public decimal? ImporteAct { get; set
; }

public int? CantidadAct { get; set
; }
}
}

También se puede crear a partir de la vista añadiendo

viernes, mayo 02, 2014

Mini-CRM en ASP.Net (III)

Mostrar ventas del comercial logado

Antes de nada, algunos links interesantes:

 

En este apartado vamos a ver cómo hacemos un volcado de las ventas pertenecientes al comercial logado en la aplicación.

Ya comenté que cada comercial tiene asignadas unas zonas y cada venta están asociada a una zona (y no directamente a un comercial). Esto quizás suene un poco raro, pero es como lo tenemos en mi empresa… Guiño Por lo tanto, tenemos una relación N a M entre comerciales y ventas a través de las zonas. He creado una nueva tabla AspNetUsers_Zonas que relaciona los comerciales (a través del User_Id) con las ventas (a través del ZonaId):

CREATE TABLE [dbo].[AspNetUsers_Zonas](
[User_Id] [nvarchar](128
) NOT NULL,
[ZonaId] [nvarchar](10
) NOT NULL,
CONSTRAINT [PK_dbo.AspNetUsers_ZonasNeumalia]
PRIMARY KEY CLUSTERED
(
[User_Id] ASC
,
[ZonaId]
ASC
)

En el explorador de soluciones de VS, sobre la carpeta Models clicar botón derecho y Agregar Nuevo elemento. Seleccionar ADO.NET Entity Data Model y en nombre escribir Zona.


image


 


Utilizaremos Codefirst (para más info ver codefirst reverse ingeniering)


 


image


image


 


Se ha generado la nueva entidad Zona. Abrir el fichero y modificarlo tal como se muestra a continuación:


namespace NeumaliaCRM.Models
{
using
System;
using
System.Collections.Generic;
using
System.ComponentModel.DataAnnotations;
using
System.ComponentModel.DataAnnotations.Schema;
using
System.Data.Entity.Spatial;

[
Table("AspNetUsers_ZonasNeumalia"
)]
public partial class
Zona
{
//[Key]
//[Column("MiCRM_User_Id", Order = 0)]
//public string UserId { get; set; }

[Key
]
[
Column
(Order = 1)]
[
StringLength
(10)]
public string ZonaId { get; set
; }

[
Key
]
[
Column("NeumaliaCRM_User_Id"
, Order = 0)]
public virtual NeumaliaCRM_User User { get; set
; }
}
}




Y modificar también el fichero IdentityModels:


using Microsoft.AspNet.Identity.EntityFramework;
using
System.Data.Entity;
using
NeumaliaCRM.Models;
using
System.Collections.Generic;

namespace
NeumaliaCRM.Models
{
// You can add profile data for the user by adding more properties to your ApplicationUser class (NeumaliaCRM_User), please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
public class NeumaliaCRM_User :
IdentityUser
{
public virtual IList<Zona> Zonas { get; set
; }
}

public class MyDbContext : IdentityDbContext<NeumaliaCRM_User
>
{
public
MyDbContext()
:
base("AESQL2008ConnectionString"
)
{
}

public DbSet<Venta> Ventas { get; set
; }
}
}

A continuación creamos mediante scaffolding un nuevo controlador y nueva vista para las ventas: botón derecho sobre Controllers, Agregar Controller. Abrir el nuevo controlador y añadir el código necesario que se muestra a continuación:


using System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Web;
using
System.Web.Mvc;
using
NeumaliaCRM.Models;
using
Microsoft.AspNet.Identity;
using
Microsoft.AspNet.Identity.EntityFramework;

namespace
NeumaliaCRM.Controllers
{
[
Authorize
]
public class VentasController :
Controller
{
private MyDbContext
_ctx;
private UserManager<NeumaliaCRM_User
> _userManager;

public
VentasController()
{
_ctx =
new MyDbContext
();
_userManager =
new UserManager<NeumaliaCRM_User>(new UserStore<NeumaliaCRM_User
>(_ctx));
}

//
// GET: /CuadroDeMando/Ventas/
public ActionResult
Index()
{
var
currentUserId = User.Identity.GetUserId();
var
currentUser = _userManager.FindById(User.Identity.GetUserId());
var currentUserZonas = currentUser.Zonas.Select(z => z.ZonaId);

var
ventas = _ctx.Ventas.Where(v => currentUserZonas.Contains(v.ZonaId)).Select(v => v);

return
View(ventas);
}