Graficos de ventas
This commit is contained in:
parent
51ba861ae0
commit
bc07e3ebd9
@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
using WebVentaCoche.DataBase;
|
||||
using WebVentaCoche.Enums;
|
||||
using WebVentaCoche.ViewModels;
|
||||
@ -16,13 +17,32 @@ namespace WebVentaCoche.Controllers
|
||||
_context = context;
|
||||
}
|
||||
|
||||
//GET:/Sales/
|
||||
public IActionResult Index()
|
||||
//GET:/Sales/HistoricalSales
|
||||
public IActionResult HistoricalSales()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
//GET:/Sales/GetMonthlyData
|
||||
//GET:/Sales/Last30DaysSales
|
||||
public IActionResult Last30DaysSales()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
//GET:/Sales/MonthSales
|
||||
public async Task<IActionResult> MonthSales()
|
||||
{
|
||||
var meses = await _context.OrderDetails.Where(d => d.Order.Status == OrderStatus.Delivered)
|
||||
.Select(d => new { d.Order.OrderDate.Year, d.Order.OrderDate.Month }).Distinct().ToListAsync();
|
||||
|
||||
ViewBag.Months = meses
|
||||
.Select(x => $"{x.Year}-{x.Month:D2}")
|
||||
.OrderBy(s => s)
|
||||
.ToList();
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetMonthlyData()
|
||||
{
|
||||
@ -43,5 +63,72 @@ namespace WebVentaCoche.Controllers
|
||||
return Json(data);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetLast30DaysData()
|
||||
{
|
||||
var desde = DateTime.UtcNow.AddDays(-30);
|
||||
|
||||
var raw = await _context.OrderDetails
|
||||
.Where(d => d.Order.Status == OrderStatus.Delivered
|
||||
&& d.Order.OrderDate >= desde)
|
||||
.GroupBy(d => new
|
||||
{
|
||||
d.Product.Name,
|
||||
Date = d.Order.OrderDate.Date
|
||||
})
|
||||
.Select(g => new
|
||||
{
|
||||
ProductName = g.Key.Name!,
|
||||
Date = g.Key.Date,
|
||||
Units = g.Sum(x => x.Quantity)
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var data = raw
|
||||
.Select(x => new
|
||||
{
|
||||
productName = x.ProductName,
|
||||
date = x.Date.ToString("yyyy-MM-dd"),
|
||||
units = x.Units
|
||||
});
|
||||
|
||||
return Json(data);
|
||||
}
|
||||
|
||||
//GET:/Sales/GetMonthData?month=2025-04
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetSalesByMonth(string month)
|
||||
{
|
||||
if (string.IsNullOrEmpty(month))
|
||||
return BadRequest();
|
||||
|
||||
var parts = month.Split('-');
|
||||
if (parts.Length != 2 ||
|
||||
!int.TryParse(parts[0], out var year) ||
|
||||
!int.TryParse(parts[1], out var mon))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var data = await _context.OrderDetails
|
||||
.Where(d =>
|
||||
d.Order.Status == OrderStatus.Delivered &&
|
||||
d.Order.OrderDate.Year == year &&
|
||||
d.Order.OrderDate.Month == mon
|
||||
)
|
||||
.GroupBy(d => d.Product.Name)
|
||||
.Select(g => new
|
||||
{
|
||||
productName = g.Key,
|
||||
units = g.Sum(x => x.Quantity),
|
||||
revenue = g.Sum(x => x.Quantity * x.UnitPrice)
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Json(data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
204
WebVentaCoche/Views/Sales/HistoricalSales.cshtml
Normal file
204
WebVentaCoche/Views/Sales/HistoricalSales.cshtml
Normal file
@ -0,0 +1,204 @@
|
||||
@{
|
||||
ViewData["Title"] = "Ventas Mensuales";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="card p-4" style="background-color: rgba(255,255,255,0.8); border-radius: 10px;">
|
||||
<h2 class="text-center mb-4">@ViewData["Title"]</h2>
|
||||
|
||||
<div id="salesTableContainer" class="table-responsive mb-4"></div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="productSelect" class="form-label">Productos</label>
|
||||
<select id="productSelect" class="form-select" multiple size="5"></select>
|
||||
<small class="form-text text-muted">
|
||||
Mantén Ctrl para seleccionar varios productos.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="legend" class="mb-4"></div>
|
||||
|
||||
<div id="salesChart" style="position: relative;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script>
|
||||
d3.json('@Url.Action("GetMonthlyData", "Sales")').then(rawData => {
|
||||
const salesData = d3.group(rawData, d => d.productName);
|
||||
const margin = { top: 30, right: 20, bottom: 60, left: 60 },
|
||||
width = 800 - margin.left - margin.right,
|
||||
height = 450 - margin.top - margin.bottom;
|
||||
|
||||
const svg = d3.select("#salesChart")
|
||||
.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", `translate(${margin.left},${margin.top})`);
|
||||
|
||||
const x0 = d3.scaleBand().range([0, width]).padding(0.2),
|
||||
x1 = d3.scaleBand().padding(0.05),
|
||||
y = d3.scaleLinear().range([height, 0]);
|
||||
|
||||
const xAxisG = svg.append("g").attr("transform", `translate(0,${height})`);
|
||||
const yAxisG = svg.append("g");
|
||||
|
||||
const allProducts = Array.from(salesData.keys());
|
||||
const color = d3.scaleOrdinal()
|
||||
.domain(allProducts)
|
||||
.range(d3.schemeCategory10);
|
||||
|
||||
const productSelect = d3.select("#productSelect");
|
||||
productSelect.selectAll("option")
|
||||
.data(allProducts)
|
||||
.join("option")
|
||||
.attr("value", d => d)
|
||||
.text(d => d);
|
||||
|
||||
const tooltip = d3.select("body")
|
||||
.append("div")
|
||||
.attr("id", "tooltip");
|
||||
|
||||
function updateTable(selected, months) {
|
||||
const container = d3.select("#salesTableContainer");
|
||||
container.selectAll("*").remove();
|
||||
if (!selected.length) return;
|
||||
|
||||
const table = container.append("table")
|
||||
.attr("class", "table table-bordered");
|
||||
const thead = table.append("thead");
|
||||
const tbody = table.append("tbody");
|
||||
|
||||
const headerRow = thead.append("tr");
|
||||
headerRow.append("th").text("Producto");
|
||||
months.forEach(m => headerRow.append("th").text(m));
|
||||
|
||||
selected.forEach(prod => {
|
||||
const row = tbody.append("tr");
|
||||
row.append("td").text(prod);
|
||||
months.forEach(m => {
|
||||
const rec = salesData.get(prod).find(d => d.month === m);
|
||||
row.append("td").text(rec ? rec.units : 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateChart(selected) {
|
||||
d3.select("#legend").selectAll("*").remove();
|
||||
if (!selected.length) {
|
||||
svg.selectAll(".barGroup").remove();
|
||||
xAxisG.call(d3.axisBottom(x0));
|
||||
yAxisG.call(d3.axisLeft(y));
|
||||
d3.select("#salesTableContainer").selectAll("*").remove();
|
||||
return;
|
||||
}
|
||||
|
||||
selected.forEach(key => {
|
||||
const item = d3.select("#legend")
|
||||
.append("div")
|
||||
.style("display", "flex")
|
||||
.style("align-items", "center")
|
||||
.style("margin-right", "1rem");
|
||||
item.append("div")
|
||||
.style("width", "16px")
|
||||
.style("height", "16px")
|
||||
.style("background", color(key))
|
||||
.style("margin-right", "6px");
|
||||
item.append("span").text(key);
|
||||
});
|
||||
|
||||
const months = Array.from(new Set(
|
||||
selected.flatMap(p => salesData.get(p).map(d => d.month))
|
||||
)).sort();
|
||||
|
||||
updateTable(selected, months);
|
||||
|
||||
const data = months.map(month => {
|
||||
const obj = { month };
|
||||
selected.forEach(p => {
|
||||
const rec = salesData.get(p).find(d => d.month === month);
|
||||
obj[p] = rec ? rec.units : 0;
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
|
||||
x0.domain(months);
|
||||
x1.domain(selected).range([0, x0.bandwidth()]);
|
||||
y.domain([0, d3.max(data, d => d3.max(selected, p => d[p])) * 1.1]);
|
||||
|
||||
xAxisG.call(d3.axisBottom(x0))
|
||||
.selectAll("text")
|
||||
.attr("transform", "rotate(-40)")
|
||||
.style("text-anchor", "end");
|
||||
yAxisG.call(d3.axisLeft(y));
|
||||
|
||||
const groups = svg.selectAll(".barGroup")
|
||||
.data(data, d => d.month);
|
||||
|
||||
const groupsEnter = groups.join(
|
||||
enter => enter.append("g")
|
||||
.attr("class", "barGroup")
|
||||
.attr("transform", d => `translate(${x0(d.month)},0)`),
|
||||
update => update,
|
||||
exit => exit.remove()
|
||||
);
|
||||
|
||||
groupsEnter.selectAll("rect")
|
||||
.data(d => selected.map(p => ({
|
||||
key: p,
|
||||
value: d[p],
|
||||
month: d.month
|
||||
})))
|
||||
.join(
|
||||
enter => enter.append("rect")
|
||||
.attr("x", d => x1(d.key))
|
||||
.attr("y", height)
|
||||
.attr("width", x1.bandwidth())
|
||||
.attr("height", 0)
|
||||
.attr("fill", d => color(d.key))
|
||||
.on("mouseover", (e, d) => {
|
||||
tooltip.html(`<strong>${d.key}</strong><br>${d.month}: ${d.value}`)
|
||||
.style("opacity", 1);
|
||||
})
|
||||
.on("mousemove", e => {
|
||||
tooltip.style("left", (e.pageX + 10) + "px")
|
||||
.style("top", (e.pageY - 28) + "px");
|
||||
})
|
||||
.on("mouseout", () => {
|
||||
tooltip.style("opacity", 0);
|
||||
})
|
||||
.call(g => g.transition().duration(600)
|
||||
.attr("y", d => y(d.value))
|
||||
.attr("height", d => height - y(d.value))
|
||||
),
|
||||
update => update.call(u => u.transition().duration(600)
|
||||
.attr("x", d => x1(d.key))
|
||||
.attr("y", d => y(d.value))
|
||||
.attr("width", x1.bandwidth())
|
||||
.attr("height", d => height - y(d.value))
|
||||
.attr("fill", d => color(d.key))
|
||||
),
|
||||
exit => exit.call(e => e.transition().duration(300)
|
||||
.attr("y", height)
|
||||
.attr("height", 0)
|
||||
.remove()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
productSelect.on("change", function () {
|
||||
const sel = Array.from(this.selectedOptions).map(o => o.value);
|
||||
updateChart(sel);
|
||||
});
|
||||
|
||||
productSelect.selectAll("option").property("selected", true);
|
||||
updateChart(allProducts);
|
||||
})
|
||||
.catch(err => console.error("Error cargando datos de ventas:", err));
|
||||
</script>
|
||||
}
|
||||
@ -1,176 +0,0 @@
|
||||
@{
|
||||
ViewData["Title"] = "Ventas Mensuales";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="container my-4">
|
||||
<h2 class="text-center mb-4">@ViewData["Title"]</h2>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="productSelect" class="form-label">Productos</label>
|
||||
<select id="productSelect" class="form-select" multiple size="5"></select>
|
||||
<small class="form-text text-muted">
|
||||
Mantén Ctrl para seleccionar varios productos.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="legend"></div>
|
||||
|
||||
<div id="salesChart" style="position: relative;"></div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script>
|
||||
d3.json('@Url.Action("GetMonthlyData","Sales")').then(rawData => {
|
||||
//Agrupamos en un Map<string, Array<{month,units}>>
|
||||
const salesData = d3.group(rawData, d => d.productName);
|
||||
|
||||
const margin = { top: 30, right: 20, bottom: 60, left: 60 },
|
||||
width = 800 - margin.left - margin.right,
|
||||
height = 450 - margin.top - margin.bottom;
|
||||
|
||||
const svg = d3.select("#salesChart")
|
||||
.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", `translate(${margin.left},${margin.top})`);
|
||||
|
||||
const x0 = d3.scaleBand().range([0, width]).padding(0.2),
|
||||
x1 = d3.scaleBand().padding(0.05),
|
||||
y = d3.scaleLinear().range([height, 0]);
|
||||
|
||||
const xAxisG = svg.append("g")
|
||||
.attr("transform", `translate(0,${height})`);
|
||||
const yAxisG = svg.append("g");
|
||||
|
||||
const allProducts = Array.from(salesData.keys());
|
||||
const color = d3.scaleOrdinal()
|
||||
.domain(allProducts)
|
||||
.range(d3.schemeCategory10);
|
||||
|
||||
const productSelect = d3.select("#productSelect");
|
||||
productSelect
|
||||
.selectAll("option")
|
||||
.data(allProducts)
|
||||
.join("option")
|
||||
.attr("value", d => d)
|
||||
.text(d => d);
|
||||
|
||||
const tooltip = d3.select("body")
|
||||
.append("div")
|
||||
.attr("id", "tooltip");
|
||||
|
||||
function updateChart(selected) {
|
||||
d3.select("#legend").selectAll("*").remove();
|
||||
|
||||
if (!selected.length) {
|
||||
svg.selectAll(".barGroup").remove();
|
||||
xAxisG.call(d3.axisBottom(x0));
|
||||
yAxisG.call(d3.axisLeft(y));
|
||||
return;
|
||||
}
|
||||
|
||||
selected.forEach(key => {
|
||||
const item = d3.select("#legend")
|
||||
.append("div")
|
||||
.style("display","flex")
|
||||
.style("align-items","center");
|
||||
item.append("div")
|
||||
.style("background", color(key));
|
||||
item.append("span").text(key);
|
||||
});
|
||||
|
||||
const months = Array.from(new Set(
|
||||
selected.flatMap(p => salesData.get(p).map(d => d.month))
|
||||
)).sort();
|
||||
|
||||
const data = months.map(month => {
|
||||
const obj = { month };
|
||||
selected.forEach(p => {
|
||||
const rec = salesData.get(p).find(d => d.month === month);
|
||||
obj[p] = rec ? rec.units : 0;
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
|
||||
x0.domain(months);
|
||||
x1.domain(selected).range([0, x0.bandwidth()]);
|
||||
y.domain([0, d3.max(data, d => d3.max(selected, p => d[p])) * 1.1]);
|
||||
|
||||
xAxisG.call(d3.axisBottom(x0))
|
||||
.selectAll("text")
|
||||
.attr("transform","rotate(-40)")
|
||||
.style("text-anchor","end");
|
||||
yAxisG.call(d3.axisLeft(y));
|
||||
|
||||
const groups = svg.selectAll(".barGroup")
|
||||
.data(data, d => d.month);
|
||||
|
||||
const groupsEnter = groups.join(
|
||||
enter => enter.append("g")
|
||||
.attr("class","barGroup")
|
||||
.attr("transform", d => `translate(${x0(d.month)},0)`),
|
||||
update => update,
|
||||
exit => exit.remove()
|
||||
);
|
||||
|
||||
groupsEnter.selectAll("rect")
|
||||
.data(d => selected.map(p => ({
|
||||
key: p,
|
||||
value: d[p],
|
||||
month: d.month
|
||||
})))
|
||||
.join(
|
||||
enter => enter.append("rect")
|
||||
.attr("x", d => x1(d.key))
|
||||
.attr("y", height)
|
||||
.attr("width", x1.bandwidth())
|
||||
.attr("height", 0)
|
||||
.attr("fill", d => color(d.key))
|
||||
.on("mouseover",(e,d)=>{
|
||||
tooltip.html(`<strong>${d.key}</strong><br>${d.month}: ${d.value}`)
|
||||
.style("opacity",1);
|
||||
})
|
||||
.on("mousemove",e=>{
|
||||
tooltip.style("left",(e.pageX+10)+"px")
|
||||
.style("top",(e.pageY-28)+"px");
|
||||
})
|
||||
.on("mouseout",()=>{
|
||||
tooltip.style("opacity",0);
|
||||
})
|
||||
.call(g => g.transition().duration(600)
|
||||
.attr("y", d => y(d.value))
|
||||
.attr("height", d => height - y(d.value))
|
||||
),
|
||||
|
||||
update => update.call(u => u.transition().duration(600)
|
||||
.attr("x", d => x1(d.key))
|
||||
.attr("y", d => y(d.value))
|
||||
.attr("width", x1.bandwidth())
|
||||
.attr("height", d => height - y(d.value))
|
||||
.attr("fill", d => color(d.key))
|
||||
),
|
||||
|
||||
exit => exit.call(e => e.transition().duration(300)
|
||||
.attr("y", height)
|
||||
.attr("height", 0)
|
||||
.remove()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
productSelect.on("change", function(){
|
||||
const sel = Array.from(this.selectedOptions).map(o=>o.value);
|
||||
updateChart(sel);
|
||||
});
|
||||
|
||||
productSelect.selectAll("option").property("selected", true);
|
||||
updateChart(allProducts);
|
||||
})
|
||||
.catch(err => console.error("Error cargando datos de ventas:", err));
|
||||
</script>
|
||||
}
|
||||
201
WebVentaCoche/Views/Sales/Last30DaysSales.cshtml
Normal file
201
WebVentaCoche/Views/Sales/Last30DaysSales.cshtml
Normal file
@ -0,0 +1,201 @@
|
||||
@{
|
||||
ViewData["Title"] = "Ventas Últimos 30 Días";
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="card p-4" style="background-color: rgba(255,255,255,0.8); border-radius: 10px;">
|
||||
<h2 class="text-center mb-4">@ViewData["Title"]</h2>
|
||||
|
||||
<div id="last30TableContainer" class="table-responsive mb-4"></div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="last30ProductSelect" class="form-label">Productos</label>
|
||||
<select id="last30ProductSelect" class="form-select" multiple size="5"></select>
|
||||
<small class="form-text text-muted">
|
||||
Mantén Ctrl para seleccionar varios productos.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="last30Legend" class="mb-4"></div>
|
||||
|
||||
<div id="last30Chart" style="position: relative;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script>
|
||||
d3.json('@Url.Action("GetLast30DaysData", "Sales")').then(rawData => {
|
||||
console.log("RAW DATA:", rawData);
|
||||
const salesData = d3.group(rawData, d => d.productName);
|
||||
const margin = { top: 30, right: 20, bottom: 60, left: 60 },
|
||||
width = 800 - margin.left - margin.right,
|
||||
height = 450 - margin.top - margin.bottom;
|
||||
|
||||
const svg = d3.select("#last30Chart")
|
||||
.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", `translate(${margin.left},${margin.top})`);
|
||||
|
||||
const x0 = d3.scaleBand().range([0, width]).padding(0.2),
|
||||
x1 = d3.scaleBand().padding(0.05),
|
||||
y = d3.scaleLinear().range([height, 0]);
|
||||
|
||||
const xAxisG = svg.append("g").attr("transform", `translate(0,${height})`);
|
||||
const yAxisG = svg.append("g");
|
||||
|
||||
const allProducts = Array.from(salesData.keys());
|
||||
const color = d3.scaleOrdinal()
|
||||
.domain(allProducts)
|
||||
.range(d3.schemeCategory10);
|
||||
|
||||
const productSelect = d3.select("#last30ProductSelect");
|
||||
productSelect.selectAll("option")
|
||||
.data(allProducts)
|
||||
.join("option")
|
||||
.attr("value", d => d)
|
||||
.text(d => d);
|
||||
|
||||
const tooltip = d3.select("body")
|
||||
.append("div")
|
||||
.attr("id", "tooltip")
|
||||
.style("position", "absolute")
|
||||
.style("padding", "6px 10px")
|
||||
.style("background", "rgba(0,0,0,0.7)")
|
||||
.style("color", "#fff")
|
||||
.style("border-radius", "4px")
|
||||
.style("pointer-events", "none")
|
||||
.style("opacity", 0)
|
||||
.style("z-index", 10000)
|
||||
.style("font-size", "0.9rem");
|
||||
|
||||
function updateTable(selected, dates) {
|
||||
const container = d3.select("#last30TableContainer");
|
||||
container.selectAll("*").remove();
|
||||
if (!selected.length) return;
|
||||
|
||||
const table = container.append("table").attr("class", "table table-bordered");
|
||||
const thead = table.append("thead"), tbody = table.append("tbody");
|
||||
const header = thead.append("tr");
|
||||
header.append("th").text("Producto");
|
||||
dates.forEach(d => header.append("th").text(d));
|
||||
|
||||
selected.forEach(prod => {
|
||||
const row = tbody.append("tr");
|
||||
row.append("td").text(prod);
|
||||
dates.forEach(d => {
|
||||
const rec = salesData.get(prod).find(x => x.date === d);
|
||||
row.append("td").text(rec ? rec.units : 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateChart(selected) {
|
||||
d3.select("#last30Legend").selectAll("*").remove();
|
||||
if (!selected.length) {
|
||||
svg.selectAll(".barGroup").remove();
|
||||
xAxisG.call(d3.axisBottom(x0));
|
||||
yAxisG.call(d3.axisLeft(y));
|
||||
d3.select("#last30TableContainer").selectAll("*").remove();
|
||||
return;
|
||||
}
|
||||
|
||||
selected.forEach(key => {
|
||||
const item = d3.select("#last30Legend")
|
||||
.append("div")
|
||||
.style("display", "flex")
|
||||
.style("align-items", "center")
|
||||
.style("margin-right", "1rem");
|
||||
item.append("div")
|
||||
.style("width", "16px").style("height", "16px")
|
||||
.style("background", color(key))
|
||||
.style("margin-right", "6px");
|
||||
item.append("span").text(key);
|
||||
});
|
||||
|
||||
const dates = Array.from(new Set(
|
||||
selected.flatMap(p => salesData.get(p).map(d => d.date))
|
||||
)).sort();
|
||||
|
||||
updateTable(selected, dates);
|
||||
|
||||
const data = dates.map(date => {
|
||||
const obj = { date };
|
||||
selected.forEach(p => {
|
||||
const rec = salesData.get(p).find(x => x.date === date);
|
||||
obj[p] = rec ? rec.units : 0;
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
|
||||
x0.domain(dates);
|
||||
x1.domain(selected).range([0, x0.bandwidth()]);
|
||||
y.domain([0, d3.max(data, d => d3.max(selected, p => d[p])) * 1.1]);
|
||||
|
||||
xAxisG.call(d3.axisBottom(x0))
|
||||
.selectAll("text")
|
||||
.attr("transform", "rotate(-40)")
|
||||
.style("text-anchor", "end");
|
||||
yAxisG.call(d3.axisLeft(y));
|
||||
|
||||
const groups = svg.selectAll(".barGroup")
|
||||
.data(data, d => d.date);
|
||||
|
||||
const ge = groups.join(
|
||||
enter => enter.append("g")
|
||||
.attr("class", "barGroup")
|
||||
.attr("transform", d => `translate(${x0(d.date)},0)`),
|
||||
update => update, exit => exit.remove()
|
||||
);
|
||||
|
||||
ge.selectAll("rect")
|
||||
.data(d => selected.map(p => ({ key: p, value: d[p], date: d.date })))
|
||||
.join(
|
||||
enter => enter.append("rect")
|
||||
.attr("x", d => x1(d.key))
|
||||
.attr("y", height)
|
||||
.attr("width", x1.bandwidth())
|
||||
.attr("height", 0)
|
||||
.attr("fill", d => color(d.key))
|
||||
.on("mouseover", (e, d) => {
|
||||
tooltip.html(`<strong>${d.key}</strong><br>${d.date}: ${d.value}`)
|
||||
.style("opacity", 1);
|
||||
})
|
||||
.on("mousemove", e => {
|
||||
tooltip.style("left", (e.pageX + 10) + "px")
|
||||
.style("top", (e.pageY - 28) + "px");
|
||||
})
|
||||
.on("mouseout", () => tooltip.style("opacity", 0))
|
||||
.call(g => g.transition().duration(600)
|
||||
.attr("y", d => y(d.value))
|
||||
.attr("height", d => height - y(d.value))
|
||||
),
|
||||
update => update.call(u => u.transition().duration(600)
|
||||
.attr("x", d => x1(d.key))
|
||||
.attr("y", d => y(d.value))
|
||||
.attr("width", x1.bandwidth())
|
||||
.attr("height", d => height - y(d.value))
|
||||
.attr("fill", d => color(d.key))
|
||||
),
|
||||
exit => exit.call(e => e.transition().duration(300)
|
||||
.attr("y", height).attr("height", 0).remove()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
productSelect.on("change", function () {
|
||||
const sel = Array.from(this.selectedOptions).map(o => o.value);
|
||||
updateChart(sel);
|
||||
});
|
||||
|
||||
productSelect.selectAll("option").property("selected", true);
|
||||
updateChart(allProducts);
|
||||
|
||||
}).catch(err => console.error(err));
|
||||
</script>
|
||||
}
|
||||
170
WebVentaCoche/Views/Sales/MonthSales.cshtml
Normal file
170
WebVentaCoche/Views/Sales/MonthSales.cshtml
Normal file
@ -0,0 +1,170 @@
|
||||
@{
|
||||
ViewData["Title"] = "Ventas por Mes";
|
||||
Layout = "_Layout";
|
||||
|
||||
// Leemos con el mismo nombre que en el controlador:
|
||||
var meses = ViewBag.Months as List<string> ?? new List<string>();
|
||||
}
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="card p-4" style="background-color: rgba(255,255,255,0.8); border-radius:10px;">
|
||||
<h2 class="text-center mb-4">@ViewData["Title"]</h2>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="monthSelect" class="form-label">Seleccione un mes</label>
|
||||
<select id="monthSelect" class="form-select">
|
||||
<option selected disabled value="">-- Elija un mes --</option>
|
||||
@foreach (var m in meses)
|
||||
{
|
||||
<option value="@m">@m</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONTENEDOR PARA LA TABLA -->
|
||||
<div id="salesTableContainer" class="table-responsive mb-4"></div>
|
||||
|
||||
<div id="salesChart" style="position: relative;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const apiUrl = '@Url.Action("GetSalesByMonth", "Sales")';
|
||||
const monthSelect = document.getElementById('monthSelect');
|
||||
|
||||
const margin = { top: 30, right: 20, bottom: 80, left: 60 },
|
||||
width = 800 - margin.left - margin.right,
|
||||
height = 450 - margin.top - margin.bottom;
|
||||
|
||||
const svg = d3.select("#salesChart")
|
||||
.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", `translate(${margin.left},${margin.top})`);
|
||||
|
||||
const x = d3.scaleBand().range([0, width]).padding(0.2),
|
||||
y = d3.scaleLinear().range([height, 0]),
|
||||
xAxisG = svg.append("g").attr("transform", `translate(0,${height})`),
|
||||
yAxisG = svg.append("g"),
|
||||
color = d3.scaleOrdinal(d3.schemeCategory10);
|
||||
|
||||
const tooltip = d3.select("body")
|
||||
.append("div")
|
||||
.attr("id", "tooltip")
|
||||
.style("position", "absolute")
|
||||
.style("pointer-events", "none")
|
||||
.style("padding", "6px 10px")
|
||||
.style("background", "rgba(0,0,0,0.7)")
|
||||
.style("color", "#fff")
|
||||
.style("border-radius", "4px")
|
||||
.style("font-size", "0.9rem")
|
||||
.style("opacity", 0)
|
||||
.style("z-index", 10000);
|
||||
|
||||
function renderTable(data) {
|
||||
const container = d3.select("#salesTableContainer");
|
||||
container.selectAll("*").remove();
|
||||
|
||||
if (!data.length) {
|
||||
container.append("p").text("No hay datos para ese mes.");
|
||||
return;
|
||||
}
|
||||
|
||||
const table = container.append("table")
|
||||
.attr("class", "table table-striped table-bordered");
|
||||
const thead = table.append("thead");
|
||||
const tbody = table.append("tbody");
|
||||
|
||||
// Cabecera
|
||||
thead.append("tr")
|
||||
.selectAll("th")
|
||||
.data(["Producto", "Unidades", "Venta (€)"])
|
||||
.join("th")
|
||||
.text(d => d);
|
||||
|
||||
// Filas
|
||||
data.forEach(d => {
|
||||
const row = tbody.append("tr");
|
||||
row.append("td").text(d.productName);
|
||||
row.append("td").text(d.units);
|
||||
row.append("td").text(d3.format(",.2f")(d.revenue));
|
||||
});
|
||||
}
|
||||
|
||||
function drawChart(data) {
|
||||
x.domain(data.map(d => d.productName));
|
||||
y.domain([0, d3.max(data, d => d.units) * 1.1]);
|
||||
|
||||
xAxisG.call(d3.axisBottom(x))
|
||||
.selectAll("text")
|
||||
.attr("transform", "rotate(-40)")
|
||||
.style("text-anchor", "end");
|
||||
yAxisG.call(d3.axisLeft(y));
|
||||
|
||||
const bars = svg.selectAll(".bar").data(data, d => d.productName);
|
||||
|
||||
bars.join(
|
||||
enter => enter.append("rect")
|
||||
.attr("class", "bar")
|
||||
.attr("x", d => x(d.productName))
|
||||
.attr("y", height)
|
||||
.attr("width", x.bandwidth())
|
||||
.attr("height", 0)
|
||||
.attr("fill", d => color(d.productName))
|
||||
.on("mouseover", (e, d) => {
|
||||
tooltip
|
||||
.html(`<strong>${d.productName}</strong><br/>Unidades: ${d.units}<br/>€ ${d3.format(",.2f")(d.revenue)}`)
|
||||
.style("opacity", 1);
|
||||
})
|
||||
.on("mousemove", e => {
|
||||
tooltip
|
||||
.style("left", (e.pageX + 10) + "px")
|
||||
.style("top", (e.pageY - 28) + "px");
|
||||
})
|
||||
.on("mouseout", () => tooltip.style("opacity", 0))
|
||||
.call(g => g.transition().duration(600)
|
||||
.attr("y", d => y(d.units))
|
||||
.attr("height", d => height - y(d.units))
|
||||
),
|
||||
|
||||
update => update.call(u => u.transition().duration(600)
|
||||
.attr("x", d => x(d.productName))
|
||||
.attr("y", d => y(d.units))
|
||||
.attr("width", x.bandwidth())
|
||||
.attr("height", d => height - y(d.units))
|
||||
.attr("fill", d => color(d.productName))
|
||||
),
|
||||
|
||||
exit => exit.call(e => e.transition().duration(300)
|
||||
.attr("y", height)
|
||||
.attr("height", 0)
|
||||
.remove()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
monthSelect.addEventListener('change', function () {
|
||||
const month = this.value;
|
||||
if (!month) {
|
||||
d3.selectAll(".bar").remove();
|
||||
d3.select("#salesTableContainer").selectAll("*").remove();
|
||||
return;
|
||||
}
|
||||
fetch(`${apiUrl}?month=${encodeURIComponent(month)}`)
|
||||
.then(res => res.ok ? res.json() : Promise.reject(res.statusText))
|
||||
.then(data => {
|
||||
// Data: [{ productName, units, revenue }, …]
|
||||
renderTable(data);
|
||||
drawChart(data);
|
||||
})
|
||||
.catch(err => console.error("Error al cargar datos:", err));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@ -50,7 +50,9 @@
|
||||
<li><a class="dropdown-item" asp-controller="Products" asp-action="Index">Productos</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="Order" asp-action="Index">Pedidos</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="User" asp-action="Index">Usuarios</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="Sales" asp-action="Index">Ventas</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="Sales" asp-action="HistoricalSales">Historico de ventas</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="Sales" asp-action="Last30DaysSales">Ventas últimos 30 dias</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="Sales" asp-action="MonthSales">Venta por meses</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user