Lista productosHome con filtro y grafico de ventas

This commit is contained in:
devRaGonSa 2025-05-02 01:08:19 +02:00
parent ab74433b70
commit 51ba861ae0
22 changed files with 768 additions and 90 deletions

View File

@ -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,

View File

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

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

View File

@ -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>

View File

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

View File

@ -3,7 +3,7 @@ using WebVentaCoche.Enums;
namespace WebVentaCoche.ViewModels
{
public class UserDetailsViewModel
public class AccountDetailsViewModel
{
public string Id { get; set; } = null!;

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View File

@ -1,4 +1,4 @@
@model WebVentaCoche.ViewModels.UserDetailsViewModel
@model WebVentaCoche.ViewModels.AccountDetailsViewModel
@{
ViewData["Title"] = "Detalles de Cuenta";

View File

@ -1,4 +1,4 @@
@model WebVentaCoche.ViewModels.UserDetailsViewModel
@model WebVentaCoche.ViewModels.AccountDetailsViewModel
@{
Layout = "_AccountLayout";
}

View File

@ -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>

View 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>
}

View File

@ -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>&copy; 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>

View 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");
}
}

View 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");
}
}

View 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>

View File

@ -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>

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB