SEARCH-1: separator-insensitive NameSearch + OriginBreeder (Herkunft) filter
- Gerbil.NameSearch (lowercase, strip whitespace/.-_) auto-synced in ApplicationContext .SaveChanges; Gridify-filterable so 'clan kleine chaoten' matches 'Clan-Kleine-Chaoten' (client strips separators the same way). GerbilSearch.Normalize shared helper. - Gerbil.OriginBreeder (free-text Herkunft) on DTO+Input, set by the FEAT-8 import from the source Zucht (imported animals have no OriginContact). Gridify-filterable. - GET /gerbils/breeders: distinct non-empty OriginBreeder values for the Herkunft dropdown. - Migration AddSearchFields (+ NameSearch backfill SQL for existing rows).
This commit is contained in:
@@ -18,6 +18,29 @@ public class ApplicationContext : DbContext
|
|||||||
public DbSet<SaleContract> SaleContracts => Set<SaleContract>();
|
public DbSet<SaleContract> SaleContracts => Set<SaleContract>();
|
||||||
public DbSet<BreederSettings> BreederSettings => Set<BreederSettings>();
|
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)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.Entity<Gerbil>(e =>
|
modelBuilder.Entity<Gerbil>(e =>
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ namespace GerbilManagerWebAPI.Dtos
|
|||||||
string? Genotype,
|
string? Genotype,
|
||||||
string? Notes,
|
string? Notes,
|
||||||
string? ImportSource,
|
string? ImportSource,
|
||||||
string? ExternalRef);
|
string? ExternalRef,
|
||||||
|
string? OriginBreeder);
|
||||||
|
|
||||||
public record LitterDto(
|
public record LitterDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
@@ -69,7 +70,8 @@ namespace GerbilManagerWebAPI.Dtos
|
|||||||
string? Genotype,
|
string? Genotype,
|
||||||
string? Notes,
|
string? Notes,
|
||||||
string? ImportSource,
|
string? ImportSource,
|
||||||
string? ExternalRef);
|
string? ExternalRef,
|
||||||
|
string? OriginBreeder);
|
||||||
|
|
||||||
public record LitterInput(
|
public record LitterInput(
|
||||||
string Name,
|
string Name,
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ namespace GerbilManagerWebAPI.Endpoints
|
|||||||
TypedResults.Ok(await db.Gerbils.AsNoTracking()
|
TypedResults.Ok(await db.Gerbils.AsNoTracking()
|
||||||
.ToPagedResultAsync(query, ToDto)));
|
.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}
|
// GET /gerbils/{id}
|
||||||
group.MapGet("/{id:guid}", async Task<Results<Ok<GerbilDto>, NotFound>> (Guid id, ApplicationContext db) =>
|
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.Notes = i.Notes;
|
||||||
g.ImportSource = i.ImportSource;
|
g.ImportSource = i.ImportSource;
|
||||||
g.ExternalRef = i.ExternalRef;
|
g.ExternalRef = i.ExternalRef;
|
||||||
|
g.OriginBreeder = i.OriginBreeder;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static GerbilDto ToDto(Gerbil g) => new(
|
internal static GerbilDto ToDto(Gerbil g) => new(
|
||||||
g.Id, g.Name, g.Gender, g.Status, g.LitterId, g.OriginContactId, g.ReceiverContactId,
|
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.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),
|
Genotype = ComposeGenotype(a.Genotype),
|
||||||
ImportSource = ImportSourceTag,
|
ImportSource = ImportSourceTag,
|
||||||
ExternalRef = a.Id,
|
ExternalRef = a.Id,
|
||||||
|
OriginBreeder = string.IsNullOrWhiteSpace(a.Zucht) ? null : a.Zucht.Trim(),
|
||||||
RawImportData = JsonSerializer.Serialize(new
|
RawImportData = JsonSerializer.Serialize(new
|
||||||
{
|
{
|
||||||
a.Genotype.RawGenotype,
|
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()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("NameSearch")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("Notes")
|
b.Property<string>("Notes")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("OriginBreeder")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<Guid?>("OriginContactId")
|
b.Property<Guid?>("OriginContactId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
|||||||
@@ -50,5 +50,27 @@ namespace GerbilManagerWebAPI.Models
|
|||||||
/// <summary>Raw import payload preserved verbatim (rawGenotype + unmappedTokens like
|
/// <summary>Raw import payload preserved verbatim (rawGenotype + unmappedTokens like
|
||||||
/// the Uw locus / WFNZ markers) so nothing from the spreadsheets is lost. JSON text.</summary>
|
/// the Uw locus / WFNZ markers) so nothing from the spreadsheets is lost. JSON text.</summary>
|
||||||
public string? RawImportData { get; set; }
|
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