Lista productosHome con filtro y grafico de ventas
This commit is contained in:
parent
ab74433b70
commit
51ba861ae0
@ -34,7 +34,7 @@ namespace WebVentaCoche.Controllers
|
||||
|
||||
var addresses = await _context.Addresses.Where(a => a.UserId == id).ToListAsync();
|
||||
|
||||
var vm = new UserDetailsViewModel
|
||||
var vm = new AccountDetailsViewModel
|
||||
{
|
||||
Id = user.Id,
|
||||
Name = user.Name,
|
||||
@ -66,7 +66,7 @@ namespace WebVentaCoche.Controllers
|
||||
|
||||
var addresses = await _context.Addresses.Where(a => a.UserId == userId).ToListAsync();
|
||||
|
||||
var vm = new UserDetailsViewModel
|
||||
var vm = new AccountDetailsViewModel
|
||||
{
|
||||
Id = user.Id,
|
||||
Name = user.Name,
|
||||
|
||||
@ -38,7 +38,7 @@ namespace WebVentaCoche.Controllers
|
||||
return View(products);
|
||||
}
|
||||
|
||||
//GET:/Home/ProductsDetailsHome/5
|
||||
//GET:/Home/ProductsDetailsHome/{id}
|
||||
public async Task<IActionResult> ProductsDetailsHome(int id)
|
||||
{
|
||||
if (id <= 0)
|
||||
|
||||
47
WebVentaCoche/Controllers/SalesController.cs
Normal file
47
WebVentaCoche/Controllers/SalesController.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WebVentaCoche.DataBase;
|
||||
using WebVentaCoche.Enums;
|
||||
using WebVentaCoche.ViewModels;
|
||||
|
||||
namespace WebVentaCoche.Controllers
|
||||
{
|
||||
[Authorize(Roles = "Administrador")]
|
||||
public class SalesController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
public SalesController(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
//GET:/Sales/
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
//GET:/Sales/GetMonthlyData
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetMonthlyData()
|
||||
{
|
||||
var query =
|
||||
from d in _context.OrderDetails
|
||||
where d.Order.Status == OrderStatus.Delivered
|
||||
let m = d.Order.OrderDate
|
||||
group d by new { d.Product.Name, Year = m.Year, Month = m.Month } into g
|
||||
orderby g.Key.Name, g.Key.Year, g.Key.Month
|
||||
select new SalesViewModel
|
||||
{
|
||||
ProductName = g.Key.Name!,
|
||||
Month = $"{g.Key.Year}-{g.Key.Month:D2}",
|
||||
Units = g.Sum(x => x.Quantity)
|
||||
};
|
||||
|
||||
var data = await query.ToListAsync();
|
||||
return Json(data);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,8 @@ using WebVentaCoche.Models;
|
||||
using WebVentaCoche.Services;
|
||||
using WebVentaCoche.ViewModels;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
using WebVentaCoche.DataBase;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace WebVentaCoche.Controllers
|
||||
{
|
||||
@ -19,13 +21,15 @@ namespace WebVentaCoche.Controllers
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly EmailService _emailService;
|
||||
private readonly VerificationService _verificationService;
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public UserController(IUserHelper userHelper, IConfiguration configuration, EmailService emailService, VerificationService verificationService)
|
||||
public UserController(IUserHelper userHelper, IConfiguration configuration, EmailService emailService, VerificationService verificationService, ApplicationDbContext context)
|
||||
{
|
||||
_userHelper = userHelper;
|
||||
_configuration = configuration;
|
||||
_emailService = emailService;
|
||||
_verificationService = verificationService;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
private async Task SenConfirmationEmail(User user, string token, string confirmationLink)
|
||||
@ -198,6 +202,94 @@ namespace WebVentaCoche.Controllers
|
||||
return View();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var users = await _context.Users.ToListAsync();
|
||||
return View(users);
|
||||
}
|
||||
|
||||
//GET:/Users/Edit/{id}
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Edit(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
return NotFound();
|
||||
|
||||
var user = await _context.Users.FindAsync(id);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new UserViewModel
|
||||
{
|
||||
Id = user.Id,
|
||||
Name = user.Name,
|
||||
Surname = user.Surname,
|
||||
Email = user.Email,
|
||||
PhoneNumber = user.PhoneNumber,
|
||||
UserType = user.UserType
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
//POST:/Users/Edit
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(UserViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
var user = await _context.Users.FindAsync(vm.Id);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
// Actualiza campos
|
||||
user.Name = vm.Name;
|
||||
user.Surname = vm.Surname;
|
||||
user.Email = vm.Email;
|
||||
user.UserName = vm.Email; // si cambias email conviene actualizar UserName
|
||||
user.PhoneNumber = vm.PhoneNumber;
|
||||
user.UserType = vm.UserType;
|
||||
|
||||
_context.Update(user);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
//GET:/Users/Create
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
//POST:/Users/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(RegisterViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid) return View(model);
|
||||
|
||||
// Valida y crea el usuario
|
||||
var user = new User
|
||||
{
|
||||
Name = model.Name,
|
||||
Surname = model.Surname,
|
||||
Email = model.Email,
|
||||
PhoneNumber = model.PhoneNumber,
|
||||
UserName = model.Email,
|
||||
UserType = Enums.UserType.Usuario
|
||||
};
|
||||
var result = await _userHelper.AddUserAsync(user, model.Password);
|
||||
if (result.Succeeded)
|
||||
return RedirectToAction(nameof(Index));
|
||||
|
||||
// Si hay errores, los mostramos
|
||||
foreach (var e in result.Errors)
|
||||
ModelState.AddModelError("", e.Description);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private TokenAuth BuildToken(User user)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
|
||||
@ -8,7 +8,7 @@ namespace WebVentaCoche.Helpers
|
||||
{
|
||||
public MappingProfile()
|
||||
{
|
||||
CreateMap<User, UserDetailsViewModel>()
|
||||
CreateMap<User, AccountDetailsViewModel>()
|
||||
.ForMember(dest => dest.Addresses,
|
||||
opt => opt.MapFrom(src => src.Addresses));
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ using WebVentaCoche.Enums;
|
||||
|
||||
namespace WebVentaCoche.ViewModels
|
||||
{
|
||||
public class UserDetailsViewModel
|
||||
public class AccountDetailsViewModel
|
||||
{
|
||||
public string Id { get; set; } = null!;
|
||||
|
||||
12
WebVentaCoche/ViewModels/ProductsHomeViewModel.cs
Normal file
12
WebVentaCoche/ViewModels/ProductsHomeViewModel.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using WebVentaCoche.Models;
|
||||
|
||||
namespace WebVentaCoche.ViewModels
|
||||
{
|
||||
public class ProductsHomeViewModel
|
||||
{
|
||||
public IEnumerable<Product> Products { get; set; } = new List<Product>();
|
||||
public int CurrentPage { get; set; }
|
||||
public int TotalPages { get; set; }
|
||||
}
|
||||
}
|
||||
9
WebVentaCoche/ViewModels/SalesViewModel.cs
Normal file
9
WebVentaCoche/ViewModels/SalesViewModel.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace WebVentaCoche.ViewModels
|
||||
{
|
||||
public class SalesViewModel
|
||||
{
|
||||
public string ProductName { get; set; } = null!;
|
||||
public string Month { get; set; } = null!;
|
||||
public int Units { get; set; }
|
||||
}
|
||||
}
|
||||
31
WebVentaCoche/ViewModels/UserViewModel.cs
Normal file
31
WebVentaCoche/ViewModels/UserViewModel.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using WebVentaCoche.Enums;
|
||||
|
||||
namespace WebVentaCoche.ViewModels
|
||||
{
|
||||
public class UserViewModel
|
||||
{
|
||||
[Required]
|
||||
public string Id { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Nombre")]
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Apellidos")]
|
||||
public string Surname { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; } = null!;
|
||||
|
||||
[Phone]
|
||||
[Display(Name = "Teléfono")]
|
||||
public string? PhoneNumber { get; set; }
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Tipo de Usuario")]
|
||||
public UserType UserType { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
@model WebVentaCoche.ViewModels.UserDetailsViewModel
|
||||
@model WebVentaCoche.ViewModels.AccountDetailsViewModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Detalles de Cuenta";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
@model WebVentaCoche.ViewModels.UserDetailsViewModel
|
||||
@model WebVentaCoche.ViewModels.AccountDetailsViewModel
|
||||
@{
|
||||
Layout = "_AccountLayout";
|
||||
}
|
||||
|
||||
@ -1,34 +1,144 @@
|
||||
@model IEnumerable<WebVentaCoche.Models.Product>
|
||||
@{
|
||||
ViewData["Title"] = "Nuestros Productos";
|
||||
}
|
||||
|
||||
<div class="container">
|
||||
<h2 class="text-center my-4">Nuestros Productos</h2>
|
||||
<!-- 1) Estilos específicos para grid y tarjetas -->
|
||||
<style>
|
||||
/* El tbody como grid de 2 columnas */
|
||||
#productsTable tbody {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(45%, 1fr));
|
||||
gap: 1rem; /* espacio entre teselas */
|
||||
}
|
||||
/* Cada <tr> no interfiere con el grid */
|
||||
#productsTable tr {
|
||||
display: contents;
|
||||
}
|
||||
/* Eliminamos bordes/paddings de celdas */
|
||||
#productsTable td {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
/* Estilo “card” translúcida */
|
||||
.card-transparent {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container my-4">
|
||||
<h2 class="text-center mb-4">@ViewData["Title"]</h2>
|
||||
|
||||
<div class="row">
|
||||
@foreach (var product in Model)
|
||||
<!-- Sidebar de filtros -->
|
||||
<aside class="col-md-3 mb-4">
|
||||
<div class="card card-transparent p-3">
|
||||
<h5>Filtros</h5>
|
||||
<div class="mb-3">
|
||||
<label for="filterName" class="form-label">Nombre</label>
|
||||
<input type="text" id="filterName" class="form-control" placeholder="Buscar nombre..." />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Precio</label>
|
||||
<div class="input-group mb-2">
|
||||
<span class="input-group-text">Min</span>
|
||||
<input type="number" id="filterPriceMin" class="form-control" placeholder="0" min="0" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Max</span>
|
||||
<input type="number" id="filterPriceMax" class="form-control" placeholder="999" min="0" />
|
||||
</div>
|
||||
</div>
|
||||
<button id="btnClearFilters" class="btn btn-sm btn-secondary w-100 mt-3">
|
||||
📋 Quitar filtros
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Tabla de productos -->
|
||||
<section class="col-md-9">
|
||||
<table id="productsTable" class="table table-borderless">
|
||||
<thead class="d-none">
|
||||
<tr><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var p in Model)
|
||||
{
|
||||
<div class="col-12 mb-4">
|
||||
<div class="card" style="background-color: rgba(255, 255, 255, 0.8); border-radius: 10px;">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4">
|
||||
@if (!string.IsNullOrEmpty(product.ImagePath))
|
||||
{
|
||||
<img src="@product.ImagePath" class="img-fluid rounded-start" alt="@product.Name" style="max-height: 200px; object-fit: cover;">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="card card-transparent">
|
||||
<div class="card-body d-flex">
|
||||
<div class="me-3" style="width:100px;">
|
||||
<img src="@(string.IsNullOrEmpty(p.ImagePath) ? "/images/default.png" : p.ImagePath)"
|
||||
class="img-fluid rounded" style="max-height:80px;" />
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="card-title">@p.Name</h5>
|
||||
<p class="card-text">@p.ShortDescription</p>
|
||||
<p class="card-text"><strong>@p.Price.ToString("0.00") €</strong></p>
|
||||
<a asp-action="ProductsDetailsHome"
|
||||
asp-controller="Home"
|
||||
asp-route-id="@p.Id"
|
||||
class="btn btn-sm btn-primary">
|
||||
Más detalles
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="/images/default.png" class="img-fluid rounded-start" alt="Imagen no disponible" style="max-height: 200px; object-fit: cover;">
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
$(function () {
|
||||
// 1) Inicializa DataTable sin su buscador integrado (dom:'lrtip')
|
||||
var table = $('#productsTable').DataTable({
|
||||
dom: 'lrtip',
|
||||
paging: true,
|
||||
ordering: true,
|
||||
lengthChange: false,
|
||||
pageLength: 8, // 8 productos / página
|
||||
order: [], // sin orden inicial
|
||||
language: { url: '//cdn.datatables.net/plug-ins/1.13.4/i18n/es-ES.json' },
|
||||
columnDefs: [{ orderable: false, targets: [0] }]
|
||||
});
|
||||
|
||||
// 2) Filtro de precio
|
||||
$.fn.dataTable.ext.search.push(function (settings, data, dataIndex) {
|
||||
if (settings.nTable.id !== 'productsTable') return true;
|
||||
var row = table.row(dataIndex).node();
|
||||
var precioTxt = $('p.card-text strong', row).first().text()
|
||||
.replace(' €', '').replace(',', '.');
|
||||
var precio = parseFloat(precioTxt) || 0;
|
||||
var min = parseFloat($('#filterPriceMin').val()) || 0;
|
||||
var max = parseFloat($('#filterPriceMax').val()) || Infinity;
|
||||
return precio >= min && precio <= max;
|
||||
});
|
||||
|
||||
// 3) Filtro por nombre usando DataTables.search()
|
||||
$('#filterName').on('input', function () {
|
||||
table.search(this.value).draw();
|
||||
});
|
||||
|
||||
// 4) Al cambiar precio, redraw para reaplicar ext.search
|
||||
$('#filterPriceMin, #filterPriceMax').on('input change', function () {
|
||||
table.draw();
|
||||
});
|
||||
|
||||
// 5) Botón Quitar filtros
|
||||
$('#btnClearFilters').on('click', function () {
|
||||
$('#filterName, #filterPriceMin, #filterPriceMax').val('');
|
||||
table.search('').draw();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@product.Name</h5>
|
||||
<p class="card-text">@product.ShortDescription</p>
|
||||
<a href="@Url.Action("ProductsDetailsHome", "Home", new { id = product.Id })" class="text-primary">Más detalles</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
176
WebVentaCoche/Views/Sales/Index.cshtml
Normal file
176
WebVentaCoche/Views/Sales/Index.cshtml
Normal file
@ -0,0 +1,176 @@
|
||||
@{
|
||||
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>
|
||||
}
|
||||
@ -3,86 +3,87 @@
|
||||
@using System.Security.Claims
|
||||
@inject IHttpContextAccessor HttpContextAccessor
|
||||
|
||||
|
||||
@{
|
||||
// Determina si es la página de gestión para ajustar la clase body
|
||||
var isGestionPage = Context.Request.Path.ToString().StartsWith("/Products");
|
||||
|
||||
// Obtener lista de IDs de productos almacenados en sesión
|
||||
var cartItemIds = CartSessionHelper.GetCartItems(HttpContextAccessor.HttpContext.Session);
|
||||
int cartItemCount = cartItemIds.Count;
|
||||
var userId = HttpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
var user = HttpContextAccessor.HttpContext.User;
|
||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] - WebVentaCoche</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="~/css/site.css" />
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.datatables.net/1.13.4/css/dataTables.bootstrap5.min.css" rel="stylesheet" />
|
||||
<link href="~/css/site.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="@(isGestionPage ? "no-background" : "")">
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-center col-10">
|
||||
<a class="navbar-brand" href="/">WebVentaCoche</a>
|
||||
<ul class="navbar-nav text-center">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Productos">Productos</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Contacto">Contacto</a>
|
||||
</li>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain"
|
||||
aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item"><a class="nav-link" href="/Productos">Productos</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/Contacto">Contacto</a></li>
|
||||
|
||||
@if (user.Identity.IsAuthenticated && user.IsInRole("Administrador"))
|
||||
{
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="gestionDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="gestionDropdown" role="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Gestión
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="gestionDropdown">
|
||||
<li><a class="dropdown-item" href="/Products">Productos</a></li>
|
||||
<li><a class="dropdown-item" href="/Order">Pedidos</a></li>
|
||||
<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>
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="collapse navbar-collapse justify-content-center" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
@if (User.Identity.IsAuthenticated)
|
||||
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
||||
@if (user.Identity.IsAuthenticated)
|
||||
{
|
||||
<li class="nav-item me-2 position-relative">
|
||||
<li class="nav-item me-3 position-relative">
|
||||
<a class="nav-link" href="@Url.Action("Index","Cart")">
|
||||
<i class="fa fa-shopping-cart" style="font-size:1.5rem;"></i>
|
||||
|
||||
@* Si cartItemCount es 0, le metemos clase "d-none" *@
|
||||
<span id="cartBadge" class="position-absolute top-0 start-100 translate-middle badge
|
||||
rounded-pill bg-danger @(cartItemCount == 0 ? "d-none" : "")">
|
||||
<span id="cartCountSpan">@cartItemCount</span>
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger @(cartItemCount==0 ? "d-none" : "")">
|
||||
@cartItemCount
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Menú desplegable de usuario -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa fa-user" style="font-size:1.5rem;"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
|
||||
<li><a class="dropdown-item" asp-controller="Account" asp-action="Details" asp-route-id="@userId">Detalles Cuenta</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="Account" asp-action="Settings">Configuración</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="Order" asp-action="UserOrders">Mis Pedidos</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li>
|
||||
<form method="post" asp-action="Logout" asp-controller="User" class="d-inline">
|
||||
<form asp-controller="User" asp-action="Logout" method="post" class="d-inline">
|
||||
<button type="submit" class="dropdown-item text-danger">Cerrar Sesión</button>
|
||||
</form>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
@ -97,11 +98,9 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="jumbotron jumbotron-fluid bg-image"
|
||||
style="background-image: url('/images/captura.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
height: 300px;">
|
||||
<div class="jumbotron jumbotron-fluid bg-image mb-4"
|
||||
style="background-image: url('/images/captura.jpg'); background-size: cover;
|
||||
background-position: center; height: 300px;">
|
||||
<div class="container text-white text-center">
|
||||
<h1 class="display-4">Bienvenido a WebVentaCoche</h1>
|
||||
<p class="lead">Tu tienda de confianza para productos de coches.</p>
|
||||
@ -115,6 +114,12 @@
|
||||
<footer class="bg-dark text-light text-center py-3 mt-4">
|
||||
<p>© 2024 WebVentaCoche. Todos los derechos reservados.</p>
|
||||
</footer>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.4/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
@RenderSection("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
|
||||
46
WebVentaCoche/Views/User/Create.cshtml
Normal file
46
WebVentaCoche/Views/User/Create.cshtml
Normal file
@ -0,0 +1,46 @@
|
||||
@model WebVentaCoche.ViewModels.RegisterViewModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Crear Usuario";
|
||||
}
|
||||
|
||||
<h2>Crear Nuevo Usuario</h2>
|
||||
|
||||
<form asp-action="Create" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
<input asp-for="Email" class="form-control" />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Password" class="form-label"></label>
|
||||
<input asp-for="Password" type="password" class="form-control" />
|
||||
<span asp-validation-for="Password" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Name" class="form-label"></label>
|
||||
<input asp-for="Name" class="form-control" />
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Surname" class="form-label"></label>
|
||||
<input asp-for="Surname" class="form-control" />
|
||||
<span asp-validation-for="Surname" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label asp-for="PhoneNumber" class="form-label"></label>
|
||||
<input asp-for="PhoneNumber" class="form-control" />
|
||||
<span asp-validation-for="PhoneNumber" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Crear</button>
|
||||
<a asp-action="Index" class="btn btn-secondary ms-2">Cancelar</a>
|
||||
</form>
|
||||
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
}
|
||||
57
WebVentaCoche/Views/User/Edit.cshtml
Normal file
57
WebVentaCoche/Views/User/Edit.cshtml
Normal file
@ -0,0 +1,57 @@
|
||||
@using WebVentaCoche.Enums
|
||||
@model WebVentaCoche.ViewModels.UserViewModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Editar Usuario";
|
||||
}
|
||||
|
||||
<div class="container mt-4">
|
||||
<h2 class="mb-4">Editar Usuario</h2>
|
||||
|
||||
<form asp-action="Edit" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" asp-for="Id" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Name" class="form-label"></label>
|
||||
<input asp-for="Name" class="form-control" />
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Surname" class="form-label"></label>
|
||||
<input asp-for="Surname" class="form-control" />
|
||||
<span asp-validation-for="Surname" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
<input asp-for="Email" class="form-control" />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="PhoneNumber" class="form-label"></label>
|
||||
<input asp-for="PhoneNumber" class="form-control" />
|
||||
<span asp-validation-for="PhoneNumber" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="UserType" class="form-label"></label>
|
||||
<select asp-for="UserType"
|
||||
asp-items="Html.GetEnumSelectList<UserType>()"
|
||||
class="form-select">
|
||||
</select>
|
||||
<span asp-validation-for="UserType" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Guardar cambios</button>
|
||||
<a asp-action="Index" class="btn btn-secondary ms-2">Cancelar</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
}
|
||||
40
WebVentaCoche/Views/User/Index.cshtml
Normal file
40
WebVentaCoche/Views/User/Index.cshtml
Normal file
@ -0,0 +1,40 @@
|
||||
@model IEnumerable<WebVentaCoche.Models.User>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Listado de Usuarios";
|
||||
}
|
||||
|
||||
<h2 class="mb-4">Usuarios Registrados</h2>
|
||||
|
||||
<a asp-action="Create" class="btn btn-success mb-3">
|
||||
<i class="fa fa-plus me-1"></i> Crear Usuario
|
||||
</a>
|
||||
|
||||
<table class="table table-striped align-middle">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Nombre</th>
|
||||
<th>Apellidos</th>
|
||||
<th>Tipo</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var u in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>@u.Email</td>
|
||||
<td>@u.Name</td>
|
||||
<td>@u.Surname</td>
|
||||
<td>@u.UserType</td>
|
||||
<td class="text-end">
|
||||
<a asp-action="Edit" asp-route-id="@u.Id"
|
||||
class="btn btn-sm btn-info me-2">
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -18,9 +18,14 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
<PackageReference Include="SendGrid" Version="9.29.0" />
|
||||
<PackageReference Include="X.PagedList.Mvc.Core" Version="10.5.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Views\User\Index.cshtml">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Update="Views\User\VerifyEmail.cshtml">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
html {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
/* Capa para la imagen borrosa */
|
||||
body::before {
|
||||
content: "";
|
||||
@ -37,23 +38,23 @@ body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: blur(3px); /* Ajusta el desenfoque */
|
||||
z-index: -1; /* Envía la capa al fondo */
|
||||
z-index: -1; /* Envía la capa al fondo */
|
||||
opacity: 0.7; /* Transparencia opcional */
|
||||
}
|
||||
|
||||
/* Asegura que el contenido esté por encima del fondo */
|
||||
/* Asegura que el contenido esté por encima del fondo */
|
||||
.container, nav, footer {
|
||||
position: relative;
|
||||
z-index: 1; /* Solo los elementos necesarios por encima del fondo */
|
||||
}
|
||||
|
||||
/* El jumbotron debe tener un índice más bajo para no interferir */
|
||||
/* El jumbotron debe tener un índice más bajo para no interferir */
|
||||
.jumbotron {
|
||||
position: relative;
|
||||
z-index: 0; /* Jumbotron no debe tapar los desplegables */
|
||||
}
|
||||
|
||||
/* Asegura que los desplegables del navbar estén por encima */
|
||||
/* Asegura que los desplegables del navbar estén por encima */
|
||||
.dropdown-menu {
|
||||
z-index: 1000 !important; /* Bootstrap ya lo configura, pero forzamos si es necesario */
|
||||
}
|
||||
@ -70,3 +71,50 @@ body {
|
||||
}
|
||||
|
||||
|
||||
#productsTable tbody {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill,minmax(45%,1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#productsTable tr {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
#productsTable td {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#tooltip {
|
||||
position: absolute;
|
||||
padding: 6px 10px;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
#legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
#legend div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#legend div > div {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
BIN
WebVentaCoche/wwwroot/images/check-engine-warning.webp
Normal file
BIN
WebVentaCoche/wwwroot/images/check-engine-warning.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Loading…
x
Reference in New Issue
Block a user