Merge feature/search-1: separator-insensitive NameSearch field + OriginBreeder (Herkunft) filterable string from import + /gerbils/breeders distinct endpoint
This commit is contained in:
@@ -18,6 +18,29 @@ public class ApplicationContext : DbContext
|
||||
public DbSet<SaleContract> SaleContracts => Set<SaleContract>();
|
||||
public DbSet<BreederSettings> BreederSettings => Set<BreederSettings>();
|
||||
|
||||
// Keep Gerbil.NameSearch in sync on every save (separator-insensitive search key),
|
||||
// so it can never drift from Name regardless of which code path mutates the entity.
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
{
|
||||
SyncNameSearch();
|
||||
return base.SaveChanges(acceptAllChangesOnSuccess);
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
|
||||
{
|
||||
SyncNameSearch();
|
||||
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
||||
}
|
||||
|
||||
private void SyncNameSearch()
|
||||
{
|
||||
foreach (var entry in ChangeTracker.Entries<Gerbil>())
|
||||
{
|
||||
if (entry.State is EntityState.Added or EntityState.Modified)
|
||||
entry.Entity.NameSearch = GerbilSearch.Normalize(entry.Entity.Name);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Gerbil>(e =>
|
||||
|
||||
@@ -24,7 +24,8 @@ namespace GerbilManagerWebAPI.Dtos
|
||||
string? Genotype,
|
||||
string? Notes,
|
||||
string? ImportSource,
|
||||
string? ExternalRef);
|
||||
string? ExternalRef,
|
||||
string? OriginBreeder);
|
||||
|
||||
public record LitterDto(
|
||||
Guid Id,
|
||||
@@ -69,7 +70,8 @@ namespace GerbilManagerWebAPI.Dtos
|
||||
string? Genotype,
|
||||
string? Notes,
|
||||
string? ImportSource,
|
||||
string? ExternalRef);
|
||||
string? ExternalRef,
|
||||
string? OriginBreeder);
|
||||
|
||||
public record LitterInput(
|
||||
string Name,
|
||||
|
||||
@@ -19,6 +19,13 @@ namespace GerbilManagerWebAPI.Endpoints
|
||||
TypedResults.Ok(await db.Gerbils.AsNoTracking()
|
||||
.ToPagedResultAsync(query, ToDto)));
|
||||
|
||||
// GET /gerbils/breeders — distinct non-empty Herkunft values for the Tiere filter dropdown
|
||||
group.MapGet("/breeders", async (ApplicationContext db) =>
|
||||
TypedResults.Ok(await db.Gerbils.AsNoTracking()
|
||||
.Where(g => g.OriginBreeder != null && g.OriginBreeder != "")
|
||||
.Select(g => g.OriginBreeder!)
|
||||
.Distinct().OrderBy(b => b).ToListAsync()));
|
||||
|
||||
// GET /gerbils/{id}
|
||||
group.MapGet("/{id:guid}", async Task<Results<Ok<GerbilDto>, NotFound>> (Guid id, ApplicationContext db) =>
|
||||
{
|
||||
@@ -87,11 +94,12 @@ namespace GerbilManagerWebAPI.Endpoints
|
||||
g.Notes = i.Notes;
|
||||
g.ImportSource = i.ImportSource;
|
||||
g.ExternalRef = i.ExternalRef;
|
||||
g.OriginBreeder = i.OriginBreeder;
|
||||
}
|
||||
|
||||
internal static GerbilDto ToDto(Gerbil g) => new(
|
||||
g.Id, g.Name, g.Gender, g.Status, g.LitterId, g.OriginContactId, g.ReceiverContactId,
|
||||
g.EnclosureId, g.ColorVarietyId, g.DateOfBirth, g.DateOfDeath, g.CauseOfDeath,
|
||||
g.GoHomeDate, g.Genotype, g.Notes, g.ImportSource, g.ExternalRef);
|
||||
g.GoHomeDate, g.Genotype, g.Notes, g.ImportSource, g.ExternalRef, g.OriginBreeder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ namespace GerbilManagerWebAPI.Import
|
||||
Genotype = ComposeGenotype(a.Genotype),
|
||||
ImportSource = ImportSourceTag,
|
||||
ExternalRef = a.Id,
|
||||
OriginBreeder = string.IsNullOrWhiteSpace(a.Zucht) ? null : a.Zucht.Trim(),
|
||||
RawImportData = JsonSerializer.Serialize(new
|
||||
{
|
||||
a.Genotype.RawGenotype,
|
||||
|
||||
1036
GerbilManagerWebAPI/Migrations/20260606070015_AddSearchFields.Designer.cs
generated
Normal file
1036
GerbilManagerWebAPI/Migrations/20260606070015_AddSearchFields.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GerbilManagerWebAPI.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSearchFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "NameSearch",
|
||||
table: "Gerbils",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "OriginBreeder",
|
||||
table: "Gerbils",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
// Backfill NameSearch for existing rows (lowercase, strip whitespace/.-_),
|
||||
// matching GerbilSearch.Normalize. New/updated rows stay in sync via
|
||||
// ApplicationContext.SaveChanges.
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE \"Gerbils\" SET \"NameSearch\" = lower(regexp_replace(\"Name\", '[[:space:]._-]', '', 'g'));");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "NameSearch",
|
||||
table: "Gerbils");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OriginBreeder",
|
||||
table: "Gerbils");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -696,9 +696,15 @@ namespace GerbilManagerWebAPI.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NameSearch")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("OriginBreeder")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("OriginContactId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
|
||||
@@ -50,5 +50,27 @@ namespace GerbilManagerWebAPI.Models
|
||||
/// <summary>Raw import payload preserved verbatim (rawGenotype + unmappedTokens like
|
||||
/// the Uw locus / WFNZ markers) so nothing from the spreadsheets is lost. JSON text.</summary>
|
||||
public string? RawImportData { get; set; }
|
||||
|
||||
/// <summary>Breeder/Herkunft (cattery/Zucht) as free text — set by the FEAT-8 import from
|
||||
/// the source Zucht (imported animals have no OriginContact). Gridify-filterable; the
|
||||
/// distinct values back the Tiere "Herkunft" dropdown (GET /gerbils/breeders).</summary>
|
||||
public string? OriginBreeder { get; set; }
|
||||
|
||||
/// <summary>Separator-insensitive search key: Name lowercased with whitespace/.-_ stripped.
|
||||
/// Kept in sync automatically on save (see ApplicationContext.SaveChanges). Gridify-filterable
|
||||
/// so "clan kleine chaoten" matches "Clan-Kleine-Chaoten" (client strips separators too).</summary>
|
||||
public string? NameSearch { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Shared normalisation for the separator-insensitive name search.</summary>
|
||||
public static class GerbilSearch
|
||||
{
|
||||
public static string Normalize(string? name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return "";
|
||||
var chars = name.ToLowerInvariant()
|
||||
.Where(c => !char.IsWhiteSpace(c) && c != '-' && c != '.' && c != '_');
|
||||
return new string(chars.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user