Added endpoints for specific verspakketten, added sorting for All Verspakketten endpoint
This commit is contained in:
2
Lutra/.github/copilot-instructions.md
vendored
2
Lutra/.github/copilot-instructions.md
vendored
@@ -80,7 +80,7 @@ Add FluentValidation, AutoMapper, NUnit, Shouldly, Moq, or Respawn only if the r
|
|||||||
|
|
||||||
## Naming and code style
|
## Naming and code style
|
||||||
|
|
||||||
- Match the existing language of the codebase; this project uses Dutch domain names in places such as `Verspakketten`, `Supermarkt`, and `Beoordeling`.
|
- Use English for general code names (variables, method names, namespaces, folders) and use Dutch only for domain-specific enums, types, and files that reflect business concepts (e.g., `Verspakketten`, `Supermarkt`, `Beoordeling`).
|
||||||
- Keep class and file names consistent with the feature name.
|
- Keep class and file names consistent with the feature name.
|
||||||
- Use sealed types where the project already does.
|
- Use sealed types where the project already does.
|
||||||
- Prefer explicit `required` members where the current codebase uses them.
|
- Prefer explicit `required` members where the current codebase uses them.
|
||||||
|
|||||||
@@ -15,14 +15,40 @@ namespace Lutra.API.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a page of verspakketten.
|
/// Gets a page of verspakketten.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="skip">The number of items to skip.</param>
|
/// <param name="skip">The number of items to skip. Default: 0.</param>
|
||||||
/// <param name="take">The maximum number of items to return.</param>
|
/// <param name="take">The maximum number of items to return. Default: 50.</param>
|
||||||
|
/// <param name="sortField">The field to sort by: Naam, PrijsInCenten, AverageCijferSmaak, or AverageCijferBereiden. Default: Naam.</param>
|
||||||
|
/// <param name="sortDirection">The sort direction: Ascending or Descending. Default: Ascending.</param>
|
||||||
/// <returns>The requested verspakket page.</returns>
|
/// <returns>The requested verspakket page.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(typeof(GetVerspakketten.Response), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(GetVerspakketten.Response), StatusCodes.Status200OK)]
|
||||||
public async Task<GetVerspakketten.Response> Get(int skip = 0, int take = 50)
|
public async Task<GetVerspakketten.Response> Get(
|
||||||
|
int skip = 0,
|
||||||
|
int take = 50,
|
||||||
|
VerspakketSortField sortField = VerspakketSortField.Naam,
|
||||||
|
SortDirection sortDirection = SortDirection.Ascending)
|
||||||
{
|
{
|
||||||
return await mediator.SendQueryAsync<GetVerspakketten.Query, GetVerspakketten.Response>(new GetVerspakketten.Query(skip, take));
|
return await mediator.SendQueryAsync<GetVerspakketten.Query, GetVerspakketten.Response>(
|
||||||
|
new GetVerspakketten.Query(skip, take, sortField, sortDirection));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a specific verspakket by ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The verspakket ID.</param>
|
||||||
|
/// <returns>Returns 200 OK with the verspakket when found, or 404 Not Found when not found.</returns>
|
||||||
|
[HttpGet("{id:guid}")]
|
||||||
|
[ProducesResponseType(typeof(GetVerspakket.Response), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<GetVerspakket.Response?>> GetById(Guid id)
|
||||||
|
{
|
||||||
|
var result = await mediator.SendQueryAsync<GetVerspakket.Query, GetVerspakket.Response?>(new GetVerspakket.Query(id));
|
||||||
|
if (result?.Verspakket == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Cortex.Mediator.Queries;
|
||||||
|
using Lutra.Application.Interfaces;
|
||||||
|
using Lutra.Application.Models.Supermarkten;
|
||||||
|
using Lutra.Application.Models.Verspakketten;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Lutra.Application.Verspakketten
|
||||||
|
{
|
||||||
|
public sealed partial class GetVerspakket
|
||||||
|
{
|
||||||
|
public sealed class Handler(ILutraDbContext context) : IQueryHandler<Query, Response?>
|
||||||
|
{
|
||||||
|
public async Task<Response?> Handle(Query request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var verspakket = await context.Verspaketten
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(v => v.Id == request.Id)
|
||||||
|
.Select(v => new Verspakket
|
||||||
|
{
|
||||||
|
Naam = v.Naam,
|
||||||
|
PrijsInCenten = v.PrijsInCenten,
|
||||||
|
Beoordelingen = v.Beoordelingen
|
||||||
|
.Select(b => new Beoordeling
|
||||||
|
{
|
||||||
|
CijferSmaak = b.CijferSmaak,
|
||||||
|
CijferBereiden = b.CijferBereiden,
|
||||||
|
Aanbevolen = b.Aanbevolen,
|
||||||
|
Tekst = b.Tekst
|
||||||
|
})
|
||||||
|
.ToArray(),
|
||||||
|
Supermarkt = new Supermarkt
|
||||||
|
{
|
||||||
|
Naam = v.Supermarkt.Naam
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.SingleOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
return verspakket is null ? null : new Response { Verspakket = verspakket };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Cortex.Mediator.Queries;
|
||||||
|
|
||||||
|
namespace Lutra.Application.Verspakketten
|
||||||
|
{
|
||||||
|
public sealed partial class GetVerspakket
|
||||||
|
{
|
||||||
|
public record Query(Guid Id) : IQuery<Response?>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Lutra.Application.Models.Verspakketten;
|
||||||
|
|
||||||
|
namespace Lutra.Application.Verspakketten
|
||||||
|
{
|
||||||
|
public sealed partial class GetVerspakket
|
||||||
|
{
|
||||||
|
public sealed class Response
|
||||||
|
{
|
||||||
|
public required Verspakket Verspakket { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
Lutra/Lutra.Application/Verspakketten/GetVerspakket.cs
Normal file
4
Lutra/Lutra.Application/Verspakketten/GetVerspakket.cs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
namespace Lutra.Application.Verspakketten
|
||||||
|
{
|
||||||
|
public sealed partial class GetVerspakket { }
|
||||||
|
}
|
||||||
@@ -12,15 +12,36 @@ namespace Lutra.Application.Verspakketten
|
|||||||
{
|
{
|
||||||
public async Task<Response> Handle(Query request, CancellationToken cancellationToken)
|
public async Task<Response> Handle(Query request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var verspakketten = await context.Verspaketten
|
var query = context.Verspaketten.AsNoTracking();
|
||||||
.AsNoTracking()
|
|
||||||
.OrderBy(v => v.Naam)
|
// Apply sort before pagination so the database handles ordering efficiently.
|
||||||
|
IOrderedQueryable<Domain.Entities.Verspakket> sorted = request.SortField switch
|
||||||
|
{
|
||||||
|
VerspakketSortField.AverageCijferSmaak =>
|
||||||
|
request.SortDirection == SortDirection.Ascending
|
||||||
|
? query.OrderBy(v => v.Beoordelingen.Average(b => (double)b.CijferSmaak))
|
||||||
|
: query.OrderByDescending(v => v.Beoordelingen.Average(b => (double)b.CijferSmaak)),
|
||||||
|
VerspakketSortField.AverageCijferBereiden =>
|
||||||
|
request.SortDirection == SortDirection.Ascending
|
||||||
|
? query.OrderBy(v => v.Beoordelingen.Average(b => (double)b.CijferBereiden))
|
||||||
|
: query.OrderByDescending(v => v.Beoordelingen.Average(b => (double)b.CijferBereiden)),
|
||||||
|
VerspakketSortField.PrijsInCenten =>
|
||||||
|
request.SortDirection == SortDirection.Ascending
|
||||||
|
? query.OrderBy(v => v.PrijsInCenten)
|
||||||
|
: query.OrderByDescending(v => v.PrijsInCenten),
|
||||||
|
VerspakketSortField.Naam or _ =>
|
||||||
|
request.SortDirection == SortDirection.Ascending
|
||||||
|
? query.OrderBy(v => v.Naam)
|
||||||
|
: query.OrderByDescending(v => v.Naam),
|
||||||
|
};
|
||||||
|
|
||||||
|
var verspakketten = await sorted
|
||||||
.Skip(request.Skip)
|
.Skip(request.Skip)
|
||||||
.Take(request.Take)
|
.Take(request.Take)
|
||||||
.Select(v => new Verspakket
|
.Select(v => new Verspakket
|
||||||
{
|
{
|
||||||
Naam = v.Naam,
|
Naam = v.Naam,
|
||||||
PrijsInCenten = null,
|
PrijsInCenten = v.PrijsInCenten,
|
||||||
Beoordelingen = v.Beoordelingen
|
Beoordelingen = v.Beoordelingen
|
||||||
.Select(b => new Beoordeling
|
.Select(b => new Beoordeling
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ namespace Lutra.Application.Verspakketten
|
|||||||
{
|
{
|
||||||
public sealed partial class GetVerspakketten
|
public sealed partial class GetVerspakketten
|
||||||
{
|
{
|
||||||
public record Query(int Skip, int Take) : IQuery<Response>;
|
public record Query(
|
||||||
|
int Skip,
|
||||||
|
int Take,
|
||||||
|
VerspakketSortField SortField = VerspakketSortField.Naam,
|
||||||
|
SortDirection SortDirection = SortDirection.Ascending) : IQuery<Response>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
Lutra/Lutra.Application/Verspakketten/SortDirection.cs
Normal file
7
Lutra/Lutra.Application/Verspakketten/SortDirection.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Lutra.Application.Verspakketten;
|
||||||
|
|
||||||
|
public enum SortDirection
|
||||||
|
{
|
||||||
|
Ascending,
|
||||||
|
Descending
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Lutra.Application.Verspakketten;
|
||||||
|
|
||||||
|
public enum VerspakketSortField
|
||||||
|
{
|
||||||
|
Naam,
|
||||||
|
PrijsInCenten,
|
||||||
|
AverageCijferSmaak,
|
||||||
|
AverageCijferBereiden
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ public class Verspakket : BaseEntity
|
|||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
public required string Naam { get; set; }
|
public required string Naam { get; set; }
|
||||||
|
|
||||||
|
public int? PrijsInCenten { get; set; }
|
||||||
|
|
||||||
[Range(1, 10)]
|
[Range(1, 10)]
|
||||||
public int AantalPersonen { get; set; }
|
public int AantalPersonen { get; set; }
|
||||||
|
|
||||||
|
|||||||
20
Lutra/Lutra.Infrastructure/LutraDbContextFactory.cs
Normal file
20
Lutra/Lutra.Infrastructure/LutraDbContextFactory.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
|
||||||
|
namespace Lutra.Infrastructure.Sql;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Design-time factory used by EF Core tools (dotnet ef migrations) so that
|
||||||
|
/// no startup project or live database connection is required when running migrations.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LutraDbContextFactory : IDesignTimeDbContextFactory<LutraDbContext>
|
||||||
|
{
|
||||||
|
public LutraDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<LutraDbContext>()
|
||||||
|
.UseNpgsql("Host=localhost;Database=lutra;Username=postgres;Password=postgres")
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
return new LutraDbContext(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
154
Lutra/Lutra.Infrastructure/Migrations/20260414193437_AddPrijsInCentenToVerspakket.Designer.cs
generated
Normal file
154
Lutra/Lutra.Infrastructure/Migrations/20260414193437_AddPrijsInCentenToVerspakket.Designer.cs
generated
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Lutra.Infrastructure.Sql;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Lutra.Infrastructure.Sql.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(LutraDbContext))]
|
||||||
|
[Migration("20260414193437_AddPrijsInCentenToVerspakket")]
|
||||||
|
partial class AddPrijsInCentenToVerspakket
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.5")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("Aanbevolen")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("CijferBereiden")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("CijferSmaak")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ModifiedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Tekst")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.Property<Guid>("VerspakketId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("VerspakketId");
|
||||||
|
|
||||||
|
b.ToTable("Beoordelingen", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lutra.Domain.Entities.Supermarkt", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ModifiedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Naam")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Supermarkten");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("AantalPersonen")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ModifiedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Naam")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<int?>("PrijsInCenten")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("SupermarktId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SupermarktId");
|
||||||
|
|
||||||
|
b.ToTable("Verspaketten");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lutra.Domain.Entities.Beoordeling", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lutra.Domain.Entities.Verspakket", null)
|
||||||
|
.WithMany("Beoordelingen")
|
||||||
|
.HasForeignKey("VerspakketId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lutra.Domain.Entities.Supermarkt", "Supermarkt")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SupermarktId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Supermarkt");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lutra.Domain.Entities.Verspakket", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Beoordelingen");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Lutra.Infrastructure.Sql.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPrijsInCentenToVerspakket : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "PrijsInCenten",
|
||||||
|
table: "Verspaketten",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PrijsInCenten",
|
||||||
|
table: "Verspaketten");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,6 +108,9 @@ namespace Lutra.Infrastructure.Sql.Migrations
|
|||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("character varying(50)");
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<int?>("PrijsInCenten")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<Guid>("SupermarktId")
|
b.Property<Guid>("SupermarktId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user