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 addresses = await _context.Addresses.Where(a => a.UserId == id).ToListAsync();
|
||||||
|
|
||||||
var vm = new UserDetailsViewModel
|
var vm = new AccountDetailsViewModel
|
||||||
{
|
{
|
||||||
Id = user.Id,
|
Id = user.Id,
|
||||||
Name = user.Name,
|
Name = user.Name,
|
||||||
@ -66,7 +66,7 @@ namespace WebVentaCoche.Controllers
|
|||||||
|
|
||||||
var addresses = await _context.Addresses.Where(a => a.UserId == userId).ToListAsync();
|
var addresses = await _context.Addresses.Where(a => a.UserId == userId).ToListAsync();
|
||||||
|
|
||||||
var vm = new UserDetailsViewModel
|
var vm = new AccountDetailsViewModel
|
||||||
{
|
{
|
||||||
Id = user.Id,
|
Id = user.Id,
|
||||||
Name = user.Name,
|
Name = user.Name,
|
||||||
|
|||||||
@ -38,7 +38,7 @@ namespace WebVentaCoche.Controllers
|
|||||||
return View(products);
|
return View(products);
|
||||||
}
|
}
|
||||||
|
|
||||||
//GET:/Home/ProductsDetailsHome/5
|
//GET:/Home/ProductsDetailsHome/{id}
|
||||||
public async Task<IActionResult> ProductsDetailsHome(int id)
|
public async Task<IActionResult> ProductsDetailsHome(int id)
|
||||||
{
|
{
|
||||||
if (id <= 0)
|
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.Services;
|
||||||
using WebVentaCoche.ViewModels;
|
using WebVentaCoche.ViewModels;
|
||||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||||
|
using WebVentaCoche.DataBase;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace WebVentaCoche.Controllers
|
namespace WebVentaCoche.Controllers
|
||||||
{
|
{
|
||||||
@ -19,13 +21,15 @@ namespace WebVentaCoche.Controllers
|
|||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly EmailService _emailService;
|
private readonly EmailService _emailService;
|
||||||
private readonly VerificationService _verificationService;
|
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;
|
_userHelper = userHelper;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_emailService = emailService;
|
_emailService = emailService;
|
||||||
_verificationService = verificationService;
|
_verificationService = verificationService;
|
||||||
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SenConfirmationEmail(User user, string token, string confirmationLink)
|
private async Task SenConfirmationEmail(User user, string token, string confirmationLink)
|
||||||
@ -198,6 +202,94 @@ namespace WebVentaCoche.Controllers
|
|||||||
return View();
|
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)
|
private TokenAuth BuildToken(User user)
|
||||||
{
|
{
|
||||||
var claims = new List<Claim>
|
var claims = new List<Claim>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ namespace WebVentaCoche.Helpers
|
|||||||
{
|
{
|
||||||
public MappingProfile()
|
public MappingProfile()
|
||||||
{
|
{
|
||||||
CreateMap<User, UserDetailsViewModel>()
|
CreateMap<User, AccountDetailsViewModel>()
|
||||||
.ForMember(dest => dest.Addresses,
|
.ForMember(dest => dest.Addresses,
|
||||||
opt => opt.MapFrom(src => src.Addresses));
|
opt => opt.MapFrom(src => src.Addresses));
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ using WebVentaCoche.Enums;
|
|||||||
|
|
||||||
namespace WebVentaCoche.ViewModels
|
namespace WebVentaCoche.ViewModels
|
||||||
{
|
{
|
||||||
public class UserDetailsViewModel
|
public class AccountDetailsViewModel
|
||||||
{
|
{
|
||||||
public string Id { get; set; } = null!;
|
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";
|
ViewData["Title"] = "Detalles de Cuenta";
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
@model WebVentaCoche.ViewModels.UserDetailsViewModel
|
@model WebVentaCoche.ViewModels.AccountDetailsViewModel
|
||||||
@{
|
@{
|
||||||
Layout = "_AccountLayout";
|
Layout = "_AccountLayout";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,34 +1,144 @@
|
|||||||
@model IEnumerable<WebVentaCoche.Models.Product>
|
@model IEnumerable<WebVentaCoche.Models.Product>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Nuestros Productos";
|
||||||
|
}
|
||||||
|
|
||||||
<div class="container">
|
<!-- 1) Estilos específicos para grid y tarjetas -->
|
||||||
<h2 class="text-center my-4">Nuestros Productos</h2>
|
<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">
|
<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">
|
<tr>
|
||||||
<div class="card" style="background-color: rgba(255, 255, 255, 0.8); border-radius: 10px;">
|
<td>
|
||||||
<div class="row g-0">
|
<div class="card card-transparent">
|
||||||
<div class="col-md-4">
|
<div class="card-body d-flex">
|
||||||
@if (!string.IsNullOrEmpty(product.ImagePath))
|
<div class="me-3" style="width:100px;">
|
||||||
{
|
<img src="@(string.IsNullOrEmpty(p.ImagePath) ? "/images/default.png" : p.ImagePath)"
|
||||||
<img src="@product.ImagePath" class="img-fluid rounded-start" alt="@product.Name" style="max-height: 200px; object-fit: cover;">
|
class="img-fluid rounded" style="max-height:80px;" />
|
||||||
}
|
</div>
|
||||||
else
|
<div>
|
||||||
{
|
<h5 class="card-title">@p.Name</h5>
|
||||||
<img src="/images/default.png" class="img-fluid rounded-start" alt="Imagen no disponible" style="max-height: 200px; object-fit: cover;">
|
<p class="card-text">@p.ShortDescription</p>
|
||||||
}
|
<p class="card-text"><strong>@p.Price.ToString("0.00") €</strong></p>
|
||||||
</div>
|
<a asp-action="ProductsDetailsHome"
|
||||||
<div class="col-md-8">
|
asp-controller="Home"
|
||||||
<div class="card-body">
|
asp-route-id="@p.Id"
|
||||||
<h5 class="card-title">@product.Name</h5>
|
class="btn btn-sm btn-primary">
|
||||||
<p class="card-text">@product.ShortDescription</p>
|
Más detalles
|
||||||
<a href="@Url.Action("ProductsDetailsHome", "Home", new { id = product.Id })" class="text-primary">Más detalles</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
}
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
}
|
||||||
|
|||||||
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,93 +3,94 @@
|
|||||||
@using System.Security.Claims
|
@using System.Security.Claims
|
||||||
@inject IHttpContextAccessor HttpContextAccessor
|
@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");
|
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);
|
var cartItemIds = CartSessionHelper.GetCartItems(HttpContextAccessor.HttpContext.Session);
|
||||||
int cartItemCount = cartItemIds.Count;
|
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>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>@ViewData["Title"] - WebVentaCoche</title>
|
<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://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 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" />
|
<link href="https://cdn.datatables.net/1.13.4/css/dataTables.bootstrap5.min.css" rel="stylesheet" />
|
||||||
|
<link href="~/css/site.css" rel="stylesheet" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="@(isGestionPage ? "no-background" : "")">
|
<body class="@(isGestionPage ? "no-background" : "")">
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="d-flex justify-content-center col-10">
|
|
||||||
<a class="navbar-brand" href="/">WebVentaCoche</a>
|
<a class="navbar-brand" href="/">WebVentaCoche</a>
|
||||||
<ul class="navbar-nav text-center">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain"
|
||||||
<li class="nav-item">
|
aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<a class="nav-link" href="/Productos">Productos</a>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</li>
|
</button>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/Contacto">Contacto</a>
|
<div class="collapse navbar-collapse" id="navbarMain">
|
||||||
</li>
|
<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">
|
<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
|
Gestión
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="gestionDropdown">
|
<ul class="dropdown-menu" aria-labelledby="gestionDropdown">
|
||||||
<li><a class="dropdown-item" href="/Products">Productos</a></li>
|
<li><a class="dropdown-item" asp-controller="Products" asp-action="Index">Productos</a></li>
|
||||||
<li><a class="dropdown-item" href="/Order">Pedidos</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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="collapse navbar-collapse justify-content-center" id="navbarNav">
|
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
||||||
<ul class="navbar-nav ms-auto">
|
@if (user.Identity.IsAuthenticated)
|
||||||
@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")">
|
<a class="nav-link" href="@Url.Action("Index","Cart")">
|
||||||
<i class="fa fa-shopping-cart" style="font-size: 1.5rem;"></i>
|
<i class="fa fa-shopping-cart" style="font-size:1.5rem;"></i>
|
||||||
|
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger @(cartItemCount==0 ? "d-none" : "")">
|
||||||
@* Si cartItemCount es 0, le metemos clase "d-none" *@
|
@cartItemCount
|
||||||
<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>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Menú desplegable de usuario -->
|
|
||||||
<li class="nav-item dropdown">
|
<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"
|
||||||
<i class="fa fa-user" style="font-size: 1.5rem;"></i>
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa fa-user" style="font-size:1.5rem;"></i>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
|
<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="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="Account" asp-action="Settings">Configuración</a></li>
|
||||||
<li><a class="dropdown-item" asp-controller="Order" asp-action="UserOrders">Mis Pedidos</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>
|
<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>
|
<button type="submit" class="dropdown-item text-danger">Cerrar Sesión</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="@Url.Action("Login", "User")">Iniciar Sesión</a>
|
<a class="nav-link" href="@Url.Action("Login","User")">Iniciar Sesión</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
@ -97,11 +98,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="jumbotron jumbotron-fluid bg-image"
|
<div class="jumbotron jumbotron-fluid bg-image mb-4"
|
||||||
style="background-image: url('/images/captura.jpg');
|
style="background-image: url('/images/captura.jpg'); background-size: cover;
|
||||||
background-size: cover;
|
background-position: center; height: 300px;">
|
||||||
background-position: center;
|
|
||||||
height: 300px;">
|
|
||||||
<div class="container text-white text-center">
|
<div class="container text-white text-center">
|
||||||
<h1 class="display-4">Bienvenido a WebVentaCoche</h1>
|
<h1 class="display-4">Bienvenido a WebVentaCoche</h1>
|
||||||
<p class="lead">Tu tienda de confianza para productos de coches.</p>
|
<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">
|
<footer class="bg-dark text-light text-center py-3 mt-4">
|
||||||
<p>© 2024 WebVentaCoche. Todos los derechos reservados.</p>
|
<p>© 2024 WebVentaCoche. Todos los derechos reservados.</p>
|
||||||
</footer>
|
</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)
|
@RenderSection("Scripts", required: false)
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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>
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||||
<PackageReference Include="SendGrid" Version="9.29.0" />
|
<PackageReference Include="SendGrid" Version="9.29.0" />
|
||||||
|
<PackageReference Include="X.PagedList.Mvc.Core" Version="10.5.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Content Update="Views\User\Index.cshtml">
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
</Content>
|
||||||
<Content Update="Views\User\VerifyEmail.cshtml">
|
<Content Update="Views\User\VerifyEmail.cshtml">
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
html {
|
html {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Capa para la imagen borrosa */
|
/* Capa para la imagen borrosa */
|
||||||
body::before {
|
body::before {
|
||||||
content: "";
|
content: "";
|
||||||
@ -37,23 +38,23 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
filter: blur(3px); /* Ajusta el desenfoque */
|
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 */
|
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 {
|
.container, nav, footer {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1; /* Solo los elementos necesarios por encima del fondo */
|
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 {
|
.jumbotron {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0; /* Jumbotron no debe tapar los desplegables */
|
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 {
|
.dropdown-menu {
|
||||||
z-index: 1000 !important; /* Bootstrap ya lo configura, pero forzamos si es necesario */
|
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