diff --git a/CSharpPizza.Api/Controllers/AdminController.cs b/CSharpPizza.Api/Controllers/AdminController.cs new file mode 100644 index 0000000..7b55ebe --- /dev/null +++ b/CSharpPizza.Api/Controllers/AdminController.cs @@ -0,0 +1,345 @@ +using AutoMapper; +using CSharpPizza.Data.Entities; +using CSharpPizza.Data.Repositories; +using CSharpPizza.Domain.Services; +using CSharpPizza.DTO.Admin; +using CSharpPizza.DTO.Orders; +using CSharpPizza.DTO.Pizzas; +using CSharpPizza.DTO.Toppings; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace CSharpPizza.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize(Roles = "Admin")] +public class AdminController : ControllerBase +{ + private readonly IOrderService _orderService; + private readonly IPizzaService _pizzaService; + private readonly IToppingService _toppingService; + private readonly IRepository _orderRepository; + private readonly IPizzaRepository _pizzaRepository; + private readonly IRepository _toppingRepository; + private readonly ILoggingService _loggingService; + private readonly IMapper _mapper; + private readonly ILogger _logger; + + public AdminController( + IOrderService orderService, + IPizzaService pizzaService, + IToppingService toppingService, + IRepository orderRepository, + IPizzaRepository pizzaRepository, + IRepository toppingRepository, + ILoggingService loggingService, + IMapper mapper, + ILogger logger) + { + _orderService = orderService; + _pizzaService = pizzaService; + _toppingService = toppingService; + _orderRepository = orderRepository; + _pizzaRepository = pizzaRepository; + _toppingRepository = toppingRepository; + _loggingService = loggingService; + _mapper = mapper; + _logger = logger; + } + + /// + /// Get all orders with optional filtering + /// + [HttpGet("orders")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>> GetAllOrders( + [FromQuery] string? status = null, + [FromQuery] DateTime? startDate = null, + [FromQuery] DateTime? endDate = null, + [FromQuery] string? customerName = null, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("Admin getting all orders with filters - Status: {Status}, StartDate: {StartDate}, EndDate: {EndDate}, CustomerName: {CustomerName}", + status, startDate, endDate, customerName); + + _ = _loggingService.LogInfoAsync($"Admin retrieved orders list with filters", + $"Status: {status}, StartDate: {startDate}, EndDate: {endDate}, CustomerName: {customerName}"); + //fetch from db + var orders = await _orderRepository.GetAllAsync(cancellationToken); + + // Apply filters + var filteredOrders = orders.AsEnumerable(); + + if (!string.IsNullOrWhiteSpace(status)) + { + if (Enum.TryParse(status, true, out var orderStatus)) + { + filteredOrders = filteredOrders.Where(o => o.Status == orderStatus); + } + } + + if (startDate.HasValue) + { + filteredOrders = filteredOrders.Where(o => o.CreatedAt >= startDate.Value); + } + + if (endDate.HasValue) + { + filteredOrders = filteredOrders.Where(o => o.CreatedAt <= endDate.Value); + } + + if (!string.IsNullOrWhiteSpace(customerName)) + { + filteredOrders = filteredOrders.Where(o => + o.User.Name.Contains(customerName, StringComparison.OrdinalIgnoreCase)); + } + + var orderList = filteredOrders + .OrderByDescending(o => o.CreatedAt) + .Select(o => _mapper.Map(o)) + .ToList(); + + return Ok(orderList); + } + + /// + /// Get order details by ID + /// + [HttpGet("orders/{id}")] + [ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> GetOrderById(Guid id, CancellationToken cancellationToken) + { + _logger.LogInformation("Admin getting order details for ID: {OrderId}", id); + + _ = _loggingService.LogInfoAsync($"Admin retrieved order details", $"OrderId: {id}"); + + var order = await _orderService.GetOrderByIdAsync(id, cancellationToken); + + if (order == null) + { + return NotFound(new { error = $"Order with ID {id} not found" }); + } + + return Ok(order); + } + + /// + /// Update order status + /// + [HttpPut("orders/{id}/status")] + [ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> UpdateOrderStatus( + Guid id, + [FromBody] UpdateOrderStatusDto updateDto, + CancellationToken cancellationToken) + { + + _logger.LogInformation("Admin updating order {OrderId} status to: {Status}", id, updateDto.Status); + + _ = _loggingService.LogInfoAsync($"Admin updated order status", + $"OrderId: {id}, NewStatus: {updateDto.Status}"); + + var order = await _orderService.UpdateOrderStatusAsync(id, updateDto.Status, cancellationToken); + return Ok(order); + } + + /// + /// Get all pizzas including soft-deleted ones + /// + [HttpGet("pizzas")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>> GetAllPizzas(CancellationToken cancellationToken) + { + + _logger.LogInformation("Admin getting all pizzas including soft-deleted"); + + _ = _loggingService.LogInfoAsync("Admin retrieved all pizzas including soft-deleted"); + + var pizzas = await _pizzaRepository.GetAllAsync(cancellationToken); + var pizzaList = pizzas.Select(p => + { + var dto = _mapper.Map(p); + dto.ComputedCost = _pizzaService.ComputePizzaCost(p); + return dto; + }).ToList(); + + return Ok(pizzaList); + } + + /// + /// Create a new pizza + /// + [HttpPost("pizzas")] + [ProducesResponseType(typeof(PizzaDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> CreatePizza( + [FromBody] CreatePizzaDto createDto, + CancellationToken cancellationToken) + { + _logger.LogInformation("Admin creating new pizza: {Name}", createDto.Name); + + _ = _loggingService.LogInfoAsync($"Admin created new pizza", $"PizzaName: {createDto.Name}"); + + var pizza = await _pizzaService.CreateAsync(createDto, cancellationToken); + return CreatedAtAction(nameof(GetOrderById), new { id = pizza.Id }, pizza); + } + + /// + /// Update a pizza + /// + [HttpPut("pizzas/{id}")] + [ProducesResponseType(typeof(PizzaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> UpdatePizza( + Guid id, + [FromBody] UpdatePizzaDto updateDto, + CancellationToken cancellationToken) + { + + + _logger.LogInformation("Admin updating pizza with ID: {Id}", id); + + _ = _loggingService.LogInfoAsync($"Admin updated pizza", $"PizzaId: {id}, PizzaName: {updateDto.Name}"); + + var pizza = await _pizzaService.UpdateAsync(id, updateDto, cancellationToken); + + if (pizza == null) + { + return NotFound(new { error = $"Pizza with ID {id} not found" }); + } + + return Ok(pizza); + } + + /// + /// Soft delete a pizza + /// + [HttpDelete("pizzas/{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task DeletePizza(Guid id, CancellationToken cancellationToken) + { + _logger.LogInformation("Admin soft deleting pizza with ID: {Id}", id); + + _ = _loggingService.LogInfoAsync($"Admin soft deleted pizza", $"PizzaId: {id}"); + + var result = await _pizzaService.DeleteAsync(id, cancellationToken); + + if (!result) + { + return NotFound(new { error = $"Pizza with ID {id} not found" }); + } + + return NoContent(); + } + + /// + /// Get all toppings including soft-deleted ones + /// + [HttpGet("toppings")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>> GetAllToppings(CancellationToken cancellationToken) + { + + _logger.LogInformation("Admin getting all toppings including soft-deleted"); + + _ = _loggingService.LogInfoAsync("Admin retrieved all toppings including soft-deleted"); + + var toppings = await _toppingRepository.GetAllAsync(cancellationToken); + var toppingList = toppings.Select(t => _mapper.Map(t)).ToList(); + + return Ok(toppingList); + } + + /// + /// Create a new topping + /// + [HttpPost("toppings")] + [ProducesResponseType(typeof(ToppingDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> CreateTopping( + [FromBody] CreateToppingDto createDto, + CancellationToken cancellationToken) + { + + _logger.LogInformation("Admin creating new topping: {Name}", createDto.Name); + + _ = _loggingService.LogInfoAsync($"Admin created new topping", $"ToppingName: {createDto.Name}"); + + var topping = await _toppingService.CreateAsync(createDto, cancellationToken); + return CreatedAtAction(nameof(GetAllToppings), new { id = topping.Id }, topping); + } + + /// + /// Update a topping + /// + [HttpPut("toppings/{id}")] + [ProducesResponseType(typeof(ToppingDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> UpdateTopping( + Guid id, + [FromBody] UpdateToppingDto updateDto, + CancellationToken cancellationToken) + { + + _logger.LogInformation("Admin updating topping with ID: {Id}", id); + + _ = _loggingService.LogInfoAsync($"Admin updated topping", $"ToppingId: {id}, ToppingName: {updateDto.Name}"); + + var topping = await _toppingService.UpdateAsync(id, updateDto, cancellationToken); + + if (topping == null) + { + return NotFound(new { error = $"Topping with ID {id} not found" }); + } + + return Ok(topping); + } + + /// + /// Soft delete a topping + /// + [HttpDelete("toppings/{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task DeleteTopping(Guid id, CancellationToken cancellationToken) + { + _logger.LogInformation("Admin soft deleting topping with ID: {Id}", id); + + _ = _loggingService.LogInfoAsync($"Admin soft deleted topping", $"ToppingId: {id}"); + + var result = await _toppingService.DeleteAsync(id, cancellationToken); + + if (!result) + { + return NotFound(new { error = $"Topping with ID {id} not found" }); + } + + return NoContent(); + } +} diff --git a/CSharpPizza.Api/Middleware/RequestLoggingMiddleware.cs b/CSharpPizza.Api/Middleware/RequestLoggingMiddleware.cs new file mode 100644 index 0000000..a187e98 --- /dev/null +++ b/CSharpPizza.Api/Middleware/RequestLoggingMiddleware.cs @@ -0,0 +1,64 @@ +using System.Security.Claims; +using CSharpPizza.Domain.Services; + +namespace CSharpPizza.Api.Middleware; + +public class RequestLoggingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public RequestLoggingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context, ILoggingService loggingService) + { + + var method = context.Request.Method; + var path = context.Request.Path; + var timestamp = DateTime.UtcNow; + + // Get user information if authenticated + var userIdClaim = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + int? userId = null; + if (!string.IsNullOrEmpty(userIdClaim) && int.TryParse(userIdClaim, out var parsedUserId)) + { + userId = parsedUserId; + } + + _logger.LogInformation("Incoming request: {Method} {Path} at {Timestamp} by User: {UserId}", + method, path, timestamp, userId?.ToString() ?? "Anonymous"); + + // Fire-and-forget logging (don't await) + _ = loggingService.LogAsync( + level: "Info", + message: $"API Request: {method} {path}", + details: $"Timestamp: {timestamp}, User: {userId?.ToString() ?? "Anonymous"}", + userId: userId, + endpoint: path, + httpMethod: method, + statusCode: null); + + // Call the next middleware in the pipeline + await _next(context); + + // Log response status code + var statusCode = context.Response.StatusCode; + + _logger.LogInformation("Response: {Method} {Path} returned {StatusCode}", + method, path, statusCode); + + // Fire-and-forget logging (don't await) + _ = loggingService.LogAsync( + level: statusCode >= 400 ? "Error" : "Info", + message: $"API Response: {method} {path}", + details: $"StatusCode: {statusCode}, User: {userId?.ToString() ?? "Anonymous"}", + userId: userId, + endpoint: path, + httpMethod: method, + statusCode: statusCode); + } +} diff --git a/CSharpPizza.Api/Program.cs b/CSharpPizza.Api/Program.cs index 7ac25dd..a133dd2 100644 --- a/CSharpPizza.Api/Program.cs +++ b/CSharpPizza.Api/Program.cs @@ -29,6 +29,7 @@ builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Register services builder.Services.AddScoped(); @@ -39,6 +40,11 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Register background logging services +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); +builder.Services.AddScoped(); + // Configure AutoMapper builder.Services.AddAutoMapper(typeof(CSharpPizza.DTO.Mappings.UserProfile).Assembly); @@ -167,6 +173,9 @@ app.UseAuthentication(); app.UseAuthorization(); +// Add request logging middleware +app.UseMiddleware(); + app.MapControllers(); app.Run(); diff --git a/CSharpPizza.Api/bin/Debug/net9.0/CSharpPizza.Api.deps.json b/CSharpPizza.Api/bin/Debug/net9.0/CSharpPizza.Api.deps.json index 1084a4e..2101d4f 100644 --- a/CSharpPizza.Api/bin/Debug/net9.0/CSharpPizza.Api.deps.json +++ b/CSharpPizza.Api/bin/Debug/net9.0/CSharpPizza.Api.deps.json @@ -521,6 +521,26 @@ } } }, + "Microsoft.Extensions.Diagnostics.Abstractions/9.0.0": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions/9.0.0": { + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.0" + } + }, + "Microsoft.Extensions.Hosting.Abstractions/9.0.0": { + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0" + } + }, "Microsoft.Extensions.Logging/9.0.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.0", @@ -913,6 +933,7 @@ "BCrypt.Net-Next": "4.0.3", "CSharpPizza.DTO": "1.0.0", "CSharpPizza.Data": "1.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "9.0.0", "Microsoft.IdentityModel.Tokens": "8.14.0", "System.IdentityModel.Tokens.Jwt": "8.14.0" }, @@ -1168,6 +1189,27 @@ "path": "microsoft.extensions.dependencymodel/9.0.0", "hashPath": "microsoft.extensions.dependencymodel.9.0.0.nupkg.sha512" }, + "Microsoft.Extensions.Diagnostics.Abstractions/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-1K8P7XzuzX8W8pmXcZjcrqS6x5eSSdvhQohmcpgiQNY/HlDAlnrhR9dvlURfFz428A+RTCJpUyB+aKTA6AgVcQ==", + "path": "microsoft.extensions.diagnostics.abstractions/9.0.0", + "hashPath": "microsoft.extensions.diagnostics.abstractions.9.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.FileProviders.Abstractions/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-uK439QzYR0q2emLVtYzwyK3x+T5bTY4yWsd/k/ZUS9LR6Sflp8MIdhGXW8kQCd86dQD4tLqvcbLkku8qHY263Q==", + "path": "microsoft.extensions.fileproviders.abstractions/9.0.0", + "hashPath": "microsoft.extensions.fileproviders.abstractions.9.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Hosting.Abstractions/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-yUKJgu81ExjvqbNWqZKshBbLntZMbMVz/P7Way2SBx7bMqA08Mfdc9O7hWDKAiSp+zPUGT6LKcSCQIPeDK+CCw==", + "path": "microsoft.extensions.hosting.abstractions/9.0.0", + "hashPath": "microsoft.extensions.hosting.abstractions.9.0.0.nupkg.sha512" + }, "Microsoft.Extensions.Logging/9.0.0": { "type": "package", "serviceable": true, diff --git a/CSharpPizza.Api/csharp-pizza.db b/CSharpPizza.Api/csharp-pizza.db index 740dd0a..77878e7 100644 Binary files a/CSharpPizza.Api/csharp-pizza.db and b/CSharpPizza.Api/csharp-pizza.db differ diff --git a/CSharpPizza.Api/obj/CSharpPizza.Api.csproj.nuget.dgspec.json b/CSharpPizza.Api/obj/CSharpPizza.Api.csproj.nuget.dgspec.json index 6cc963f..2633878 100644 --- a/CSharpPizza.Api/obj/CSharpPizza.Api.csproj.nuget.dgspec.json +++ b/CSharpPizza.Api/obj/CSharpPizza.Api.csproj.nuget.dgspec.json @@ -261,6 +261,10 @@ "target": "Package", "version": "[4.0.3, )" }, + "Microsoft.Extensions.Hosting.Abstractions": { + "target": "Package", + "version": "[9.0.0, )" + }, "Microsoft.IdentityModel.Tokens": { "target": "Package", "version": "[8.14.0, )" diff --git a/CSharpPizza.Api/obj/Debug/net9.0/CSharpPizza.Api.AssemblyInfo.cs b/CSharpPizza.Api/obj/Debug/net9.0/CSharpPizza.Api.AssemblyInfo.cs index 2443d27..a6416b9 100644 --- a/CSharpPizza.Api/obj/Debug/net9.0/CSharpPizza.Api.AssemblyInfo.cs +++ b/CSharpPizza.Api/obj/Debug/net9.0/CSharpPizza.Api.AssemblyInfo.cs @@ -13,7 +13,7 @@ [assembly: System.Reflection.AssemblyCompanyAttribute("CSharpPizza.Api")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+0f2ea68a7cfc1ee055cda29bd334562793872a4a")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+37f3aaa8ab62a512cffac90d34ff442f110ff7c0")] [assembly: System.Reflection.AssemblyProductAttribute("CSharpPizza.Api")] [assembly: System.Reflection.AssemblyTitleAttribute("CSharpPizza.Api")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/CSharpPizza.Api/obj/Debug/net9.0/CSharpPizza.Api.csproj.FileListAbsolute.txt b/CSharpPizza.Api/obj/Debug/net9.0/CSharpPizza.Api.csproj.FileListAbsolute.txt index 25cc2a1..beb88d2 100644 --- a/CSharpPizza.Api/obj/Debug/net9.0/CSharpPizza.Api.csproj.FileListAbsolute.txt +++ b/CSharpPizza.Api/obj/Debug/net9.0/CSharpPizza.Api.csproj.FileListAbsolute.txt @@ -148,6 +148,7 @@ C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\CSharpPizza. C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\CSharpPizza.Api.csproj.CoreCompileInputs.cache C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\CSharpPizza.Api.MvcApplicationPartsAssemblyInfo.cs C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\CSharpPizza.Api.MvcApplicationPartsAssemblyInfo.cache +C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\CSharpPizza.Api.sourcelink.json C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\rjimswa.dswa.cache.json C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\rjsmrazor.dswa.cache.json C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\rjsmcshtml.dswa.cache.json @@ -162,4 +163,3 @@ C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\refint\CShar C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\CSharpPizza.Api.pdb C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\CSharpPizza.Api.genruntimeconfig.cache C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\ref\CSharpPizza.Api.dll -C:\Users\princ\repo\csharp-project\CSharpPizza.Api\obj\Debug\net9.0\CSharpPizza.Api.sourcelink.json diff --git a/CSharpPizza.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json b/CSharpPizza.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json index 32769f1..a66fa6d 100644 --- a/CSharpPizza.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json +++ b/CSharpPizza.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"PHw3jjWBKF81DYsc5h3FDTLACQkzWq+Pt27M+7R9tsM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["IznhvaV3J7lgPbEA24dFaPloqFkuxyp5sS49\u002B1nKOy0=","CGU2MilnF\u002BUW\u002BORoSUPiIu6dPYbncMmzTBMiEiEYfDM=","x0yqGwiR2g4/a8lR/v1hTKeyfEDLEk120yr0waLgryM=","LyawzLxWkJQyqWyW8YB4ADC3ap7TvoM91NuTKUy\u002BlfU=","JFLNhlMToFtpox74Wvkp3UX2jJwsOmUloxTUP/h2oYo=","b92C2jNZPTq4Pg9wrL44ZrQzim6dKV1dJkWtYafeLB0=","haN5onKbwHdoFLraurYDOSkVx61fv/n0rX/jUdY6dTk=","AmCgmiKclltFCynpVNrCEV4kBPF1wFvibNRfg5OENUU="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"PHw3jjWBKF81DYsc5h3FDTLACQkzWq+Pt27M+7R9tsM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["IznhvaV3J7lgPbEA24dFaPloqFkuxyp5sS49\u002B1nKOy0=","CGU2MilnF\u002BUW\u002BORoSUPiIu6dPYbncMmzTBMiEiEYfDM=","8kIKV6OzCTlyZCXUrZlNphfhVMC4MkKBXNeMDM/4eVI=","b92C2jNZPTq4Pg9wrL44ZrQzim6dKV1dJkWtYafeLB0=","haN5onKbwHdoFLraurYDOSkVx61fv/n0rX/jUdY6dTk=","4PuhPJq7lrSwQ7ACrvE7lXTpDRCEbmep3xGrCk2J264="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/CSharpPizza.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json b/CSharpPizza.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json index 23e408a..f3ba189 100644 --- a/CSharpPizza.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json +++ b/CSharpPizza.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"ZkcXimgRQaglrQlljonUGg6P/DX4dXgrZ3s4jzN+bP0=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["IznhvaV3J7lgPbEA24dFaPloqFkuxyp5sS49\u002B1nKOy0=","CGU2MilnF\u002BUW\u002BORoSUPiIu6dPYbncMmzTBMiEiEYfDM=","x0yqGwiR2g4/a8lR/v1hTKeyfEDLEk120yr0waLgryM=","LyawzLxWkJQyqWyW8YB4ADC3ap7TvoM91NuTKUy\u002BlfU=","JFLNhlMToFtpox74Wvkp3UX2jJwsOmUloxTUP/h2oYo=","b92C2jNZPTq4Pg9wrL44ZrQzim6dKV1dJkWtYafeLB0=","haN5onKbwHdoFLraurYDOSkVx61fv/n0rX/jUdY6dTk=","AmCgmiKclltFCynpVNrCEV4kBPF1wFvibNRfg5OENUU="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"ZkcXimgRQaglrQlljonUGg6P/DX4dXgrZ3s4jzN+bP0=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["IznhvaV3J7lgPbEA24dFaPloqFkuxyp5sS49\u002B1nKOy0=","CGU2MilnF\u002BUW\u002BORoSUPiIu6dPYbncMmzTBMiEiEYfDM=","8kIKV6OzCTlyZCXUrZlNphfhVMC4MkKBXNeMDM/4eVI=","b92C2jNZPTq4Pg9wrL44ZrQzim6dKV1dJkWtYafeLB0=","haN5onKbwHdoFLraurYDOSkVx61fv/n0rX/jUdY6dTk=","4PuhPJq7lrSwQ7ACrvE7lXTpDRCEbmep3xGrCk2J264="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/CSharpPizza.Api/obj/project.assets.json b/CSharpPizza.Api/obj/project.assets.json index e85016d..e3cc7d5 100644 --- a/CSharpPizza.Api/obj/project.assets.json +++ b/CSharpPizza.Api/obj/project.assets.json @@ -726,6 +726,68 @@ "buildTransitive/net8.0/_._": {} } }, + "Microsoft.Extensions.Diagnostics.Abstractions/9.0.0": { + "type": "package", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + }, + "compile": { + "lib/net9.0/Microsoft.Extensions.Diagnostics.Abstractions.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Diagnostics.Abstractions.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net8.0/_._": {} + } + }, + "Microsoft.Extensions.FileProviders.Abstractions/9.0.0": { + "type": "package", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.0" + }, + "compile": { + "lib/net9.0/Microsoft.Extensions.FileProviders.Abstractions.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.FileProviders.Abstractions.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net8.0/_._": {} + } + }, + "Microsoft.Extensions.Hosting.Abstractions/9.0.0": { + "type": "package", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0" + }, + "compile": { + "lib/net9.0/Microsoft.Extensions.Hosting.Abstractions.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Hosting.Abstractions.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net8.0/_._": {} + } + }, "Microsoft.Extensions.Logging/9.0.0": { "type": "package", "dependencies": { @@ -1395,6 +1457,7 @@ "BCrypt.Net-Next": "4.0.3", "CSharpPizza.DTO": "1.0.0", "CSharpPizza.Data": "1.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "9.0.0", "Microsoft.IdentityModel.Tokens": "8.14.0", "System.IdentityModel.Tokens.Jwt": "8.14.0" }, @@ -3136,6 +3199,91 @@ "useSharedDesignerContext.txt" ] }, + "Microsoft.Extensions.Diagnostics.Abstractions/9.0.0": { + "sha512": "1K8P7XzuzX8W8pmXcZjcrqS6x5eSSdvhQohmcpgiQNY/HlDAlnrhR9dvlURfFz428A+RTCJpUyB+aKTA6AgVcQ==", + "type": "package", + "path": "microsoft.extensions.diagnostics.abstractions/9.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/Microsoft.Extensions.Diagnostics.Abstractions.targets", + "buildTransitive/net462/_._", + "buildTransitive/net8.0/_._", + "buildTransitive/netcoreapp2.0/Microsoft.Extensions.Diagnostics.Abstractions.targets", + "lib/net462/Microsoft.Extensions.Diagnostics.Abstractions.dll", + "lib/net462/Microsoft.Extensions.Diagnostics.Abstractions.xml", + "lib/net8.0/Microsoft.Extensions.Diagnostics.Abstractions.dll", + "lib/net8.0/Microsoft.Extensions.Diagnostics.Abstractions.xml", + "lib/net9.0/Microsoft.Extensions.Diagnostics.Abstractions.dll", + "lib/net9.0/Microsoft.Extensions.Diagnostics.Abstractions.xml", + "lib/netstandard2.0/Microsoft.Extensions.Diagnostics.Abstractions.dll", + "lib/netstandard2.0/Microsoft.Extensions.Diagnostics.Abstractions.xml", + "microsoft.extensions.diagnostics.abstractions.9.0.0.nupkg.sha512", + "microsoft.extensions.diagnostics.abstractions.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "Microsoft.Extensions.FileProviders.Abstractions/9.0.0": { + "sha512": "uK439QzYR0q2emLVtYzwyK3x+T5bTY4yWsd/k/ZUS9LR6Sflp8MIdhGXW8kQCd86dQD4tLqvcbLkku8qHY263Q==", + "type": "package", + "path": "microsoft.extensions.fileproviders.abstractions/9.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "PACKAGE.md", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/Microsoft.Extensions.FileProviders.Abstractions.targets", + "buildTransitive/net462/_._", + "buildTransitive/net8.0/_._", + "buildTransitive/netcoreapp2.0/Microsoft.Extensions.FileProviders.Abstractions.targets", + "lib/net462/Microsoft.Extensions.FileProviders.Abstractions.dll", + "lib/net462/Microsoft.Extensions.FileProviders.Abstractions.xml", + "lib/net8.0/Microsoft.Extensions.FileProviders.Abstractions.dll", + "lib/net8.0/Microsoft.Extensions.FileProviders.Abstractions.xml", + "lib/net9.0/Microsoft.Extensions.FileProviders.Abstractions.dll", + "lib/net9.0/Microsoft.Extensions.FileProviders.Abstractions.xml", + "lib/netstandard2.0/Microsoft.Extensions.FileProviders.Abstractions.dll", + "lib/netstandard2.0/Microsoft.Extensions.FileProviders.Abstractions.xml", + "microsoft.extensions.fileproviders.abstractions.9.0.0.nupkg.sha512", + "microsoft.extensions.fileproviders.abstractions.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "Microsoft.Extensions.Hosting.Abstractions/9.0.0": { + "sha512": "yUKJgu81ExjvqbNWqZKshBbLntZMbMVz/P7Way2SBx7bMqA08Mfdc9O7hWDKAiSp+zPUGT6LKcSCQIPeDK+CCw==", + "type": "package", + "path": "microsoft.extensions.hosting.abstractions/9.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "PACKAGE.md", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/Microsoft.Extensions.Hosting.Abstractions.targets", + "buildTransitive/net462/_._", + "buildTransitive/net8.0/_._", + "buildTransitive/netcoreapp2.0/Microsoft.Extensions.Hosting.Abstractions.targets", + "lib/net462/Microsoft.Extensions.Hosting.Abstractions.dll", + "lib/net462/Microsoft.Extensions.Hosting.Abstractions.xml", + "lib/net8.0/Microsoft.Extensions.Hosting.Abstractions.dll", + "lib/net8.0/Microsoft.Extensions.Hosting.Abstractions.xml", + "lib/net9.0/Microsoft.Extensions.Hosting.Abstractions.dll", + "lib/net9.0/Microsoft.Extensions.Hosting.Abstractions.xml", + "lib/netstandard2.0/Microsoft.Extensions.Hosting.Abstractions.dll", + "lib/netstandard2.0/Microsoft.Extensions.Hosting.Abstractions.xml", + "lib/netstandard2.1/Microsoft.Extensions.Hosting.Abstractions.dll", + "lib/netstandard2.1/Microsoft.Extensions.Hosting.Abstractions.xml", + "microsoft.extensions.hosting.abstractions.9.0.0.nupkg.sha512", + "microsoft.extensions.hosting.abstractions.nuspec", + "useSharedDesignerContext.txt" + ] + }, "Microsoft.Extensions.Logging/9.0.0": { "sha512": "crjWyORoug0kK7RSNJBTeSE6VX8IQgLf3nUpTB9m62bPXp/tzbnOsnbe8TXEG0AASNaKZddnpHKw7fET8E++Pg==", "type": "package", diff --git a/CSharpPizza.DTO/Admin/AdminOrderFilterDto.cs b/CSharpPizza.DTO/Admin/AdminOrderFilterDto.cs new file mode 100644 index 0000000..7e03243 --- /dev/null +++ b/CSharpPizza.DTO/Admin/AdminOrderFilterDto.cs @@ -0,0 +1,9 @@ +namespace CSharpPizza.DTO.Admin; + +public class AdminOrderFilterDto +{ + public string? Status { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public string? CustomerName { get; set; } +} \ No newline at end of file diff --git a/CSharpPizza.DTO/Admin/AdminOrderListDto.cs b/CSharpPizza.DTO/Admin/AdminOrderListDto.cs new file mode 100644 index 0000000..ae49fd6 --- /dev/null +++ b/CSharpPizza.DTO/Admin/AdminOrderListDto.cs @@ -0,0 +1,13 @@ +namespace CSharpPizza.DTO.Admin; + +public class AdminOrderListDto +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string CustomerName { get; set; } = string.Empty; + public string CustomerEmail { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public decimal TotalAmount { get; set; } + public int ItemCount { get; set; } + public DateTime CreatedAt { get; set; } +} \ No newline at end of file diff --git a/CSharpPizza.DTO/Mappings/AdminProfile.cs b/CSharpPizza.DTO/Mappings/AdminProfile.cs new file mode 100644 index 0000000..7b9a739 --- /dev/null +++ b/CSharpPizza.DTO/Mappings/AdminProfile.cs @@ -0,0 +1,18 @@ +using AutoMapper; +using CSharpPizza.Data.Entities; +using CSharpPizza.DTO.Admin; + +namespace CSharpPizza.DTO.Mappings; + +public class AdminProfile : Profile +{ + public AdminProfile() + { + // Order -> AdminOrderListDto + CreateMap() + .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status.ToString())) + .ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.OrderItems.Count)) + .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.User.Name)) + .ForMember(dest => dest.CustomerEmail, opt => opt.MapFrom(src => src.User.Email)); + } +} \ No newline at end of file diff --git a/CSharpPizza.DTO/obj/Debug/net9.0/CSharpPizza.DTO.AssemblyInfo.cs b/CSharpPizza.DTO/obj/Debug/net9.0/CSharpPizza.DTO.AssemblyInfo.cs index 3219431..5ee94d8 100644 --- a/CSharpPizza.DTO/obj/Debug/net9.0/CSharpPizza.DTO.AssemblyInfo.cs +++ b/CSharpPizza.DTO/obj/Debug/net9.0/CSharpPizza.DTO.AssemblyInfo.cs @@ -13,7 +13,7 @@ [assembly: System.Reflection.AssemblyCompanyAttribute("CSharpPizza.DTO")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+0f2ea68a7cfc1ee055cda29bd334562793872a4a")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+aacba4cc90a48cc425765b19fcf6068933c39438")] [assembly: System.Reflection.AssemblyProductAttribute("CSharpPizza.DTO")] [assembly: System.Reflection.AssemblyTitleAttribute("CSharpPizza.DTO")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/CSharpPizza.DTO/obj/Debug/net9.0/CSharpPizza.DTO.csproj.FileListAbsolute.txt b/CSharpPizza.DTO/obj/Debug/net9.0/CSharpPizza.DTO.csproj.FileListAbsolute.txt index 493b456..fd63962 100644 --- a/CSharpPizza.DTO/obj/Debug/net9.0/CSharpPizza.DTO.csproj.FileListAbsolute.txt +++ b/CSharpPizza.DTO/obj/Debug/net9.0/CSharpPizza.DTO.csproj.FileListAbsolute.txt @@ -1,16 +1,16 @@ C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\bin\Debug\net9.0\CSharpPizza.DTO.deps.json C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\bin\Debug\net9.0\CSharpPizza.DTO.dll C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\bin\Debug\net9.0\CSharpPizza.DTO.pdb +C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\bin\Debug\net9.0\CSharpPizza.Data.dll +C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\bin\Debug\net9.0\CSharpPizza.Data.pdb +C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\CSharpPizza.DTO.csproj.AssemblyReference.cache C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\CSharpPizza.DTO.GeneratedMSBuildEditorConfig.editorconfig C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\CSharpPizza.DTO.AssemblyInfoInputs.cache C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\CSharpPizza.DTO.AssemblyInfo.cs C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\CSharpPizza.DTO.csproj.CoreCompileInputs.cache +C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\CSharpPizza.DTO.sourcelink.json +C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\CSharpPi.AF978441.Up2Date C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\CSharpPizza.DTO.dll C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\refint\CSharpPizza.DTO.dll C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\CSharpPizza.DTO.pdb C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\ref\CSharpPizza.DTO.dll -C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\bin\Debug\net9.0\CSharpPizza.Data.dll -C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\bin\Debug\net9.0\CSharpPizza.Data.pdb -C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\CSharpPizza.DTO.csproj.AssemblyReference.cache -C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\CSharpPi.AF978441.Up2Date -C:\Users\princ\repo\csharp-project\CSharpPizza.DTO\obj\Debug\net9.0\CSharpPizza.DTO.sourcelink.json diff --git a/CSharpPizza.Data/Entities/Log.cs b/CSharpPizza.Data/Entities/Log.cs new file mode 100644 index 0000000..32abeed --- /dev/null +++ b/CSharpPizza.Data/Entities/Log.cs @@ -0,0 +1,12 @@ +namespace CSharpPizza.Data.Entities; + +public class Log : BaseEntity +{ + public string LogLevel { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; + public string? Details { get; set; } + public int? UserId { get; set; } + public string? Endpoint { get; set; } + public string? HttpMethod { get; set; } + public int? StatusCode { get; set; } +} \ No newline at end of file diff --git a/CSharpPizza.Data/Migrations/20251113045133_AddLogEntity.Designer.cs b/CSharpPizza.Data/Migrations/20251113045133_AddLogEntity.Designer.cs new file mode 100644 index 0000000..6c90183 --- /dev/null +++ b/CSharpPizza.Data/Migrations/20251113045133_AddLogEntity.Designer.cs @@ -0,0 +1,474 @@ +ο»Ώ// +using System; +using CSharpPizza.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CSharpPizza.Data.Migrations +{ + [DbContext(typeof(PizzaDbContext))] + [Migration("20251113045133_AddLogEntity")] + partial class AddLogEntity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("CSharpPizza.Data.Entities.Cart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Carts"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CartId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("PizzaId") + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.HasIndex("PizzaId"); + + b.ToTable("CartItems"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.CartItemTopping", b => + { + b.Property("CartItemId") + .HasColumnType("TEXT"); + + b.Property("ToppingId") + .HasColumnType("TEXT"); + + b.Property("IsAdded") + .HasColumnType("INTEGER"); + + b.HasKey("CartItemId", "ToppingId"); + + b.HasIndex("ToppingId"); + + b.ToTable("CartItemToppings"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Endpoint") + .HasColumnType("TEXT"); + + b.Property("HttpMethod") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LogLevel") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StatusCode") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TotalAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("OrderId") + .HasColumnType("TEXT"); + + b.Property("PizzaName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PizzaPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("Toppings") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.Pizza", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BasePrice") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ImageUrl") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Pizzas"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.PizzaTopping", b => + { + b.Property("PizzaId") + .HasColumnType("TEXT"); + + b.Property("ToppingId") + .HasColumnType("TEXT"); + + b.HasKey("PizzaId", "ToppingId"); + + b.HasIndex("ToppingId"); + + b.ToTable("PizzaToppings"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.Topping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Cost") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Toppings"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("Mobile") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserRole") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.Cart", b => + { + b.HasOne("CSharpPizza.Data.Entities.User", "User") + .WithOne("Cart") + .HasForeignKey("CSharpPizza.Data.Entities.Cart", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.CartItem", b => + { + b.HasOne("CSharpPizza.Data.Entities.Cart", "Cart") + .WithMany("CartItems") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CSharpPizza.Data.Entities.Pizza", "Pizza") + .WithMany("CartItems") + .HasForeignKey("PizzaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + + b.Navigation("Pizza"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.CartItemTopping", b => + { + b.HasOne("CSharpPizza.Data.Entities.CartItem", "CartItem") + .WithMany("CartItemToppings") + .HasForeignKey("CartItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CSharpPizza.Data.Entities.Topping", "Topping") + .WithMany("CartItemToppings") + .HasForeignKey("ToppingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CartItem"); + + b.Navigation("Topping"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.Order", b => + { + b.HasOne("CSharpPizza.Data.Entities.User", "User") + .WithMany("Orders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.OrderItem", b => + { + b.HasOne("CSharpPizza.Data.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.PizzaTopping", b => + { + b.HasOne("CSharpPizza.Data.Entities.Pizza", "Pizza") + .WithMany("PizzaToppings") + .HasForeignKey("PizzaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CSharpPizza.Data.Entities.Topping", "Topping") + .WithMany("PizzaToppings") + .HasForeignKey("ToppingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Pizza"); + + b.Navigation("Topping"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.Cart", b => + { + b.Navigation("CartItems"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.CartItem", b => + { + b.Navigation("CartItemToppings"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.Order", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.Pizza", b => + { + b.Navigation("CartItems"); + + b.Navigation("PizzaToppings"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.Topping", b => + { + b.Navigation("CartItemToppings"); + + b.Navigation("PizzaToppings"); + }); + + modelBuilder.Entity("CSharpPizza.Data.Entities.User", b => + { + b.Navigation("Cart"); + + b.Navigation("Orders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CSharpPizza.Data/Migrations/20251113045133_AddLogEntity.cs b/CSharpPizza.Data/Migrations/20251113045133_AddLogEntity.cs new file mode 100644 index 0000000..fed39c9 --- /dev/null +++ b/CSharpPizza.Data/Migrations/20251113045133_AddLogEntity.cs @@ -0,0 +1,43 @@ +ο»Ώusing System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CSharpPizza.Data.Migrations +{ + /// + public partial class AddLogEntity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Logs", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + LogLevel = table.Column(type: "TEXT", nullable: false), + Message = table.Column(type: "TEXT", nullable: false), + Details = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "INTEGER", nullable: true), + Endpoint = table.Column(type: "TEXT", nullable: true), + HttpMethod = table.Column(type: "TEXT", nullable: true), + StatusCode = table.Column(type: "INTEGER", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Logs", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Logs"); + } + } +} diff --git a/CSharpPizza.Data/Migrations/PizzaDbContextModelSnapshot.cs b/CSharpPizza.Data/Migrations/PizzaDbContextModelSnapshot.cs index d002c49..1519ba6 100644 --- a/CSharpPizza.Data/Migrations/PizzaDbContextModelSnapshot.cs +++ b/CSharpPizza.Data/Migrations/PizzaDbContextModelSnapshot.cs @@ -94,6 +94,49 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("CartItemToppings"); }); + modelBuilder.Entity("CSharpPizza.Data.Entities.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Endpoint") + .HasColumnType("TEXT"); + + b.Property("HttpMethod") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LogLevel") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StatusCode") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + modelBuilder.Entity("CSharpPizza.Data.Entities.Order", b => { b.Property("Id") diff --git a/CSharpPizza.Data/PizzaDbContext.cs b/CSharpPizza.Data/PizzaDbContext.cs index 61b1c8a..de4f3b0 100644 --- a/CSharpPizza.Data/PizzaDbContext.cs +++ b/CSharpPizza.Data/PizzaDbContext.cs @@ -18,6 +18,7 @@ public PizzaDbContext(DbContextOptions options) : base(options) public DbSet CartItemToppings => Set(); public DbSet Orders => Set(); public DbSet OrderItems => Set(); + public DbSet Logs => Set(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -39,6 +40,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasQueryFilter(ci => !ci.IsDeleted); modelBuilder.Entity().HasQueryFilter(o => !o.IsDeleted); modelBuilder.Entity().HasQueryFilter(oi => !oi.IsDeleted); + modelBuilder.Entity().HasQueryFilter(l => !l.IsDeleted); // User configuration modelBuilder.Entity(entity => @@ -162,6 +164,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(oi => oi.OrderId) .OnDelete(DeleteBehavior.Cascade); }); + + // Log configuration + modelBuilder.Entity(entity => + { + entity.HasKey(l => l.Id); + entity.Property(l => l.LogLevel).IsRequired(); + entity.Property(l => l.Message).IsRequired(); + }); } public override int SaveChanges() diff --git a/CSharpPizza.Data/Repositories/ILogRepository.cs b/CSharpPizza.Data/Repositories/ILogRepository.cs new file mode 100644 index 0000000..8becf5d --- /dev/null +++ b/CSharpPizza.Data/Repositories/ILogRepository.cs @@ -0,0 +1,7 @@ +using CSharpPizza.Data.Entities; + +namespace CSharpPizza.Data.Repositories; + +public interface ILogRepository : IRepository +{ +} \ No newline at end of file diff --git a/CSharpPizza.Data/Repositories/LogRepository.cs b/CSharpPizza.Data/Repositories/LogRepository.cs new file mode 100644 index 0000000..8a827cd --- /dev/null +++ b/CSharpPizza.Data/Repositories/LogRepository.cs @@ -0,0 +1,10 @@ +using CSharpPizza.Data.Entities; + +namespace CSharpPizza.Data.Repositories; + +public class LogRepository : Repository, ILogRepository +{ + public LogRepository(PizzaDbContext context) : base(context) + { + } +} \ No newline at end of file diff --git a/CSharpPizza.Data/obj/Debug/net9.0/CSharpPizza.Data.AssemblyInfo.cs b/CSharpPizza.Data/obj/Debug/net9.0/CSharpPizza.Data.AssemblyInfo.cs index 021b67a..43be259 100644 --- a/CSharpPizza.Data/obj/Debug/net9.0/CSharpPizza.Data.AssemblyInfo.cs +++ b/CSharpPizza.Data/obj/Debug/net9.0/CSharpPizza.Data.AssemblyInfo.cs @@ -13,7 +13,7 @@ [assembly: System.Reflection.AssemblyCompanyAttribute("CSharpPizza.Data")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+0f2ea68a7cfc1ee055cda29bd334562793872a4a")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+aacba4cc90a48cc425765b19fcf6068933c39438")] [assembly: System.Reflection.AssemblyProductAttribute("CSharpPizza.Data")] [assembly: System.Reflection.AssemblyTitleAttribute("CSharpPizza.Data")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/CSharpPizza.Data/obj/Debug/net9.0/CSharpPizza.Data.csproj.FileListAbsolute.txt b/CSharpPizza.Data/obj/Debug/net9.0/CSharpPizza.Data.csproj.FileListAbsolute.txt index 3dba4fc..2d97f57 100644 --- a/CSharpPizza.Data/obj/Debug/net9.0/CSharpPizza.Data.csproj.FileListAbsolute.txt +++ b/CSharpPizza.Data/obj/Debug/net9.0/CSharpPizza.Data.csproj.FileListAbsolute.txt @@ -7,9 +7,9 @@ C:\Users\princ\repo\csharp-project\CSharpPizza.Data\obj\Debug\net9.0\CSharpPizza C:\Users\princ\repo\csharp-project\CSharpPizza.Data\obj\Debug\net9.0\CSharpPizza.Data.AssemblyInfoInputs.cache C:\Users\princ\repo\csharp-project\CSharpPizza.Data\obj\Debug\net9.0\CSharpPizza.Data.AssemblyInfo.cs C:\Users\princ\repo\csharp-project\CSharpPizza.Data\obj\Debug\net9.0\CSharpPizza.Data.csproj.CoreCompileInputs.cache +C:\Users\princ\repo\csharp-project\CSharpPizza.Data\obj\Debug\net9.0\CSharpPizza.Data.sourcelink.json C:\Users\princ\repo\csharp-project\CSharpPizza.Data\obj\Debug\net9.0\CSharpPizza.Data.dll C:\Users\princ\repo\csharp-project\CSharpPizza.Data\obj\Debug\net9.0\refint\CSharpPizza.Data.dll C:\Users\princ\repo\csharp-project\CSharpPizza.Data\obj\Debug\net9.0\CSharpPizza.Data.pdb C:\Users\princ\repo\csharp-project\CSharpPizza.Data\obj\Debug\net9.0\CSharpPizza.Data.genruntimeconfig.cache C:\Users\princ\repo\csharp-project\CSharpPizza.Data\obj\Debug\net9.0\ref\CSharpPizza.Data.dll -C:\Users\princ\repo\csharp-project\CSharpPizza.Data\obj\Debug\net9.0\CSharpPizza.Data.sourcelink.json diff --git a/CSharpPizza.Domain/CSharpPizza.Domain.csproj b/CSharpPizza.Domain/CSharpPizza.Domain.csproj index 65a7678..fcabe38 100644 --- a/CSharpPizza.Domain/CSharpPizza.Domain.csproj +++ b/CSharpPizza.Domain/CSharpPizza.Domain.csproj @@ -7,6 +7,7 @@ + diff --git a/CSharpPizza.Domain/Extensions/StringExtensions.cs b/CSharpPizza.Domain/Extensions/StringExtensions.cs new file mode 100644 index 0000000..01e8436 --- /dev/null +++ b/CSharpPizza.Domain/Extensions/StringExtensions.cs @@ -0,0 +1,10 @@ +namespace CSharpPizza.Domain.Extensions; + +public static class StringExtensions +{ + public static async Task FetchDataAsync(this string url) + { + using var client = new HttpClient(); + return await client.GetStringAsync(url); + } +} \ No newline at end of file diff --git a/CSharpPizza.Domain/Models/LogEntry.cs b/CSharpPizza.Domain/Models/LogEntry.cs new file mode 100644 index 0000000..b2ac366 --- /dev/null +++ b/CSharpPizza.Domain/Models/LogEntry.cs @@ -0,0 +1,16 @@ +namespace CSharpPizza.Domain.Models; + +/// +/// Represents a log entry to be queued for background processing +/// +public class LogEntry +{ + public string Level { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; + public string? Details { get; set; } + public int? UserId { get; set; } + public string? Endpoint { get; set; } + public string? HttpMethod { get; set; } + public int? StatusCode { get; set; } + public DateTime Timestamp { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/CSharpPizza.Domain/Services/ILoggingService.cs b/CSharpPizza.Domain/Services/ILoggingService.cs new file mode 100644 index 0000000..116a1a3 --- /dev/null +++ b/CSharpPizza.Domain/Services/ILoggingService.cs @@ -0,0 +1,8 @@ +namespace CSharpPizza.Domain.Services; + +public interface ILoggingService +{ + Task LogAsync(string level, string message, string? details = null, int? userId = null, string? endpoint = null, string? httpMethod = null, int? statusCode = null); + Task LogInfoAsync(string message, string? details = null); + Task LogErrorAsync(string message, string? details = null); +} \ No newline at end of file diff --git a/CSharpPizza.Domain/Services/LoggingService.cs b/CSharpPizza.Domain/Services/LoggingService.cs new file mode 100644 index 0000000..6f4ac90 --- /dev/null +++ b/CSharpPizza.Domain/Services/LoggingService.cs @@ -0,0 +1,47 @@ +using CSharpPizza.Data.Entities; +using CSharpPizza.Data.Repositories; + +namespace CSharpPizza.Domain.Services; + +public class LoggingService : ILoggingService +{ + private readonly ILogRepository _logRepository; + + public LoggingService(ILogRepository logRepository) + { + _logRepository = logRepository; + } + + public Task LogAsync(string level, string message, string? details = null, int? userId = null, string? endpoint = null, string? httpMethod = null, int? statusCode = null) + { + Task.Run(async () => + { + var log = new Log + { + Id = Guid.NewGuid(), + LogLevel = level, + Message = message, + Details = details, + UserId = userId, + Endpoint = endpoint, + HttpMethod = httpMethod, + StatusCode = statusCode + }; + + await _logRepository.AddAsync(log); + await _logRepository.SaveChangesAsync(); + }); + + return Task.CompletedTask; + } + + public Task LogInfoAsync(string message, string? details = null) + { + return LogAsync("Info", message, details); + } + + public Task LogErrorAsync(string message, string? details = null) + { + return LogAsync("Error", message, details); + } +} \ No newline at end of file diff --git a/CSharpPizza.Domain/Services/PizzaService.cs b/CSharpPizza.Domain/Services/PizzaService.cs index 0d9545c..0e0df0e 100644 --- a/CSharpPizza.Domain/Services/PizzaService.cs +++ b/CSharpPizza.Domain/Services/PizzaService.cs @@ -1,6 +1,7 @@ using AutoMapper; using CSharpPizza.Data.Entities; using CSharpPizza.Data.Repositories; +using CSharpPizza.Domain.Extensions; using CSharpPizza.DTO.Pizzas; namespace CSharpPizza.Domain.Services; @@ -61,11 +62,18 @@ public async Task> GetAllAsync(CancellationToken cancellation public async Task CreateAsync(CreatePizzaDto createDto, CancellationToken cancellationToken = default) { + + var description = createDto.Description; + if (string.IsNullOrWhiteSpace(description)) + { + description = await "https://lorem-api.com/api/lorem".FetchDataAsync(); + } + // Create pizza entity var pizza = new Pizza { Name = createDto.Name, - Description = createDto.Description, + Description = description, BasePrice = createDto.BasePrice, ImageUrl = createDto.ImageUrl, Slug = GenerateSlug(createDto.Name) @@ -104,9 +112,15 @@ public async Task UpdateAsync(Guid id, UpdatePizzaDto updateDto, Cance throw new KeyNotFoundException($"Pizza with ID {id} not found"); } + var description = updateDto.Description; + if (string.IsNullOrWhiteSpace(description)) + { + description = await "https://lorem-api.com/api/lorem".FetchDataAsync(); + } + // Update pizza properties pizza.Name = updateDto.Name; - pizza.Description = updateDto.Description; + pizza.Description = description; pizza.BasePrice = updateDto.BasePrice; pizza.ImageUrl = updateDto.ImageUrl; pizza.Slug = GenerateSlug(updateDto.Name); diff --git a/CSharpPizza.Domain/Services/ToppingService.cs b/CSharpPizza.Domain/Services/ToppingService.cs index 51a9992..4bb5b13 100644 --- a/CSharpPizza.Domain/Services/ToppingService.cs +++ b/CSharpPizza.Domain/Services/ToppingService.cs @@ -1,6 +1,7 @@ using AutoMapper; using CSharpPizza.Data.Entities; using CSharpPizza.Data.Repositories; +using CSharpPizza.Domain.Extensions; using CSharpPizza.DTO.Toppings; namespace CSharpPizza.Domain.Services; @@ -30,10 +31,17 @@ public async Task> GetAllAsync(CancellationToken cancellationTo public async Task CreateAsync(CreateToppingDto createDto, CancellationToken cancellationToken = default) { + + var description = createDto.Description; + if (string.IsNullOrWhiteSpace(description)) + { + description = await "https://lorem-api.com/api/lorem".FetchDataAsync(); + } + var topping = new Topping { Name = createDto.Name, - Description = createDto.Description, + Description = description, Cost = createDto.Cost }; @@ -51,8 +59,14 @@ public async Task UpdateAsync(Guid id, UpdateToppingDto updateDto, C throw new KeyNotFoundException($"Topping with ID {id} not found"); } + var description = updateDto.Description; + if (string.IsNullOrWhiteSpace(description)) + { + description = await "https://lorem-api.com/api/lorem".FetchDataAsync(); + } + topping.Name = updateDto.Name; - topping.Description = updateDto.Description; + topping.Description = description; topping.Cost = updateDto.Cost; await _toppingRepository.UpdateAsync(topping, cancellationToken); diff --git a/CSharpPizza.Domain/bin/Debug/net9.0/CSharpPizza.Domain.deps.json b/CSharpPizza.Domain/bin/Debug/net9.0/CSharpPizza.Domain.deps.json index 484fefd..439bb4f 100644 --- a/CSharpPizza.Domain/bin/Debug/net9.0/CSharpPizza.Domain.deps.json +++ b/CSharpPizza.Domain/bin/Debug/net9.0/CSharpPizza.Domain.deps.json @@ -11,6 +11,7 @@ "BCrypt.Net-Next": "4.0.3", "CSharpPizza.DTO": "1.0.0", "CSharpPizza.Data": "1.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "9.0.0", "Microsoft.IdentityModel.Tokens": "8.14.0", "System.IdentityModel.Tokens.Jwt": "8.14.0" }, @@ -192,6 +193,44 @@ } } }, + "Microsoft.Extensions.Diagnostics.Abstractions/9.0.0": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Diagnostics.Abstractions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.24.52809" + } + } + }, + "Microsoft.Extensions.FileProviders.Abstractions/9.0.0": { + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.0" + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.FileProviders.Abstractions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.24.52809" + } + } + }, + "Microsoft.Extensions.Hosting.Abstractions/9.0.0": { + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0" + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Hosting.Abstractions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.24.52809" + } + } + }, "Microsoft.Extensions.Logging/9.0.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.0", @@ -587,6 +626,27 @@ "path": "microsoft.extensions.dependencymodel/9.0.0", "hashPath": "microsoft.extensions.dependencymodel.9.0.0.nupkg.sha512" }, + "Microsoft.Extensions.Diagnostics.Abstractions/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-1K8P7XzuzX8W8pmXcZjcrqS6x5eSSdvhQohmcpgiQNY/HlDAlnrhR9dvlURfFz428A+RTCJpUyB+aKTA6AgVcQ==", + "path": "microsoft.extensions.diagnostics.abstractions/9.0.0", + "hashPath": "microsoft.extensions.diagnostics.abstractions.9.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.FileProviders.Abstractions/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-uK439QzYR0q2emLVtYzwyK3x+T5bTY4yWsd/k/ZUS9LR6Sflp8MIdhGXW8kQCd86dQD4tLqvcbLkku8qHY263Q==", + "path": "microsoft.extensions.fileproviders.abstractions/9.0.0", + "hashPath": "microsoft.extensions.fileproviders.abstractions.9.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Hosting.Abstractions/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-yUKJgu81ExjvqbNWqZKshBbLntZMbMVz/P7Way2SBx7bMqA08Mfdc9O7hWDKAiSp+zPUGT6LKcSCQIPeDK+CCw==", + "path": "microsoft.extensions.hosting.abstractions/9.0.0", + "hashPath": "microsoft.extensions.hosting.abstractions.9.0.0.nupkg.sha512" + }, "Microsoft.Extensions.Logging/9.0.0": { "type": "package", "serviceable": true, diff --git a/CSharpPizza.Domain/obj/CSharpPizza.Domain.csproj.nuget.dgspec.json b/CSharpPizza.Domain/obj/CSharpPizza.Domain.csproj.nuget.dgspec.json index 24a6365..f313721 100644 --- a/CSharpPizza.Domain/obj/CSharpPizza.Domain.csproj.nuget.dgspec.json +++ b/CSharpPizza.Domain/obj/CSharpPizza.Domain.csproj.nuget.dgspec.json @@ -148,6 +148,10 @@ "target": "Package", "version": "[4.0.3, )" }, + "Microsoft.Extensions.Hosting.Abstractions": { + "target": "Package", + "version": "[9.0.0, )" + }, "Microsoft.IdentityModel.Tokens": { "target": "Package", "version": "[8.14.0, )" diff --git a/CSharpPizza.Domain/obj/Debug/net9.0/CSharpPizza.Domain.AssemblyInfo.cs b/CSharpPizza.Domain/obj/Debug/net9.0/CSharpPizza.Domain.AssemblyInfo.cs index b3cf2d9..33e6535 100644 --- a/CSharpPizza.Domain/obj/Debug/net9.0/CSharpPizza.Domain.AssemblyInfo.cs +++ b/CSharpPizza.Domain/obj/Debug/net9.0/CSharpPizza.Domain.AssemblyInfo.cs @@ -13,7 +13,7 @@ [assembly: System.Reflection.AssemblyCompanyAttribute("CSharpPizza.Domain")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+0f2ea68a7cfc1ee055cda29bd334562793872a4a")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+37f3aaa8ab62a512cffac90d34ff442f110ff7c0")] [assembly: System.Reflection.AssemblyProductAttribute("CSharpPizza.Domain")] [assembly: System.Reflection.AssemblyTitleAttribute("CSharpPizza.Domain")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/CSharpPizza.Domain/obj/Debug/net9.0/CSharpPizza.Domain.csproj.FileListAbsolute.txt b/CSharpPizza.Domain/obj/Debug/net9.0/CSharpPizza.Domain.csproj.FileListAbsolute.txt index 54a2d31..f0e8991 100644 --- a/CSharpPizza.Domain/obj/Debug/net9.0/CSharpPizza.Domain.csproj.FileListAbsolute.txt +++ b/CSharpPizza.Domain/obj/Debug/net9.0/CSharpPizza.Domain.csproj.FileListAbsolute.txt @@ -10,9 +10,9 @@ C:\Users\princ\repo\csharp-project\CSharpPizza.Domain\obj\Debug\net9.0\CSharpPiz C:\Users\princ\repo\csharp-project\CSharpPizza.Domain\obj\Debug\net9.0\CSharpPizza.Domain.AssemblyInfoInputs.cache C:\Users\princ\repo\csharp-project\CSharpPizza.Domain\obj\Debug\net9.0\CSharpPizza.Domain.AssemblyInfo.cs C:\Users\princ\repo\csharp-project\CSharpPizza.Domain\obj\Debug\net9.0\CSharpPizza.Domain.csproj.CoreCompileInputs.cache +C:\Users\princ\repo\csharp-project\CSharpPizza.Domain\obj\Debug\net9.0\CSharpPizza.Domain.sourcelink.json C:\Users\princ\repo\csharp-project\CSharpPizza.Domain\obj\Debug\net9.0\CSharpPi.DB1CE04A.Up2Date C:\Users\princ\repo\csharp-project\CSharpPizza.Domain\obj\Debug\net9.0\CSharpPizza.Domain.dll C:\Users\princ\repo\csharp-project\CSharpPizza.Domain\obj\Debug\net9.0\refint\CSharpPizza.Domain.dll C:\Users\princ\repo\csharp-project\CSharpPizza.Domain\obj\Debug\net9.0\CSharpPizza.Domain.pdb C:\Users\princ\repo\csharp-project\CSharpPizza.Domain\obj\Debug\net9.0\ref\CSharpPizza.Domain.dll -C:\Users\princ\repo\csharp-project\CSharpPizza.Domain\obj\Debug\net9.0\CSharpPizza.Domain.sourcelink.json diff --git a/CSharpPizza.Domain/obj/project.assets.json b/CSharpPizza.Domain/obj/project.assets.json index 82a79f2..f2645fe 100644 --- a/CSharpPizza.Domain/obj/project.assets.json +++ b/CSharpPizza.Domain/obj/project.assets.json @@ -280,6 +280,68 @@ "buildTransitive/net8.0/_._": {} } }, + "Microsoft.Extensions.Diagnostics.Abstractions/9.0.0": { + "type": "package", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + }, + "compile": { + "lib/net9.0/Microsoft.Extensions.Diagnostics.Abstractions.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Diagnostics.Abstractions.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net8.0/_._": {} + } + }, + "Microsoft.Extensions.FileProviders.Abstractions/9.0.0": { + "type": "package", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.0" + }, + "compile": { + "lib/net9.0/Microsoft.Extensions.FileProviders.Abstractions.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.FileProviders.Abstractions.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net8.0/_._": {} + } + }, + "Microsoft.Extensions.Hosting.Abstractions/9.0.0": { + "type": "package", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0" + }, + "compile": { + "lib/net9.0/Microsoft.Extensions.Hosting.Abstractions.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Hosting.Abstractions.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net8.0/_._": {} + } + }, "Microsoft.Extensions.Logging/9.0.0": { "type": "package", "dependencies": { @@ -1036,6 +1098,91 @@ "useSharedDesignerContext.txt" ] }, + "Microsoft.Extensions.Diagnostics.Abstractions/9.0.0": { + "sha512": "1K8P7XzuzX8W8pmXcZjcrqS6x5eSSdvhQohmcpgiQNY/HlDAlnrhR9dvlURfFz428A+RTCJpUyB+aKTA6AgVcQ==", + "type": "package", + "path": "microsoft.extensions.diagnostics.abstractions/9.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/Microsoft.Extensions.Diagnostics.Abstractions.targets", + "buildTransitive/net462/_._", + "buildTransitive/net8.0/_._", + "buildTransitive/netcoreapp2.0/Microsoft.Extensions.Diagnostics.Abstractions.targets", + "lib/net462/Microsoft.Extensions.Diagnostics.Abstractions.dll", + "lib/net462/Microsoft.Extensions.Diagnostics.Abstractions.xml", + "lib/net8.0/Microsoft.Extensions.Diagnostics.Abstractions.dll", + "lib/net8.0/Microsoft.Extensions.Diagnostics.Abstractions.xml", + "lib/net9.0/Microsoft.Extensions.Diagnostics.Abstractions.dll", + "lib/net9.0/Microsoft.Extensions.Diagnostics.Abstractions.xml", + "lib/netstandard2.0/Microsoft.Extensions.Diagnostics.Abstractions.dll", + "lib/netstandard2.0/Microsoft.Extensions.Diagnostics.Abstractions.xml", + "microsoft.extensions.diagnostics.abstractions.9.0.0.nupkg.sha512", + "microsoft.extensions.diagnostics.abstractions.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "Microsoft.Extensions.FileProviders.Abstractions/9.0.0": { + "sha512": "uK439QzYR0q2emLVtYzwyK3x+T5bTY4yWsd/k/ZUS9LR6Sflp8MIdhGXW8kQCd86dQD4tLqvcbLkku8qHY263Q==", + "type": "package", + "path": "microsoft.extensions.fileproviders.abstractions/9.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "PACKAGE.md", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/Microsoft.Extensions.FileProviders.Abstractions.targets", + "buildTransitive/net462/_._", + "buildTransitive/net8.0/_._", + "buildTransitive/netcoreapp2.0/Microsoft.Extensions.FileProviders.Abstractions.targets", + "lib/net462/Microsoft.Extensions.FileProviders.Abstractions.dll", + "lib/net462/Microsoft.Extensions.FileProviders.Abstractions.xml", + "lib/net8.0/Microsoft.Extensions.FileProviders.Abstractions.dll", + "lib/net8.0/Microsoft.Extensions.FileProviders.Abstractions.xml", + "lib/net9.0/Microsoft.Extensions.FileProviders.Abstractions.dll", + "lib/net9.0/Microsoft.Extensions.FileProviders.Abstractions.xml", + "lib/netstandard2.0/Microsoft.Extensions.FileProviders.Abstractions.dll", + "lib/netstandard2.0/Microsoft.Extensions.FileProviders.Abstractions.xml", + "microsoft.extensions.fileproviders.abstractions.9.0.0.nupkg.sha512", + "microsoft.extensions.fileproviders.abstractions.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "Microsoft.Extensions.Hosting.Abstractions/9.0.0": { + "sha512": "yUKJgu81ExjvqbNWqZKshBbLntZMbMVz/P7Way2SBx7bMqA08Mfdc9O7hWDKAiSp+zPUGT6LKcSCQIPeDK+CCw==", + "type": "package", + "path": "microsoft.extensions.hosting.abstractions/9.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "PACKAGE.md", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/Microsoft.Extensions.Hosting.Abstractions.targets", + "buildTransitive/net462/_._", + "buildTransitive/net8.0/_._", + "buildTransitive/netcoreapp2.0/Microsoft.Extensions.Hosting.Abstractions.targets", + "lib/net462/Microsoft.Extensions.Hosting.Abstractions.dll", + "lib/net462/Microsoft.Extensions.Hosting.Abstractions.xml", + "lib/net8.0/Microsoft.Extensions.Hosting.Abstractions.dll", + "lib/net8.0/Microsoft.Extensions.Hosting.Abstractions.xml", + "lib/net9.0/Microsoft.Extensions.Hosting.Abstractions.dll", + "lib/net9.0/Microsoft.Extensions.Hosting.Abstractions.xml", + "lib/netstandard2.0/Microsoft.Extensions.Hosting.Abstractions.dll", + "lib/netstandard2.0/Microsoft.Extensions.Hosting.Abstractions.xml", + "lib/netstandard2.1/Microsoft.Extensions.Hosting.Abstractions.dll", + "lib/netstandard2.1/Microsoft.Extensions.Hosting.Abstractions.xml", + "microsoft.extensions.hosting.abstractions.9.0.0.nupkg.sha512", + "microsoft.extensions.hosting.abstractions.nuspec", + "useSharedDesignerContext.txt" + ] + }, "Microsoft.Extensions.Logging/9.0.0": { "sha512": "crjWyORoug0kK7RSNJBTeSE6VX8IQgLf3nUpTB9m62bPXp/tzbnOsnbe8TXEG0AASNaKZddnpHKw7fET8E++Pg==", "type": "package", @@ -1530,6 +1677,7 @@ "BCrypt.Net-Next >= 4.0.3", "CSharpPizza.DTO >= 1.0.0", "CSharpPizza.Data >= 1.0.0", + "Microsoft.Extensions.Hosting.Abstractions >= 9.0.0", "Microsoft.IdentityModel.Tokens >= 8.14.0", "System.IdentityModel.Tokens.Jwt >= 8.14.0" ] @@ -1599,6 +1747,10 @@ "target": "Package", "version": "[4.0.3, )" }, + "Microsoft.Extensions.Hosting.Abstractions": { + "target": "Package", + "version": "[9.0.0, )" + }, "Microsoft.IdentityModel.Tokens": { "target": "Package", "version": "[8.14.0, )" diff --git a/CSharpPizza.WebUI/src/App.tsx b/CSharpPizza.WebUI/src/App.tsx index 365d061..4dc9f8e 100644 --- a/CSharpPizza.WebUI/src/App.tsx +++ b/CSharpPizza.WebUI/src/App.tsx @@ -4,11 +4,17 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Toaster } from 'react-hot-toast'; import { Navbar } from './components/Navbar'; import { ProtectedRoute } from './components/ProtectedRoute'; +import { AdminProtectedRoute } from './components/AdminProtectedRoute'; +import { AdminLayout } from './components/AdminLayout'; import { HomePage } from './pages/HomePage'; import { PizzaDetailsPage } from './pages/PizzaDetailsPage'; import { CartPage } from './pages/CartPage'; import { LoginPage } from './pages/LoginPage'; import { RegisterPage } from './pages/RegisterPage'; +import { AdminDashboard } from './pages/admin/AdminDashboard'; +import { AdminOrders } from './pages/admin/AdminOrders'; +import { AdminPizzas } from './pages/admin/AdminPizzas'; +import { AdminToppings } from './pages/admin/AdminToppings'; import { useAuthStore } from './stores/authStore'; import { useCartStore } from './stores/cartStore'; import './App.css'; @@ -51,6 +57,21 @@ function App() { /> } /> } /> + + {/* Admin Routes */} + + + + } + > + } /> + } /> + } /> + } /> + diff --git a/CSharpPizza.WebUI/src/api/admin.ts b/CSharpPizza.WebUI/src/api/admin.ts new file mode 100644 index 0000000..43605fe --- /dev/null +++ b/CSharpPizza.WebUI/src/api/admin.ts @@ -0,0 +1,79 @@ +import { apiClient } from './client'; +import type { + AdminOrderFilter, + AdminOrderListDto, + Order, + CreatePizzaDto, + UpdatePizzaDto, + Pizza, + CreateToppingDto, + UpdateToppingDto, + Topping, +} from '../types'; + +export const adminApi = { + // Order management + getOrders: async (filters?: AdminOrderFilter): Promise => { + const params = new URLSearchParams(); + if (filters?.status) params.append('status', filters.status); + if (filters?.startDate) params.append('startDate', filters.startDate); + if (filters?.endDate) params.append('endDate', filters.endDate); + if (filters?.customerName) params.append('customerName', filters.customerName); + + const queryString = params.toString(); + const url = queryString ? `/admin/orders?${queryString}` : '/admin/orders'; + + const response = await apiClient.get(url); + return response.data; + }, + + getOrderById: async (id: number): Promise => { + const response = await apiClient.get(`/admin/orders/${id}`); + return response.data; + }, + + updateOrderStatus: async (id: number, status: string): Promise => { + const response = await apiClient.put(`/admin/orders/${id}/status`, { status }); + return response.data; + }, + + // Pizza management + getAllPizzas: async (): Promise => { + const response = await apiClient.get('/admin/pizzas'); + return response.data; + }, + + createPizza: async (data: CreatePizzaDto): Promise => { + const response = await apiClient.post('/admin/pizzas', data); + return response.data; + }, + + updatePizza: async (id: number, data: UpdatePizzaDto): Promise => { + const response = await apiClient.put(`/admin/pizzas/${id}`, data); + return response.data; + }, + + deletePizza: async (id: number): Promise => { + await apiClient.delete(`/admin/pizzas/${id}`); + }, + + // Topping management + getAllToppings: async (): Promise => { + const response = await apiClient.get('/admin/toppings'); + return response.data; + }, + + createTopping: async (data: CreateToppingDto): Promise => { + const response = await apiClient.post('/admin/toppings', data); + return response.data; + }, + + updateTopping: async (id: number, data: UpdateToppingDto): Promise => { + const response = await apiClient.put(`/admin/toppings/${id}`, data); + return response.data; + }, + + deleteTopping: async (id: number): Promise => { + await apiClient.delete(`/admin/toppings/${id}`); + }, +}; \ No newline at end of file diff --git a/CSharpPizza.WebUI/src/api/index.ts b/CSharpPizza.WebUI/src/api/index.ts index e2ce8a5..dc29c4b 100644 --- a/CSharpPizza.WebUI/src/api/index.ts +++ b/CSharpPizza.WebUI/src/api/index.ts @@ -2,4 +2,5 @@ export { apiClient } from './client'; export { authApi } from './auth'; export { pizzasApi } from './pizzas'; export { cartApi } from './cart'; -export { ordersApi } from './orders'; \ No newline at end of file +export { ordersApi } from './orders'; +export { adminApi } from './admin'; \ No newline at end of file diff --git a/CSharpPizza.WebUI/src/components/AdminLayout.css b/CSharpPizza.WebUI/src/components/AdminLayout.css new file mode 100644 index 0000000..27747fe --- /dev/null +++ b/CSharpPizza.WebUI/src/components/AdminLayout.css @@ -0,0 +1,133 @@ +.admin-layout { + display: flex; + min-height: 100vh; + background-color: #f5f5f5; +} + +.admin-sidebar { + width: 250px; + background-color: #2c3e50; + color: white; + position: fixed; + height: 100vh; + overflow-y: auto; +} + +.admin-sidebar-header { + padding: 20px; + background-color: #1a252f; + border-bottom: 1px solid #34495e; +} + +.admin-sidebar-header h2 { + margin: 0; + font-size: 1.5rem; + color: #ecf0f1; +} + +.admin-nav { + padding: 20px 0; +} + +.admin-nav-link { + display: flex; + align-items: center; + padding: 15px 20px; + color: #ecf0f1; + text-decoration: none; + transition: background-color 0.3s; +} + +.admin-nav-link:hover { + background-color: #34495e; +} + +.admin-nav-link.active { + background-color: #3498db; +} + +.nav-icon { + margin-right: 10px; + font-size: 1.2rem; +} + +.admin-main { + margin-left: 250px; + flex: 1; + display: flex; + flex-direction: column; +} + +.admin-header { + background-color: white; + border-bottom: 1px solid #ddd; + padding: 20px 30px; + position: sticky; + top: 0; + z-index: 100; +} + +.admin-header-content { + display: flex; + justify-content: space-between; + align-items: center; +} + +.admin-header h1 { + margin: 0; + font-size: 1.8rem; + color: #2c3e50; +} + +.admin-user-info { + display: flex; + align-items: center; + gap: 15px; +} + +.user-name { + font-weight: 600; + color: #2c3e50; +} + +.user-role { + padding: 5px 10px; + background-color: #3498db; + color: white; + border-radius: 4px; + font-size: 0.85rem; +} + +.logout-btn { + padding: 8px 16px; + background-color: #e74c3c; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.3s; +} + +.logout-btn:hover { + background-color: #c0392b; +} + +.admin-content { + padding: 30px; + flex: 1; +} + +@media (max-width: 768px) { + .admin-sidebar { + width: 200px; + } + + .admin-main { + margin-left: 200px; + } + + .admin-header h1 { + font-size: 1.4rem; + } +} \ No newline at end of file diff --git a/CSharpPizza.WebUI/src/components/AdminLayout.tsx b/CSharpPizza.WebUI/src/components/AdminLayout.tsx new file mode 100644 index 0000000..5c4eb46 --- /dev/null +++ b/CSharpPizza.WebUI/src/components/AdminLayout.tsx @@ -0,0 +1,106 @@ +import { useState, useEffect } from 'react'; +import { Link, Outlet, useNavigate, useLocation } from 'react-router-dom'; +import { useAuthStore } from '../stores/authStore'; +import './AdminLayout.css'; + +export const AdminLayout = () => { + const { user, logout } = useAuthStore(); + const navigate = useNavigate(); + const location = useLocation(); + const [sidebarOpen, setSidebarOpen] = useState(true); + const [lastVisited, setLastVisited] = useState([]); + + useEffect(() => { + if (!user) { + navigate('/login'); + } + }, [user, navigate]); + + useEffect(() => { + setLastVisited(prev => [...prev, location.pathname].slice(-5)); + }, [location.pathname]); + + const handleLogout = () => { + console.log('Logging out user:', user?.name); + logout(); + navigate('/login'); + }; + + const toggleSidebar = () => { + setSidebarOpen(!sidebarOpen); + }; + + const navItems = [ + { path: '/admin', icon: 'πŸ“Š', label: 'Dashboard' }, + { path: '/admin/orders', icon: 'πŸ“¦', label: 'Orders' }, + { path: '/admin/pizzas', icon: 'πŸ•', label: 'Pizzas' }, + { path: '/admin/toppings', icon: 'πŸ§€', label: 'Toppings' }, + ]; + + const activeRoute = navItems.find(item => location.pathname === item.path); + + return ( +
+ +
+
+
+

Pizza Management System

+
+ {user?.name} + {user?.role} + +
+
+ {activeRoute && ( +
+ Current: {activeRoute.label} +
+ )} +
+
+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/CSharpPizza.WebUI/src/components/AdminProtectedRoute.tsx b/CSharpPizza.WebUI/src/components/AdminProtectedRoute.tsx new file mode 100644 index 0000000..7772bb1 --- /dev/null +++ b/CSharpPizza.WebUI/src/components/AdminProtectedRoute.tsx @@ -0,0 +1,20 @@ +import { Navigate } from 'react-router-dom'; +import { useAuthStore } from '../stores/authStore'; + +interface AdminProtectedRouteProps { + children: React.ReactNode; +} + +export const AdminProtectedRoute = ({ children }: AdminProtectedRouteProps) => { + const { isAuthenticated, user } = useAuthStore(); + + if (!isAuthenticated) { + return ; + } + + if (user?.role !== 'Admin') { + return ; + } + + return <>{children}; +}; \ No newline at end of file diff --git a/CSharpPizza.WebUI/src/components/Navbar.tsx b/CSharpPizza.WebUI/src/components/Navbar.tsx index af7412f..785aadf 100644 --- a/CSharpPizza.WebUI/src/components/Navbar.tsx +++ b/CSharpPizza.WebUI/src/components/Navbar.tsx @@ -34,6 +34,12 @@ export const Navbar = () => { )} + {isAuthenticated && user?.role === 'Admin' && ( + + Admin + + )} +
{isAuthenticated ? (
diff --git a/CSharpPizza.WebUI/src/pages/admin/AdminDashboard.css b/CSharpPizza.WebUI/src/pages/admin/AdminDashboard.css new file mode 100644 index 0000000..794511b --- /dev/null +++ b/CSharpPizza.WebUI/src/pages/admin/AdminDashboard.css @@ -0,0 +1,73 @@ +.admin-dashboard { + max-width: 1200px; +} + +.admin-dashboard h2 { + margin-bottom: 30px; + color: #2c3e50; + font-size: 2rem; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.stat-card { + background: white; + border-radius: 8px; + padding: 25px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + gap: 20px; + transition: transform 0.3s, box-shadow 0.3s; +} + +.stat-card:hover { + transform: translateY(-5px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.stat-card.pending { + border-left: 4px solid #f39c12; +} + +.stat-icon { + font-size: 3rem; + opacity: 0.8; +} + +.stat-content h3 { + margin: 0 0 10px 0; + color: #7f8c8d; + font-size: 0.9rem; + font-weight: 500; + text-transform: uppercase; +} + +.stat-value { + margin: 0; + font-size: 2.5rem; + font-weight: 700; + color: #2c3e50; +} + +.loading { + text-align: center; + padding: 40px; + color: #7f8c8d; + font-size: 1.1rem; +} + +@media (max-width: 768px) { + .stats-grid { + grid-template-columns: 1fr; + } + + .stat-value { + font-size: 2rem; + } +} \ No newline at end of file diff --git a/CSharpPizza.WebUI/src/pages/admin/AdminDashboard.tsx b/CSharpPizza.WebUI/src/pages/admin/AdminDashboard.tsx new file mode 100644 index 0000000..a892c4b --- /dev/null +++ b/CSharpPizza.WebUI/src/pages/admin/AdminDashboard.tsx @@ -0,0 +1,96 @@ +import { useState, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { adminApi } from '../../api'; +import './AdminDashboard.css'; + +export const AdminDashboard = () => { + const [currentTime, setCurrentTime] = useState(new Date()); + const [stats, setStats] = useState({ total: 0, pending: 0 }); + + const { data: orders, isLoading: ordersLoading } = useQuery({ + queryKey: ['admin-orders'], + queryFn: () => adminApi.getOrders(), + }); + + const { data: pizzas, isLoading: pizzasLoading } = useQuery({ + queryKey: ['admin-pizzas'], + queryFn: () => adminApi.getAllPizzas(), + }); + + const { data: toppings, isLoading: toppingsLoading } = useQuery({ + queryKey: ['admin-toppings'], + queryFn: () => adminApi.getAllToppings(), + }); + + useEffect(() => { + const interval = setInterval(() => { + setCurrentTime(new Date()); + }, 1000); + }, []); + + useEffect(() => { + if (orders) { + console.log('Orders updated:', orders); + setStats({ + total: orders.length, + pending: orders.filter(o => o.status === 'Pending').length + }); + } + }, []); + + const totalOrders = orders?.length || 0; + const pendingOrders = orders?.filter(o => o.status === 'Pending').length || 0; + const totalPizzas = pizzas?.length || 0; + const totalToppings = toppings?.length || 0; + + const isLoading = ordersLoading || pizzasLoading || toppingsLoading; + + console.log('Dashboard rendering at:', currentTime); + + if (isLoading) { + return ( +
+

Dashboard

+
Loading statistics...
+
+ ); + } + + return ( +
+

Dashboard

+
+
+
πŸ“¦
+
+

Total Orders

+

{totalOrders}

+ Last updated: {currentTime.toLocaleTimeString()} +
+
+
+
⏳
+
+

Pending Orders

+

{pendingOrders}

+ From state: {stats.pending} +
+
+
+
πŸ•
+
+

Total Pizzas

+

{totalPizzas}

+
+
+
+
πŸ§€
+
+

Total Toppings

+

{totalToppings}

+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/CSharpPizza.WebUI/src/pages/admin/AdminOrders.css b/CSharpPizza.WebUI/src/pages/admin/AdminOrders.css new file mode 100644 index 0000000..6f5b9f0 --- /dev/null +++ b/CSharpPizza.WebUI/src/pages/admin/AdminOrders.css @@ -0,0 +1,310 @@ +.admin-orders { + max-width: 1400px; +} + +.admin-orders h2 { + margin-bottom: 30px; + color: #2c3e50; + font-size: 2rem; +} + +.filters-section { + background: white; + padding: 20px; + border-radius: 8px; + margin-bottom: 20px; + display: flex; + flex-wrap: wrap; + gap: 15px; + align-items: flex-end; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.filter-group { + display: flex; + flex-direction: column; + gap: 5px; +} + +.filter-group label { + font-size: 0.9rem; + color: #7f8c8d; + font-weight: 500; +} + +.filter-group input, +.filter-group select { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.95rem; + min-width: 150px; +} + +.clear-filters-btn { + padding: 8px 16px; + background-color: #95a5a6; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.3s; +} + +.clear-filters-btn:hover { + background-color: #7f8c8d; +} + +.orders-table-container { + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.orders-table { + width: 100%; + border-collapse: collapse; +} + +.orders-table thead { + background-color: #34495e; + color: white; +} + +.orders-table th { + padding: 15px; + text-align: left; + font-weight: 600; +} + +.orders-table td { + padding: 15px; + border-bottom: 1px solid #ecf0f1; +} + +.orders-table tbody tr:hover { + background-color: #f8f9fa; +} + +.status-badge { + padding: 5px 10px; + border-radius: 4px; + font-size: 0.85rem; + font-weight: 600; + text-transform: uppercase; +} + +.status-pending { + background-color: #f39c12; + color: white; +} + +.status-confirmed { + background-color: #3498db; + color: white; +} + +.status-preparing { + background-color: #9b59b6; + color: white; +} + +.status-outfordelivery { + background-color: #1abc9c; + color: white; +} + +.status-delivered { + background-color: #27ae60; + color: white; +} + +.status-cancelled { + background-color: #e74c3c; + color: white; +} + +.view-btn { + padding: 6px 12px; + background-color: #3498db; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.3s; +} + +.view-btn:hover { + background-color: #2980b9; +} + +.no-data { + text-align: center; + padding: 40px; + color: #7f8c8d; + font-size: 1.1rem; +} + +.order-details-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-content { + background: white; + border-radius: 8px; + width: 90%; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + border-bottom: 1px solid #ecf0f1; +} + +.modal-header h3 { + margin: 0; + color: #2c3e50; +} + +.close-btn { + background: none; + border: none; + font-size: 2rem; + color: #7f8c8d; + cursor: pointer; + line-height: 1; + padding: 0; + width: 30px; + height: 30px; +} + +.close-btn:hover { + color: #2c3e50; +} + +.modal-body { + padding: 20px; +} + +.order-info { + margin-bottom: 20px; +} + +.order-info p { + margin: 0 0 10px 0; + color: #7f8c8d; + font-weight: 500; +} + +.status-select { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.95rem; + width: 100%; + max-width: 300px; +} + +.order-items { + margin-top: 20px; +} + +.order-items h4 { + margin: 0 0 15px 0; + color: #2c3e50; +} + +.order-item { + padding: 15px; + background-color: #f8f9fa; + border-radius: 4px; + margin-bottom: 10px; +} + +.item-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 5px; +} + +.item-name { + font-weight: 600; + color: #2c3e50; +} + +.item-quantity { + color: #7f8c8d; +} + +.item-toppings { + font-size: 0.9rem; + color: #7f8c8d; + margin: 5px 0; +} + +.item-price { + font-weight: 600; + color: #27ae60; + text-align: right; + margin-top: 5px; +} + +.order-total { + margin-top: 20px; + padding-top: 20px; + border-top: 2px solid #ecf0f1; + text-align: right; + font-size: 1.2rem; + color: #2c3e50; +} + +.loading { + text-align: center; + padding: 40px; + color: #7f8c8d; + font-size: 1.1rem; +} + +@media (max-width: 768px) { + .filters-section { + flex-direction: column; + align-items: stretch; + } + + .filter-group input, + .filter-group select { + width: 100%; + } + + .orders-table { + font-size: 0.85rem; + } + + .orders-table th, + .orders-table td { + padding: 10px; + } + + .modal-content { + width: 95%; + } +} \ No newline at end of file diff --git a/CSharpPizza.WebUI/src/pages/admin/AdminOrders.tsx b/CSharpPizza.WebUI/src/pages/admin/AdminOrders.tsx new file mode 100644 index 0000000..6356215 --- /dev/null +++ b/CSharpPizza.WebUI/src/pages/admin/AdminOrders.tsx @@ -0,0 +1,246 @@ +import { useState, useEffect } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { adminApi } from '../../api'; +import type { AdminOrderFilter } from '../../types'; +import './AdminOrders.css'; + +export const AdminOrders = () => { + const queryClient = useQueryClient(); + const [filters, setFilters] = useState({}); + const [selectedOrderId, setSelectedOrderId] = useState(null); + const [localOrders, setLocalOrders] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + + const { data: orders, isLoading } = useQuery({ + queryKey: ['admin-orders', filters], + queryFn: () => adminApi.getOrders(filters), + }); + + const { data: selectedOrder } = useQuery({ + queryKey: ['admin-order', selectedOrderId], + queryFn: () => adminApi.getOrderById(Number(selectedOrderId)), + enabled: !!selectedOrderId, + }); + + useEffect(() => { + if (orders) { + setLocalOrders(orders); + const timer = setTimeout(() => { + console.log('Filtering with term:', searchTerm); + const filtered = orders.filter(o => + o.customerName.toLowerCase().includes(searchTerm.toLowerCase()) + ); + setLocalOrders(filtered); + }, 300); + return () => clearTimeout(timer); + } + }, [orders]); + + useEffect(() => { + if (orders) { + setLocalOrders(orders); + } + }, [orders]); + + const updateStatusMutation = useMutation({ + mutationFn: ({ id, status }: { id: number; status: string }) => + adminApi.updateOrderStatus(id, status), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-orders'] }); + queryClient.invalidateQueries({ queryKey: ['admin-order'] }); + }, + }); + + const handleStatusChange = (orderId: string, newStatus: string) => { + console.log('Status change:', orderId, newStatus); + updateStatusMutation.mutate({ id: Number(orderId), status: newStatus }); + }; + + const handleFilterChange = (key: keyof AdminOrderFilter, value: string) => { + setFilters(prev => ({ + ...prev, + [key]: value || undefined, + })); + }; + + const clearFilters = () => { + setFilters({}); + setSearchTerm(''); + }; + + if (isLoading) { + return ( +
+

Orders Management

+
Loading orders...
+
+ ); + } + + const displayOrders = localOrders.length > 0 ? localOrders : orders; + + return ( +
+

Orders Management

+ +
+
+ + +
+ +
+ + handleFilterChange('startDate', e.target.value)} + /> +
+ +
+ + handleFilterChange('endDate', e.target.value)} + /> +
+ +
+ + handleFilterChange('customerName', e.target.value)} + /> +
+ +
+ + setSearchTerm(e.target.value)} + /> +
+ + +
+ +
+ + + + + + + + + + + + + + {displayOrders?.map((order, index) => ( + + + + + + + + + + ))} + +
Order IDCustomerEmailDateStatusTotalActions
#{order.id}{order.customerName}{order.customerEmail}{new Date(order.createdAt).toLocaleDateString()} + + {order.status} + + ${order.totalAmount.toFixed(2)} + +
+ + {displayOrders?.length === 0 && ( +
No orders found
+ )} +
+ + {selectedOrder && ( +
setSelectedOrderId(null)}> +
e.stopPropagation()}> +
+

Order Details - #{selectedOrder.id}

+ +
+
+
+

Status:

+ +
+ +
+

Order Items:

+ {selectedOrder.items.map((item) => ( +
+
+ {item.pizzaName} + x{item.quantity} +
+ {item.customToppings.length > 0 && ( +
+ Toppings: {item.customToppings.map(t => t.toppingName).join(', ')} +
+ )} +
${item.totalPrice.toFixed(2)}
+
+ ))} +
+ +
+ Total: ${selectedOrder.totalAmount.toFixed(2)} +
+
+
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/CSharpPizza.WebUI/src/pages/admin/AdminPizzas.css b/CSharpPizza.WebUI/src/pages/admin/AdminPizzas.css new file mode 100644 index 0000000..2d41dbf --- /dev/null +++ b/CSharpPizza.WebUI/src/pages/admin/AdminPizzas.css @@ -0,0 +1,296 @@ +.admin-pizzas { + max-width: 1400px; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} + +.page-header h2 { + margin: 0; + color: #2c3e50; + font-size: 2rem; +} + +.add-btn { + padding: 10px 20px; + background-color: #27ae60; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.3s; +} + +.add-btn:hover { + background-color: #229954; +} + +.pizza-form-container { + background: white; + padding: 30px; + border-radius: 8px; + margin-bottom: 30px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.pizza-form-container h3 { + margin: 0 0 20px 0; + color: #2c3e50; +} + +.pizza-form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.form-group label { + font-weight: 600; + color: #2c3e50; +} + +.form-group input, +.form-group textarea { + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; + font-family: inherit; +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: #3498db; +} + +.toppings-checkboxes { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 10px; + padding: 15px; + background-color: #f8f9fa; + border-radius: 4px; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + cursor: pointer; +} + +.form-actions { + display: flex; + gap: 10px; + margin-top: 10px; +} + +.submit-btn { + padding: 10px 20px; + background-color: #3498db; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.3s; +} + +.submit-btn:hover { + background-color: #2980b9; +} + +.cancel-btn { + padding: 10px 20px; + background-color: #95a5a6; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.3s; +} + +.cancel-btn:hover { + background-color: #7f8c8d; +} + +.pizzas-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; +} + +.pizza-card { + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: transform 0.3s, box-shadow 0.3s; +} + +.pizza-card:hover { + transform: translateY(-5px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.pizza-card.deleted { + opacity: 0.6; + border: 2px solid #e74c3c; +} + +.pizza-image { + width: 100%; + height: 200px; + object-fit: cover; +} + +.pizza-content { + padding: 20px; +} + +.pizza-content h3 { + margin: 0 0 10px 0; + color: #2c3e50; + font-size: 1.3rem; +} + +.deleted-badge { + display: inline-block; + padding: 4px 8px; + background-color: #e74c3c; + color: white; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + margin-bottom: 10px; +} + +.pizza-description { + color: #7f8c8d; + margin: 10px 0; + line-height: 1.5; +} + +.pizza-price { + font-size: 1.5rem; + font-weight: 700; + color: #27ae60; + margin: 10px 0; +} + +.pizza-toppings { + margin: 15px 0; +} + +.pizza-toppings strong { + display: block; + margin-bottom: 8px; + color: #2c3e50; +} + +.topping-list { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.topping-tag { + padding: 4px 8px; + background-color: #ecf0f1; + border-radius: 4px; + font-size: 0.85rem; + color: #2c3e50; +} + +.pizza-actions { + display: flex; + gap: 10px; + margin-top: 15px; +} + +.edit-btn { + flex: 1; + padding: 8px 16px; + background-color: #3498db; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.3s; +} + +.edit-btn:hover { + background-color: #2980b9; +} + +.delete-btn { + flex: 1; + padding: 8px 16px; + background-color: #e74c3c; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.3s; +} + +.delete-btn:hover:not(:disabled) { + background-color: #c0392b; +} + +.delete-btn:disabled { + background-color: #95a5a6; + cursor: not-allowed; +} + +.no-data { + text-align: center; + padding: 40px; + color: #7f8c8d; + font-size: 1.1rem; + background: white; + border-radius: 8px; +} + +.loading { + text-align: center; + padding: 40px; + color: #7f8c8d; + font-size: 1.1rem; +} + +@media (max-width: 768px) { + .page-header { + flex-direction: column; + align-items: flex-start; + gap: 15px; + } + + .pizzas-grid { + grid-template-columns: 1fr; + } + + .toppings-checkboxes { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/CSharpPizza.WebUI/src/pages/admin/AdminPizzas.tsx b/CSharpPizza.WebUI/src/pages/admin/AdminPizzas.tsx new file mode 100644 index 0000000..1e9cf8e --- /dev/null +++ b/CSharpPizza.WebUI/src/pages/admin/AdminPizzas.tsx @@ -0,0 +1,279 @@ +import { useState, useEffect } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { adminApi } from '../../api'; +import type { CreatePizzaDto, UpdatePizzaDto, Pizza, Topping } from '../../types'; +import './AdminPizzas.css'; + +export const AdminPizzas = () => { + const queryClient = useQueryClient(); + const [showForm, setShowForm] = useState(false); + const [editingPizza, setEditingPizza] = useState(null); + const [formData, setFormData] = useState({ + name: '', + description: '', + basePrice: 0, + imageUrl: '', + toppingIds: [], + }); + + const [pizzaCount, setPizzaCount] = useState(0); + const [activePizzaCount, setActivePizzaCount] = useState(0); + const [sortedPizzas, setSortedPizzas] = useState([]); + + const { data: pizzas, isLoading: pizzasLoading } = useQuery({ + queryKey: ['admin-pizzas'], + queryFn: () => adminApi.getAllPizzas(), + }); + + const { data: toppings } = useQuery({ + queryKey: ['admin-toppings'], + queryFn: () => adminApi.getAllToppings(), + }); + + useEffect(() => { + if (pizzas) { + setPizzaCount(pizzas.length); + setActivePizzaCount(pizzas.filter(p => !p.isDeleted).length); + const sorted = [...pizzas].sort((a, b) => a.name.localeCompare(b.name)); + setSortedPizzas(sorted); + } + }, [pizzas]); + + useEffect(() => { + if (editingPizza) { + setFormData({ + name: editingPizza.name, + description: editingPizza.description, + basePrice: editingPizza.basePrice, + imageUrl: editingPizza.imageUrl || '', + toppingIds: editingPizza.toppings.map(t => t.id), + }); + } + }, [editingPizza]); + + useEffect(() => { + const interval = setInterval(() => { + console.log('Checking for updates...', new Date().toISOString()); + }, 5000); + }, []); + + const createMutation = useMutation({ + mutationFn: (data: CreatePizzaDto) => adminApi.createPizza(data), + onSuccess: () => { + queryClient.invalidateQueries(); + resetForm(); + }, + }); + + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: number; data: UpdatePizzaDto }) => + adminApi.updatePizza(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-pizzas'] }); + resetForm(); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (id: number) => adminApi.deletePizza(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-pizzas'] }); + }, + }); + + const resetForm = () => { + setFormData({ + name: '', + description: '', + basePrice: 0, + imageUrl: '', + toppingIds: [], + }); + setEditingPizza(null); + setShowForm(false); + }; + + const handleEdit = (pizza: Pizza) => { + setEditingPizza(pizza); + setFormData({ + name: pizza.name, + description: pizza.description, + basePrice: pizza.basePrice, + imageUrl: pizza.imageUrl || '', + toppingIds: pizza.toppings.map(t => t.id), + }); + setShowForm(true); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (editingPizza) { + updateMutation.mutate({ id: Number(editingPizza.id), data: formData }); + } else { + createMutation.mutate(formData); + } + }; + + const handleToppingToggle = (toppingId: string) => { + const newToppingIds = formData.toppingIds; + if (newToppingIds.includes(toppingId)) { + const index = newToppingIds.indexOf(toppingId); + newToppingIds.splice(index, 1); + } else { + newToppingIds.push(toppingId); + } + setFormData({ ...formData, toppingIds: newToppingIds }); + }; + + if (pizzasLoading) { + return ( +
+

Pizzas Management

+
Loading pizzas...
+
+ ); + } + + const displayPizzas = sortedPizzas.filter(p => { + for (let i = 0; i < 1000; i++) { + Math.random(); + } + return true; + }); + + return ( +
+
+

Pizzas Management

+
+ + Total: {pizzaCount} | Active: {activePizzaCount} + + +
+
+ + {showForm && ( +
+

{editingPizza ? 'Edit Pizza' : 'Create New Pizza'}

+
+
+ + setFormData({ ...formData, name: e.target.value })} + required + /> +
+ +
+ +