171 lines
6.9 KiB
Plaintext
171 lines
6.9 KiB
Plaintext
@{
|
|
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>
|
|
}
|