Compare commits
11 Commits
feature/ch
...
feature/st
| Author | SHA1 | Date | |
|---|---|---|---|
| a42966ad8e | |||
| 76e635a122 | |||
| 00886b1c6e | |||
| d5e8946d7d | |||
| e367657254 | |||
| 8c43a8dc03 | |||
| 08732d8348 | |||
| c6edb2e9d0 | |||
| 8e475dff6a | |||
| 4403574f3d | |||
| bcdea8011b |
@@ -9,10 +9,10 @@
|
|||||||
# 2. npm test + npm run build (Frontend)
|
# 2. npm test + npm run build (Frontend)
|
||||||
# 3. Docker-Images bauen und in die Gitea-Registry pushen
|
# 3. Docker-Images bauen und in die Gitea-Registry pushen
|
||||||
#
|
#
|
||||||
# Registry: truenas:13000 (internes Gitea Container Registry)
|
# Registry: 192.168.2.115:13000 (internes Gitea Container Registry)
|
||||||
# Images:
|
# Images:
|
||||||
# truenas:13000/gulum/gerbilmanager-api:latest
|
# 192.168.2.115:13000/gulum/gerbilmanager-api:latest
|
||||||
# truenas:13000/gulum/gerbilmanager-frontend:latest
|
# 192.168.2.115:13000/gulum/gerbilmanager-frontend:latest
|
||||||
|
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: truenas:13000
|
REGISTRY: 192.168.2.115:13000
|
||||||
REGISTRY_OWNER: gulum
|
REGISTRY_OWNER: gulum
|
||||||
DOTNET_VERSION: "10.0.x"
|
DOTNET_VERSION: "10.0.x"
|
||||||
NODE_VERSION: "22"
|
NODE_VERSION: "22"
|
||||||
|
|||||||
276
GerbilManager.Tests/ImportDocxServiceTests.cs
Normal file
276
GerbilManager.Tests/ImportDocxServiceTests.cs
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
using GerbilManagerWebAPI.Import;
|
||||||
|
using GerbilManagerWebAPI.Models;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace GerbilManager.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// FEAT-8d-CREATE / FIX-8D-DEDUP: docx importer CREATE path tests.
|
||||||
|
/// Uses SQLite (not InMemory) so FK + unique-index constraints are enforced —
|
||||||
|
/// this is the only reliable way to catch duplicate-ExternalRef crashes.
|
||||||
|
/// </summary>
|
||||||
|
public class ImportDocxServiceTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _dir;
|
||||||
|
|
||||||
|
public ImportDocxServiceTests()
|
||||||
|
{
|
||||||
|
_dir = Path.Combine(Path.GetTempPath(), "feat8d-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { Directory.Delete(_dir, recursive: true); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// One open connection keeps the :memory: SQLite DB alive across calls.
|
||||||
|
private (ApplicationContext db, SqliteConnection conn) NewSqliteDb()
|
||||||
|
{
|
||||||
|
var conn = new SqliteConnection("DataSource=:memory:");
|
||||||
|
conn.Open();
|
||||||
|
var opts = new DbContextOptionsBuilder<ApplicationContext>().UseSqlite(conn).Options;
|
||||||
|
var db = new ApplicationContext(opts);
|
||||||
|
db.Database.EnsureCreated(); // schema WITH unique index on ExternalRef
|
||||||
|
return (db, conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteLitters(object litters) =>
|
||||||
|
File.WriteAllText(Path.Combine(_dir, "docx_litters.json"),
|
||||||
|
System.Text.Json.JsonSerializer.Serialize(litters));
|
||||||
|
|
||||||
|
private void WriteAnimals(object animals) =>
|
||||||
|
File.WriteAllText(Path.Combine(_dir, "docx_animals.json"),
|
||||||
|
System.Text.Json.JsonSerializer.Serialize(animals));
|
||||||
|
|
||||||
|
// ── Test 1: dry-run shows correct counts without writing ─────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DryRun_counts_new_animal_without_writing()
|
||||||
|
{
|
||||||
|
var (db, conn) = NewSqliteDb();
|
||||||
|
await using (conn)
|
||||||
|
await using (db)
|
||||||
|
{
|
||||||
|
// Litter in DB — date-only lookup (±5 days, exactly one candidate)
|
||||||
|
var litter = new Litter
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Testwurf",
|
||||||
|
Date = new DateOnly(2023, 5, 1),
|
||||||
|
};
|
||||||
|
db.Litters.Add(litter);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
WriteLitters(Array.Empty<object>());
|
||||||
|
WriteAnimals(new[]
|
||||||
|
{
|
||||||
|
new { wsCode = "4/4", litterDob = "01.05.2023", name = "Pepper", gender = "female",
|
||||||
|
owner = "Max Mustermann", abgabeDate = "01.07.2023",
|
||||||
|
deathDate = "", deathCause = "", farbschlag = "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
var report = await new ImportDocxService(db, _dir).RunAsync(execute: false);
|
||||||
|
|
||||||
|
Assert.False(report.Executed);
|
||||||
|
Assert.Equal(1, report.Created);
|
||||||
|
Assert.Equal(1, report.LitterLinked); // date-only match finds the one litter
|
||||||
|
Assert.Equal(1, report.GoHomeFilled);
|
||||||
|
Assert.Equal(0, await db.Gerbils.CountAsync()); // nothing written
|
||||||
|
Assert.Equal(0, await db.Contacts.CountAsync()); // nothing written
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 2: execute creates animal with all fields + FK constraints ───────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_creates_animal_with_litter_contact_and_goHomeDate()
|
||||||
|
{
|
||||||
|
var (db, conn) = NewSqliteDb();
|
||||||
|
await using (conn)
|
||||||
|
await using (db)
|
||||||
|
{
|
||||||
|
var litter = new Litter
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Testwurf",
|
||||||
|
Date = new DateOnly(2023, 5, 1),
|
||||||
|
};
|
||||||
|
db.Litters.Add(litter);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
WriteLitters(Array.Empty<object>());
|
||||||
|
WriteAnimals(new[]
|
||||||
|
{
|
||||||
|
new { wsCode = "4/4", litterDob = "01.05.2023", name = "Pepper", gender = "female",
|
||||||
|
owner = "Max Mustermann", abgabeDate = "01.07.2023",
|
||||||
|
deathDate = "", deathCause = "", farbschlag = "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
var report = await new ImportDocxService(db, _dir).RunAsync(execute: true);
|
||||||
|
|
||||||
|
Assert.True(report.Executed);
|
||||||
|
Assert.Equal(1, report.Created);
|
||||||
|
Assert.Equal(1, report.ContactsCreated);
|
||||||
|
|
||||||
|
var gerbil = await db.Gerbils.SingleAsync();
|
||||||
|
Assert.Equal("Pepper", gerbil.Name);
|
||||||
|
Assert.Equal(new DateOnly(2023, 5, 1), gerbil.DateOfBirth);
|
||||||
|
Assert.Equal(Gender.female, gerbil.Gender);
|
||||||
|
Assert.Equal(GerbilStatus.GivenAway, gerbil.Status);
|
||||||
|
Assert.Equal(litter.Id, gerbil.LitterId); // date-only link worked
|
||||||
|
Assert.Equal(new DateOnly(2023, 7, 1), gerbil.GoHomeDate);
|
||||||
|
Assert.Equal("Zucht der Kleinen Chaoten", gerbil.OriginBreeder);
|
||||||
|
Assert.False(gerbil.IsResident);
|
||||||
|
Assert.Equal("docx", gerbil.ImportSource);
|
||||||
|
Assert.StartsWith("docx-", gerbil.ExternalRef);
|
||||||
|
|
||||||
|
var contact = await db.Contacts.SingleAsync();
|
||||||
|
Assert.Equal("Max Mustermann", contact.Name);
|
||||||
|
Assert.Equal(contact.Id, gerbil.ReceiverContactId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 3: idempotency — second run creates zero ────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_is_idempotent_second_run_creates_zero()
|
||||||
|
{
|
||||||
|
var (db, conn) = NewSqliteDb();
|
||||||
|
await using (conn)
|
||||||
|
await using (db)
|
||||||
|
{
|
||||||
|
WriteLitters(Array.Empty<object>());
|
||||||
|
WriteAnimals(new[]
|
||||||
|
{
|
||||||
|
new { wsCode = "2/3", litterDob = "15.03.2023", name = "Flash", gender = "male",
|
||||||
|
owner = "", abgabeDate = "01.05.2023",
|
||||||
|
deathDate = "", deathCause = "", farbschlag = "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
var first = await new ImportDocxService(db, _dir).RunAsync(execute: true);
|
||||||
|
Assert.Equal(1, first.Created);
|
||||||
|
|
||||||
|
var second = await new ImportDocxService(db, _dir).RunAsync(execute: true);
|
||||||
|
Assert.Equal(0, second.Created); // ExternalRef path, no dupe
|
||||||
|
Assert.Equal(1, await db.Gerbils.CountAsync()); // exactly one row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 4: existing main-import animal → enrich only, no duplicate ──────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_enriches_existing_animal_does_not_duplicate()
|
||||||
|
{
|
||||||
|
var (db, conn) = NewSqliteDb();
|
||||||
|
await using (conn)
|
||||||
|
await using (db)
|
||||||
|
{
|
||||||
|
// Animal already in DB (from main import, ExternalRef ≠ "docx-…")
|
||||||
|
var existing = new Gerbil
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Eddie",
|
||||||
|
DateOfBirth = new DateOnly(2022, 8, 10),
|
||||||
|
Gender = Gender.male,
|
||||||
|
ExternalRef = "main-eddie-xyz", // NOT a "docx-" ref
|
||||||
|
IsResident = false,
|
||||||
|
};
|
||||||
|
db.Gerbils.Add(existing);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
WriteLitters(Array.Empty<object>());
|
||||||
|
WriteAnimals(new[]
|
||||||
|
{
|
||||||
|
// Same name+dob → enrich, not create
|
||||||
|
new { wsCode = "", litterDob = "10.08.2022", name = "Eddie", gender = "male",
|
||||||
|
owner = "Anna Beispiel", abgabeDate = "01.10.2022",
|
||||||
|
deathDate = "", deathCause = "", farbschlag = "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
var report = await new ImportDocxService(db, _dir).RunAsync(execute: true);
|
||||||
|
|
||||||
|
Assert.Equal(0, report.Created);
|
||||||
|
Assert.Equal(1, await db.Gerbils.CountAsync()); // still exactly one
|
||||||
|
|
||||||
|
// GoHomeDate was enriched
|
||||||
|
var updated = await db.Gerbils.SingleAsync();
|
||||||
|
Assert.Equal(new DateOnly(2022, 10, 1), updated.GoHomeDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 5: Deceased status when deathDate set ───────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_sets_Deceased_status_when_deathDate_provided()
|
||||||
|
{
|
||||||
|
var (db, conn) = NewSqliteDb();
|
||||||
|
await using (conn)
|
||||||
|
await using (db)
|
||||||
|
{
|
||||||
|
WriteLitters(Array.Empty<object>());
|
||||||
|
WriteAnimals(new[]
|
||||||
|
{
|
||||||
|
new { wsCode = "1/5", litterDob = "01.01.2022", name = "Ghost", gender = "male",
|
||||||
|
owner = "", abgabeDate = "",
|
||||||
|
deathDate = "15.06.2022", deathCause = "Tumor", farbschlag = "" }
|
||||||
|
});
|
||||||
|
|
||||||
|
await new ImportDocxService(db, _dir).RunAsync(execute: true);
|
||||||
|
|
||||||
|
var gerbil = await db.Gerbils.SingleAsync();
|
||||||
|
Assert.Equal(GerbilStatus.Deceased, gerbil.Status);
|
||||||
|
Assert.Equal(new DateOnly(2022, 6, 15), gerbil.DateOfDeath);
|
||||||
|
Assert.Equal("Tumor", gerbil.CauseOfDeath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 6: P0 REGRESSION — same-name siblings get distinct ExternalRefs ─
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_same_name_siblings_created_with_distinct_ExternalRefs()
|
||||||
|
{
|
||||||
|
// P0 regression: two animals in the same litter with the same normalized name
|
||||||
|
// previously caused duplicate ExternalRef → Npgsql/SQLite 23505 unique-key crash.
|
||||||
|
// Fix: ordinal disambiguation (-2) ensures uniqueness within the batch.
|
||||||
|
// The unique index on IX_Gerbils_ExternalRef (via EnsureCreated on SQLite) makes
|
||||||
|
// this test an authoritative regression gate.
|
||||||
|
var (db, conn) = NewSqliteDb();
|
||||||
|
await using (conn)
|
||||||
|
await using (db)
|
||||||
|
{
|
||||||
|
WriteLitters(Array.Empty<object>());
|
||||||
|
WriteAnimals(new[]
|
||||||
|
{
|
||||||
|
new { wsCode = "4/4", litterDob = "10.06.2022", name = "Mochi", gender = "female",
|
||||||
|
owner = "Eva Müller", abgabeDate = "10.08.2022",
|
||||||
|
deathDate = "", deathCause = "", farbschlag = "" },
|
||||||
|
// Identical name+wsCode+litterDob → base ExternalRef collision
|
||||||
|
new { wsCode = "4/4", litterDob = "10.06.2022", name = "Mochi", gender = "female",
|
||||||
|
owner = "Lena Braun", abgabeDate = "11.08.2022",
|
||||||
|
deathDate = "", deathCause = "", farbschlag = "" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Must NOT throw unique-key violation
|
||||||
|
var report = await new ImportDocxService(db, _dir).RunAsync(execute: true);
|
||||||
|
|
||||||
|
Assert.Equal(2, report.Created);
|
||||||
|
Assert.Equal(2, await db.Gerbils.CountAsync());
|
||||||
|
|
||||||
|
var refs = (await db.Gerbils.Select(g => g.ExternalRef!).ToListAsync()).OrderBy(r => r).ToList();
|
||||||
|
// Both start with the docx- prefix
|
||||||
|
Assert.All(refs, r => Assert.StartsWith("docx-", r));
|
||||||
|
// Must be distinct (unique index enforces this in SQLite)
|
||||||
|
Assert.Equal(2, refs.Distinct().Count());
|
||||||
|
// Second occurrence gets the -2 suffix
|
||||||
|
Assert.Single(refs, r => r.EndsWith("-2"));
|
||||||
|
|
||||||
|
// Idempotent re-run: zero new, still 2 in DB
|
||||||
|
var second = await new ImportDocxService(db, _dir).RunAsync(execute: true);
|
||||||
|
Assert.Equal(0, second.Created);
|
||||||
|
Assert.Equal(2, await db.Gerbils.CountAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -261,6 +261,54 @@ namespace GerbilManager.Tests
|
|||||||
finally { try { Directory.Delete(dir, recursive: true); } catch { } }
|
finally { try { Directory.Delete(dir, recursive: true); } catch { } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_circular_gerbil_litter_dependency_does_not_throw()
|
||||||
|
{
|
||||||
|
// Regression for "Unable to save changes because a circular dependency was detected":
|
||||||
|
// Gerbil [Added] ← FK{MotherId} Litter [Added] ← FK{LitterId} Gerbil [Added].
|
||||||
|
// Triggered when a gerbil is both parent (in one synth litter) and offspring
|
||||||
|
// (LitterId → another synth litter) in the SAME SaveChanges batch — forms a cycle EF
|
||||||
|
// topo-sort cannot resolve. This test encodes the minimal reproducer: A's mother is B,
|
||||||
|
// B's mother is A (artificial genealogical cycle, but triggers the EF cycle reliably).
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "cycle-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
using var conn = new SqliteConnection("DataSource=:memory:");
|
||||||
|
conn.Open();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(Path.Combine(dir, "litters.json"), "[]");
|
||||||
|
File.WriteAllText(Path.Combine(dir, "animals.json"), """
|
||||||
|
[
|
||||||
|
{"id":"alpha","name":"Alpha","dob":"01.01.2020","death":"","farbschlag":"",
|
||||||
|
"genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]},"conflict":false,
|
||||||
|
"parentRefs":[{"name":"Beta","dob":"01.01.2019","roleGuess":"mother","method":"chart-position","confidence":"medium"}]},
|
||||||
|
{"id":"beta","name":"Beta","dob":"01.01.2019","death":"","farbschlag":"",
|
||||||
|
"genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]},"conflict":false,
|
||||||
|
"parentRefs":[{"name":"Alpha","dob":"01.01.2020","roleGuess":"mother","method":"chart-position","confidence":"medium"}]}
|
||||||
|
]
|
||||||
|
""");
|
||||||
|
var opts = new DbContextOptionsBuilder<ApplicationContext>().UseSqlite(conn).Options;
|
||||||
|
using var db = new ApplicationContext(opts);
|
||||||
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
// must NOT throw InvalidOperationException (circular dependency)
|
||||||
|
var report = await new ImportService(db, dir, dir).RunAsync(execute: true);
|
||||||
|
|
||||||
|
Assert.Equal(2, await db.Gerbils.CountAsync());
|
||||||
|
var alpha = await db.Gerbils.SingleAsync(g => g.ExternalRef == "alpha");
|
||||||
|
var beta = await db.Gerbils.SingleAsync(g => g.ExternalRef == "beta");
|
||||||
|
// both should be litter-linked
|
||||||
|
Assert.NotNull(alpha.LitterId);
|
||||||
|
Assert.NotNull(beta.LitterId);
|
||||||
|
// deferred FK update must have set the litter parents correctly
|
||||||
|
var alphaLitter = await db.Litters.SingleAsync(l => l.Id == alpha.LitterId);
|
||||||
|
Assert.Equal(beta.Id, alphaLitter.MotherId);
|
||||||
|
var betaLitter = await db.Litters.SingleAsync(l => l.Id == beta.LitterId);
|
||||||
|
Assert.Equal(alpha.Id, betaLitter.MotherId);
|
||||||
|
}
|
||||||
|
finally { try { Directory.Delete(dir, recursive: true); } catch { } }
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Derived_litter_with_quarantined_parent_leaves_FK_null_no_throw()
|
public async Task Derived_litter_with_quarantined_parent_leaves_FK_null_no_throw()
|
||||||
{
|
{
|
||||||
@@ -573,27 +621,32 @@ namespace GerbilManager.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void SeedGen3g_existing_varieties_preserve_id_name_binding()
|
public void SeedGen3g_existing_varieties_preserve_id_name_binding()
|
||||||
{
|
{
|
||||||
// SEED-HELL bounce regression: the 61 existing entries must NOT change their
|
// CATALOG-RESEED regression: ID->Name bindings for all 66 existing entries must be
|
||||||
// Id->Name binding after GEN-3g. The 5 new entries (IDs 62-66) are appended.
|
// stable. 4 new Dilute Fuchs varieties appended (IDs 67-70). CP-Fuchs (ID 58) and
|
||||||
// A hand-assigned Gerbil.ColorVarietyId pointing to "CP-Fuchs" (ID 58) must
|
// the CP-*-Hell entries (IDs 62-66) must survive with their original names intact.
|
||||||
// still map to CP-Fuchs after the migration runs (append-only, no rename-shift).
|
using var db = NewDb(); // EnsureCreated applies HasData including 70-entry seed
|
||||||
using var db = NewDb(); // EnsureCreated applies HasData including new 66-entry seed
|
|
||||||
|
|
||||||
// ID 58 (index 57 in old catalog, 0-based) = CP-Fuchs — must still be CP-Fuchs
|
// ID 58 = CP-Fuchs — must still be CP-Fuchs (no rename-shift from new entries)
|
||||||
var cpFuchsId = new Guid("00000000-0000-0000-0000-000000000058");
|
var cpFuchsId = new Guid("00000000-0000-0000-0000-000000000058");
|
||||||
var cpFuchs = db.ColorVarieties.Find(cpFuchsId);
|
var cpFuchs = db.ColorVarieties.Find(cpFuchsId);
|
||||||
Assert.NotNull(cpFuchs);
|
Assert.NotNull(cpFuchs);
|
||||||
Assert.Equal("CP-Fuchs", cpFuchs!.Name);
|
Assert.Equal("CP-Fuchs", cpFuchs!.Name);
|
||||||
|
|
||||||
// New entries at IDs 62-66 exist with correct names
|
// Existing CP-*-Hell entries (IDs 62-66) unchanged
|
||||||
Assert.Equal("CP-Agouti-Hell", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000062"))!.Name);
|
Assert.Equal("CP-Agouti-Hell", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000062"))!.Name);
|
||||||
Assert.Equal("CP-Silberagouti-Hell", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000063"))!.Name);
|
Assert.Equal("CP-Silberagouti-Hell", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000063"))!.Name);
|
||||||
Assert.Equal("CP-Algierfuchs-Hell", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000064"))!.Name);
|
Assert.Equal("CP-Algierfuchs-Hell", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000064"))!.Name);
|
||||||
Assert.Equal("CP-Polarfuchs-Hell", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000065"))!.Name);
|
Assert.Equal("CP-Polarfuchs-Hell", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000065"))!.Name);
|
||||||
Assert.Equal("CP-Orangeschimmel-Hell", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000066"))!.Name);
|
Assert.Equal("CP-Orangeschimmel-Hell", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000066"))!.Name);
|
||||||
|
|
||||||
// Total count is exactly 66
|
// 4 new Dilute Fuchs varieties appended (IDs 67-70)
|
||||||
Assert.Equal(66, db.ColorVarieties.Count());
|
Assert.Equal("Dilute Algierfuchs", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000067"))!.Name);
|
||||||
|
Assert.Equal("Dilute Goldfuchs", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000068"))!.Name);
|
||||||
|
Assert.Equal("Dilute Rotfuchs", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000069"))!.Name);
|
||||||
|
Assert.Equal("Dilute Polarfuchs", db.ColorVarieties.Find(new Guid("00000000-0000-0000-0000-000000000070"))!.Name);
|
||||||
|
|
||||||
|
// Total count is exactly 70
|
||||||
|
Assert.Equal(70, db.ColorVarieties.Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|||||||
@@ -282,79 +282,97 @@ public class ApplicationContext : DbContext
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static void SeedColorVarieties(ModelBuilder modelBuilder)
|
private static void SeedColorVarieties(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
// GEN-3g (Kevin): 5 CP-*-Hell variants appended at the end (IDs 62-66).
|
// CATALOG-RESEED (AR-5 / colorVarietySeed.backend.json): 70 entries, frozen symbols.
|
||||||
// Existing 61 entries are UNCHANGED (same ID->Name binding preserved).
|
// ID = array_index + 1 (stable, never changes — live FKs safe).
|
||||||
// SortOrder for new entries is appended; Kevin's frontend catalog handles
|
// SortOrder comes from colorVarietySeed.backend.json (decoupled from ID).
|
||||||
// the interleaved display order via its own sortOrder values.
|
// New entries appended at end (IDs 67-70: Dilute Algierfuchs/Goldfuchs/Rotfuchs/Polarfuchs).
|
||||||
(string Name, string Genotype)[] catalog =
|
// GUARDRAIL: DO NOT reseed from colorVarietySeed.generated.json — it uses bracket display
|
||||||
|
// notation (e[f]/c[chm]) which breaks CR-11 GenotypePotentiallyMatches() (frozen vs brackets).
|
||||||
|
(string Name, string Genotype, int SortOrder)[] catalog =
|
||||||
{
|
{
|
||||||
("Pink Eyed White (PEW)", "AA chch DD EE GG pp spsp rere"),
|
// --- C-locus white / partial albino (IDs 1-3) ---
|
||||||
("Hermelin", "aa chch DD EE GG PP spsp rere"),
|
("Pink Eyed White (PEW)", "AA chch DD EE GG pp spsp rere", 0),
|
||||||
("Himalaya", "AA chch DD EE GG PP spsp rere"),
|
("Hermelin", "aa chch DD EE GG PP spsp rere", 1),
|
||||||
("Zobel", "aa cchmcchm DD EE gg PP spsp rere"),
|
("Himalaya", "AA chch DD EE GG PP spsp rere", 2),
|
||||||
("Rotaugenschimmel", "AA CC DD efef GG pp spsp rere"),
|
// --- Zobel / colourpoint dark (ID 4) ---
|
||||||
("Agouti", "AA CC DD EE GG PP spsp rere"),
|
("Zobel", "aa cchmcchm DD EE gg PP spsp rere", 3),
|
||||||
("Schwarz", "aa CC DD EE GG PP spsp rere"),
|
// --- Schimmel family (IDs 5) ---
|
||||||
("Silberagouti", "AA CC DD EE gg PP spsp rere"),
|
("Rotaugenschimmel", "AA CC DD efef GG pp spsp rere", 4),
|
||||||
("Anthrazit", "aa CC DD EE gg PP spsp rere"),
|
// --- Standard colours (IDs 6-15) ---
|
||||||
("Algierfuchs", "AA CC DD ee GG PP spsp rere"),
|
("Agouti", "AA CC DD EE GG PP spsp rere", 5),
|
||||||
("Blau", "aa CC dd EE GG PP spsp rere"),
|
("Schwarz", "aa CC DD EE GG PP spsp rere", 6),
|
||||||
("Gold", "AA CC DD EE GG pp spsp rere"),
|
("Silberagouti","AA CC DD EE gg PP spsp rere", 7),
|
||||||
("Platin", "aa CC DD EE GG pp spsp rere"),
|
("Anthrazit", "aa CC DD EE gg PP spsp rere", 8),
|
||||||
("Goldfuchs", "AA CC DD ee GG pp spsp rere"),
|
("Algierfuchs", "AA CC DD ee GG PP spsp rere", 9),
|
||||||
("Rotfuchs", "aa CC DD ee GG pp spsp rere"),
|
("Blau", "aa CC dd EE GG PP spsp rere", 10),
|
||||||
("dd Gold", "AA CC dd EE GG pp spsp rere"),
|
("Gold", "AA CC DD EE GG pp spsp rere", 11),
|
||||||
("dd Platin", "aa CC dd EE GG pp spsp rere"),
|
("Platin", "aa CC DD EE GG pp spsp rere", 12),
|
||||||
("Altweiss (REW)", "aa CC DD EE gg pp spsp rere"),
|
("Goldfuchs", "AA CC DD ee GG pp spsp rere", 13),
|
||||||
("Apricot (Blassfuchs)", "AA CC DD ee gg pp spsp rere"),
|
("Rotfuchs", "aa CC DD ee GG pp spsp rere", 14),
|
||||||
("Blaufuchs", "aa CC DD ee gg PP spsp rere"),
|
// --- Dilute (dd) standard (IDs 16-17) ---
|
||||||
("C-Separator", "aa CC DD ee gg pp spsp rere"),
|
("Dilute Gold", "AA CC dd EE GG pp spsp rere", 15),
|
||||||
("Elfenbein", "AA CC DD EE gg pp spsp rere"),
|
("Dilute Platin","aa CC dd EE GG pp spsp rere", 16),
|
||||||
("Kohlfuchs", "aa CC DD ee GG PP spsp rere"),
|
// --- REW / Apricot / misc C-locus (IDs 18-28) ---
|
||||||
("Polarfuchs", "AA CC DD ee gg PP spsp rere"),
|
("Altweiss (REW)", "aa CC DD EE gg pp spsp rere", 17),
|
||||||
("Saphir", "aa CC DD EE GG pp spsp rere"),
|
("Apricot (Blassfuchs)","AA CC DD ee gg pp spsp rere", 18),
|
||||||
("Orangeschimmel", "AA CC DD efef GG PP spsp rere"),
|
("Blaufuchs", "aa CC DD ee gg PP spsp rere", 19),
|
||||||
("Topas", "AA CC DD EE GG pp spsp rere"),
|
("C-Separator", "aa CC DD ee gg pp spsp rere", 20),
|
||||||
("Platin-Hell", "aa CC DD EE GG pp spsp rere"),
|
("Elfenbein", "AA CC DD EE gg pp spsp rere", 21),
|
||||||
("Agouti dd", "AA CC dd EE GG PP spsp rere"),
|
("Kohlfuchs", "aa CC DD ee GG PP spsp rere", 22),
|
||||||
("Silberagouti dd", "AA CC dd EE gg PP spsp rere"),
|
("Polarfuchs", "AA CC DD ee gg PP spsp rere", 23),
|
||||||
("Kohlfuchs dd", "aa CC dd ee GG PP spsp rere"),
|
("Saphir", "aa CC DD EE GG pp spsp rere", 24),
|
||||||
("Anthrazit dd", "aa CC dd EE gg PP spsp rere"),
|
("Orangeschimmel","AA CC DD efef GG PP spsp rere", 25),
|
||||||
("Silberschimmel", "AA CC DD efef gg PP spsp rere"),
|
("Topas", "AA CC DD EE GG pp spsp rere", 26),
|
||||||
("Polarfuchsschimmel", "AA CC DD efef gg PP spsp rere"),
|
("Platin-Hell", "aa CC DD EE GG pp spsp rere", 27),
|
||||||
("Algierfuchsschimmel", "AA CC DD efef GG PP spsp rere"),
|
// --- Dilute (dd) varieties (IDs 29-32) ---
|
||||||
("Kohlfuchsschimmel", "aa CC DD efef GG PP spsp rere"),
|
("Dilute Agouti", "AA CC dd EE GG PP spsp rere", 28),
|
||||||
("Blaufuchsschimmel", "aa CC DD efef gg PP spsp rere"),
|
("Dilute Silberagouti","AA CC dd EE gg PP spsp rere", 29),
|
||||||
("Kohlfuchs, hell", "aa CC DD ee GG PP spsp rere"),
|
("Dilute Kohlfuchs", "aa CC dd ee GG PP spsp rere", 30),
|
||||||
("Goldfuchs, hell", "AA CC DD ee GG pp spsp rere"),
|
("Dilute Anthrazit", "aa CC dd EE gg PP spsp rere", 31),
|
||||||
("Goldfuchsschimmel", "AA CC DD efef GG pp spsp rere"),
|
// --- Schimmel / Fuchsschimmel (IDs 33-37) ---
|
||||||
("Gold-Hell", "AA CC DD EE GG pp spsp rere"),
|
("Silberschimmel", "AA CC DD efef gg PP spsp rere", 36),
|
||||||
("Blaufuchs, hell", "aa CC DD ee gg PP spsp rere"),
|
("Polarfuchsschimmel", "AA CC DD efef gg PP spsp rere", 37),
|
||||||
("Rotfuchsschimmel", "aa CC DD efef GG pp spsp rere"),
|
("Algierfuchsschimmel", "AA CC DD efef GG PP spsp rere", 38),
|
||||||
("Polarfuchs, hell", "AA CC DD ee gg PP spsp rere"),
|
("Kohlfuchsschimmel", "aa CC DD efef GG PP spsp rere", 39),
|
||||||
("Kohlfuchsschimmel, hell", "aa CC DD efef GG PP spsp rere"),
|
("Blaufuchsschimmel", "aa CC DD efef gg PP spsp rere", 40),
|
||||||
("Rotfuchs, hell", "aa CC DD ee GG pp spsp rere"),
|
// --- Hell variants (IDs 38-48) ---
|
||||||
("Kohlfuchs-Hell", "aa CC DD ee GG PP spsp rere"),
|
("Kohlfuchs, hell", "aa CC DD ee GG PP spsp rere", 41),
|
||||||
("Algierfuchs, hell", "AA CC DD ee GG PP spsp rere"),
|
("Goldfuchs, hell", "AA CC DD ee GG pp spsp rere", 42),
|
||||||
("Topas dd", "AA CC dd EE GG pp spsp rere"),
|
("Goldfuchsschimmel", "AA CC DD efef GG pp spsp rere", 43),
|
||||||
("Blaufuchs dd", "aa CC dd ee gg pp spsp rere"),
|
("Gold-Hell", "AA CC DD EE GG pp spsp rere", 44),
|
||||||
("Marder", "aa cchmcchm DD EE GG PP spsp rere"),
|
("Blaufuchs, hell", "aa CC DD ee gg PP spsp rere", 45),
|
||||||
("Siam", "aa cchmch DD EE GG PP spsp rere"),
|
("Rotfuchsschimmel", "aa CC DD efef GG pp spsp rere", 46),
|
||||||
("Zobel-Hell", "aa cchmch DD EE gg PP spsp rere"),
|
("Polarfuchs, hell", "AA CC DD ee gg PP spsp rere", 47),
|
||||||
("CP-Agouti", "AA cchmcchm DD EE GG PP spsp rere"),
|
("Kohlfuchsschimmel, hell","aa CC DD efef GG PP spsp rere", 48),
|
||||||
("CP-Silberagouti", "AA cchmcchm DD EE gg PP spsp rere"),
|
("Rotfuchs, hell", "aa CC DD ee GG pp spsp rere", 49),
|
||||||
("CP-Algierfuchs", "AA cchmcchm DD ee GG PP spsp rere"),
|
("Kohlfuchs-Hell", "aa CC DD ee GG PP spsp rere", 50),
|
||||||
("CP-Polarfuchs", "AA cchmcchm DD ee gg PP spsp rere"),
|
("Algierfuchs, hell", "AA CC DD ee GG PP spsp rere", 51),
|
||||||
("CP-Fuchs", "AA cchmcchm dd ee GG PP spsp rere"),
|
// --- Dilute (dd) renamed variants (IDs 49-50) ---
|
||||||
("CP-Fuchs-Hell", "AA cchmch dd ee GG PP spsp rere"),
|
("Dilute Topas", "AA CC dd EE GG pp spsp rere", 52),
|
||||||
("CP-Blaufuchs", "AA cchmcchm dd ee gg PP spsp rere"),
|
("Dilute Blaufuchs","aa CC dd ee gg pp spsp rere", 53),
|
||||||
("CP-Orangeschimmel", "AA cchmcchm DD efef GG PP spsp rere"),
|
// --- Marder / Siam / CP- series (IDs 51-66) ---
|
||||||
// GEN-3g: 5 new CP-*-Hell variants appended (IDs 62-66, no ID->Name drift)
|
("Marder", "aa cchmcchm DD EE GG PP spsp rere", 54),
|
||||||
("CP-Agouti-Hell", "AA cchmch DD EE GG PP spsp rere"),
|
("Siam", "aa cchmch DD EE GG PP spsp rere", 55),
|
||||||
("CP-Silberagouti-Hell", "AA cchmch DD EE gg PP spsp rere"),
|
("Zobel-Hell","aa cchmch DD EE gg PP spsp rere", 56),
|
||||||
("CP-Algierfuchs-Hell", "AA cchmch DD ee GG PP spsp rere"),
|
("CP-Agouti", "AA cchmcchm DD EE GG PP spsp rere", 57),
|
||||||
("CP-Polarfuchs-Hell", "AA cchmch DD ee gg PP spsp rere"),
|
("CP-Silberagouti","AA cchmcchm DD EE gg PP spsp rere", 59),
|
||||||
("CP-Orangeschimmel-Hell","AA cchmch DD efef GG PP spsp rere"),
|
("CP-Algierfuchs", "AA cchmcchm DD ee GG PP spsp rere", 61),
|
||||||
|
("CP-Polarfuchs", "AA cchmcchm DD ee gg PP spsp rere", 63),
|
||||||
|
("CP-Fuchs", "AA cchmcchm dd ee GG PP spsp rere", 65),
|
||||||
|
("CP-Fuchs-Hell", "AA cchmch dd ee GG PP spsp rere", 66),
|
||||||
|
("CP-Blaufuchs", "AA cchmcchm dd ee gg PP spsp rere", 67),
|
||||||
|
("CP-Orangeschimmel","AA cchmcchm DD efef GG PP spsp rere", 68),
|
||||||
|
// GEN-3g / CATALOG-RESEED: CP-*-Hell interleaved (IDs 62-66, SortOrders from backend.json)
|
||||||
|
("CP-Agouti-Hell", "AA cchmch DD EE GG PP spsp rere", 58),
|
||||||
|
("CP-Silberagouti-Hell", "AA cchmch DD EE gg PP spsp rere", 60),
|
||||||
|
("CP-Algierfuchs-Hell", "AA cchmch DD ee GG PP spsp rere", 62),
|
||||||
|
("CP-Polarfuchs-Hell", "AA cchmch DD ee gg PP spsp rere", 64),
|
||||||
|
("CP-Orangeschimmel-Hell","AA cchmch DD efef GG PP spsp rere", 69),
|
||||||
|
// CATALOG-RESEED: 4 new Dilute (dd) Fuchs varieties appended (IDs 67-70)
|
||||||
|
("Dilute Algierfuchs","AA CC dd ee GG PP spsp rere", 32),
|
||||||
|
("Dilute Goldfuchs", "AA CC dd ee GG pp spsp rere", 33),
|
||||||
|
("Dilute Rotfuchs", "aa CC dd ee GG pp spsp rere", 34),
|
||||||
|
("Dilute Polarfuchs", "AA CC dd ee gg PP spsp rere", 35),
|
||||||
};
|
};
|
||||||
|
|
||||||
var rows = new ColorVariety[catalog.Length];
|
var rows = new ColorVariety[catalog.Length];
|
||||||
@@ -362,11 +380,11 @@ public class ApplicationContext : DbContext
|
|||||||
{
|
{
|
||||||
rows[i] = new ColorVariety
|
rows[i] = new ColorVariety
|
||||||
{
|
{
|
||||||
// Stable, deterministic GUIDs so the HasData seed is migration-stable.
|
// Stable, deterministic GUIDs — ID = array_index + 1 (never reorder existing entries).
|
||||||
Id = new Guid($"00000000-0000-0000-0000-{(i + 1):D12}"),
|
Id = new Guid($"00000000-0000-0000-0000-{(i + 1):D12}"),
|
||||||
Name = catalog[i].Name,
|
Name = catalog[i].Name,
|
||||||
CanonicalGenotype = catalog[i].Genotype,
|
CanonicalGenotype = catalog[i].Genotype,
|
||||||
SortOrder = i,
|
SortOrder = catalog[i].SortOrder,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
modelBuilder.Entity<ColorVariety>().HasData(rows);
|
modelBuilder.Entity<ColorVariety>().HasData(rows);
|
||||||
|
|||||||
@@ -6,16 +6,23 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// FEAT-8d docx loader. Consumes tools/import/output/docx_litters.json +
|
/// FEAT-8d docx loader. Consumes tools/import/output/docx_litters.json +
|
||||||
/// docx_animals.json (produced by extract_docx.py) and enriches the database:
|
/// docx_animals.json (produced by extract_docx.py) and enriches OR CREATES animals.
|
||||||
///
|
///
|
||||||
/// Load policy (IDEMPOTENT NACHZUG after main WIPE+REIMPORT):
|
/// Load policy (IDEMPOTENT NACHZUG after main WIPE+REIMPORT):
|
||||||
/// - Litter link: match docx WS-code to Litters.PairingCode → set Gerbil.LitterId
|
/// Match order per docx row:
|
||||||
/// for animals matched by normalize(name) + litter birth date.
|
/// 1. ExternalRef "docx-…" (previously created by this loader) → enrich (fill-NULL-only)
|
||||||
/// - ReceiverContact: lookup-or-create Contact by owner name → set ReceiverContactId.
|
/// 2. normalize(name)+litterDob in Gerbils (from main import) → enrich (fill-NULL-only)
|
||||||
/// - GoHomeDate, DateOfDeath, CauseOfDeath: fill if currently null (fill-NULL-only).
|
/// 3. No match → CREATE: abgegebene Jungtiere that never appeared in the pedigree charts.
|
||||||
/// - NEVER overwrites a manually-set non-null value.
|
|
||||||
///
|
///
|
||||||
/// Idempotent: running multiple times is safe. Each run resolves whatever is still null.
|
/// Created animals: Status=GivenAway (or Deceased), IsResident=false,
|
||||||
|
/// OriginBreeder='Zucht der Kleinen Chaoten', ImportSource="docx",
|
||||||
|
/// ExternalRef = stable "docx-{ws}-{normname}-{litterDob}[-N]" key (idempotent;
|
||||||
|
/// -N ordinal suffix disambiguates same-name siblings in a litter).
|
||||||
|
///
|
||||||
|
/// NEVER overwrites a manually-set non-null value (fill-NULL-only for all fields).
|
||||||
|
///
|
||||||
|
/// Idempotent: running multiple times is safe. Re-run finds existing rows via ExternalRef.
|
||||||
|
/// Execute wraps all writes in a single transaction (atomic: crash → full rollback).
|
||||||
/// Execute is gated by the endpoint; this service only acts when asked.
|
/// Execute is gated by the endpoint; this service only acts when asked.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ImportDocxService
|
public sealed class ImportDocxService
|
||||||
@@ -48,29 +55,38 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
{
|
{
|
||||||
notes.Add($"Keine Quelldaten in {_sourceDir} (docx_litters.json/docx_animals.json). " +
|
notes.Add($"Keine Quelldaten in {_sourceDir} (docx_litters.json/docx_animals.json). " +
|
||||||
"extract_docx.py zuerst ausführen.");
|
"extract_docx.py zuerst ausführen.");
|
||||||
return new ImportDocxReport(false, 0, 0, 0, 0, 0, 0, notes);
|
return new ImportDocxReport(false, 0, 0, 0, 0, 0, 0, 0, notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build lookup: PairingCode → Litter.Id (WS-code normalised: spaces removed)
|
// Litter lookup by birth date (DayNumber) → list of matching DB litters.
|
||||||
|
// NOTE: WsCode in docx is a litter-size fraction ("4/4", "/5") — NOT a PairingCode.
|
||||||
|
// Date-only lookup with uniqueness guard avoids false links (only link when
|
||||||
|
// exactly one DB litter falls within ±5 days of the docx litter DOB).
|
||||||
var littersInDb = await _db.Litters
|
var littersInDb = await _db.Litters
|
||||||
.Where(l => l.PairingCode != null)
|
.Select(l => new { l.Id, l.Date })
|
||||||
.Select(l => new { l.Id, l.Date, l.PairingCode })
|
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
var litterByWs = littersInDb
|
var littersByDayNumber = littersInDb
|
||||||
.GroupBy(l => l.PairingCode!.Replace(" ", ""))
|
.GroupBy(l => l.Date.DayNumber)
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
// Build animal lookup: normalize(name) + litter_dob → Gerbil (for litter-link)
|
// normalize(name)+litterDob → Gerbil snapshot (main-import enrich path)
|
||||||
var gerbilsInDb = await _db.Gerbils
|
var gerbilsInDb = await _db.Gerbils
|
||||||
.Select(g => new { g.Id, g.Name, g.DateOfBirth, g.LitterId,
|
.Select(g => new { g.Id, g.Name, g.DateOfBirth, g.LitterId,
|
||||||
g.ReceiverContactId, g.GoHomeDate, g.DateOfDeath, g.CauseOfDeath })
|
g.ReceiverContactId, g.GoHomeDate, g.DateOfDeath, g.CauseOfDeath,
|
||||||
|
g.ExternalRef })
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
var gerbilByKey = gerbilsInDb
|
var gerbilByKey = gerbilsInDb
|
||||||
.Where(g => g.DateOfBirth is not null)
|
.Where(g => g.DateOfBirth is not null)
|
||||||
.GroupBy(g => NameDobKey(g.Name, g.DateOfBirth!.Value))
|
.GroupBy(g => NameDobKey(g.Name, g.DateOfBirth!.Value))
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
// Contact lookup: normalized name → existing Contact
|
// ExternalRef → snapshot for previously docx-created animals (idempotency across runs)
|
||||||
|
var docxExternalRefs = gerbilsInDb
|
||||||
|
.Where(g => g.ExternalRef?.StartsWith("docx-") == true)
|
||||||
|
.ToDictionary(g => g.ExternalRef!,
|
||||||
|
g => new { g.Id, g.LitterId, g.GoHomeDate, g.DateOfDeath, g.ReceiverContactId });
|
||||||
|
|
||||||
|
// Contact lookup: normalized name → existing Contact.Id
|
||||||
var contactsInDb = await _db.Contacts
|
var contactsInDb = await _db.Contacts
|
||||||
.Select(c => new { c.Id, c.Name })
|
.Select(c => new { c.Id, c.Name })
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@@ -78,107 +94,215 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
.GroupBy(c => NormalizeName(c.Name))
|
.GroupBy(c => NormalizeName(c.Name))
|
||||||
.ToDictionary(g => g.Key, g => g.First().Id);
|
.ToDictionary(g => g.Key, g => g.First().Id);
|
||||||
|
|
||||||
int litterLinked = 0, goHomeFilled = 0, deathFilled = 0;
|
// ColorVariety lookup: normalized name → Id (for CREATE path Farbschlag matching)
|
||||||
|
var colorVarietyByName = (await _db.ColorVarieties
|
||||||
|
.Select(cv => new { cv.Id, cv.Name })
|
||||||
|
.ToListAsync())
|
||||||
|
.GroupBy(cv => NormalizeName(cv.Name))
|
||||||
|
.ToDictionary(g => g.Key, g => g.First().Id);
|
||||||
|
|
||||||
|
int animalsCreated = 0, litterLinked = 0, goHomeFilled = 0, deathFilled = 0;
|
||||||
int ownerLinked = 0, ownerCreated = 0, skipped = 0;
|
int ownerLinked = 0, ownerCreated = 0, skipped = 0;
|
||||||
|
|
||||||
foreach (var da in docxAnimals)
|
// Ordinal counter for collision-free ExternalRef within this batch.
|
||||||
|
// Two animals with the same base ref (same ws+name+litterDob) get -2, -3 suffixes.
|
||||||
|
var externalRefOrdinals = new Dictionary<string, int>();
|
||||||
|
|
||||||
|
// Belt-and-suspenders: guard against adding the same ExternalRef twice in one run.
|
||||||
|
var batchRefs = new HashSet<string>();
|
||||||
|
|
||||||
|
// --- Planning pass (dry-run counts + execute writes) ---
|
||||||
|
// Execute path is wrapped in a single transaction for atomicity.
|
||||||
|
Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction? tx = null;
|
||||||
|
if (execute)
|
||||||
|
tx = await _db.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(da.Name)) { skipped++; continue; }
|
foreach (var da in docxAnimals)
|
||||||
|
|
||||||
// Resolve the litter by WS-code + approximate birth date
|
|
||||||
Guid? litterId = null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(da.WsCode) && !string.IsNullOrWhiteSpace(da.LitterDob))
|
|
||||||
{
|
{
|
||||||
var litterDob = ParseDate(da.LitterDob);
|
if (string.IsNullOrWhiteSpace(da.Name)) { skipped++; continue; }
|
||||||
if (litterDob is not null && litterByWs.TryGetValue(da.WsCode.Replace(" ", ""), out var cands))
|
|
||||||
|
// Collision-free ExternalRef: ordinal suffix for same-name siblings
|
||||||
|
var baseRef = DocxExternalRefBase(da);
|
||||||
|
externalRefOrdinals.TryGetValue(baseRef, out var ord);
|
||||||
|
ord++;
|
||||||
|
externalRefOrdinals[baseRef] = ord;
|
||||||
|
var externalRef = ord == 1 ? baseRef : $"{baseRef}-{ord}";
|
||||||
|
|
||||||
|
// Resolve litter: date ±5 days, unambiguous (exactly one candidate)
|
||||||
|
Guid? litterId = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(da.LitterDob))
|
||||||
{
|
{
|
||||||
// Pick the litter whose date matches (within ±5 days for rounding)
|
var litterDob = ParseDate(da.LitterDob);
|
||||||
var match = cands.FirstOrDefault(l =>
|
if (litterDob is not null)
|
||||||
Math.Abs((l.Date.DayNumber - litterDob.Value.DayNumber)) <= 5);
|
|
||||||
litterId = match?.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the gerbil by name + litter birth date
|
|
||||||
var animalDob = litterId is not null
|
|
||||||
? (await _db.Litters.Where(l => l.Id == litterId).Select(l => (DateOnly?)l.Date).FirstOrDefaultAsync())
|
|
||||||
: ParseDate(da.LitterDob);
|
|
||||||
|
|
||||||
if (animalDob is null) { skipped++; continue; }
|
|
||||||
|
|
||||||
var key = NameDobKey(da.Name, animalDob.Value);
|
|
||||||
if (!gerbilByKey.TryGetValue(key, out var gerbilCands)) { skipped++; continue; }
|
|
||||||
|
|
||||||
// If multiple gerbils match (same name+dob), take the one without a litter link first
|
|
||||||
var gerbilSnap = gerbilCands.FirstOrDefault(g => g.LitterId == null)
|
|
||||||
?? gerbilCands.First();
|
|
||||||
|
|
||||||
// Resolve receiver contact (lookup-or-create)
|
|
||||||
Guid? receiverId = null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(da.Owner))
|
|
||||||
{
|
|
||||||
var normOwner = NormalizeName(da.Owner);
|
|
||||||
if (contactByNorm.TryGetValue(normOwner, out var existingId))
|
|
||||||
{
|
|
||||||
receiverId = existingId;
|
|
||||||
ownerLinked++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ownerCreated++;
|
|
||||||
if (execute)
|
|
||||||
{
|
{
|
||||||
var newContact = new Contact { Id = Guid.NewGuid(), Name = da.Owner.Trim() };
|
var candidates = new List<Guid>();
|
||||||
_db.Contacts.Add(newContact);
|
for (int delta = -5; delta <= 5; delta++)
|
||||||
await _db.SaveChangesAsync();
|
{
|
||||||
receiverId = newContact.Id;
|
if (littersByDayNumber.TryGetValue(litterDob.Value.DayNumber + delta, out var cl))
|
||||||
contactByNorm[normOwner] = receiverId.Value;
|
candidates.AddRange(cl.Select(l => l.Id));
|
||||||
|
}
|
||||||
|
if (candidates.Count == 1)
|
||||||
|
litterId = candidates[0];
|
||||||
|
// If 0 or >1 candidates: no link (avoid false links)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var goHomeDate = ParseDate(da.AbgabeDate);
|
// Animal DOB = litter birth date (docx has no per-animal DOB)
|
||||||
var deathDate = ParseDate(da.DeathDate);
|
var animalDob = litterId is not null
|
||||||
|
? (await _db.Litters.Where(l => l.Id == litterId).Select(l => (DateOnly?)l.Date).FirstOrDefaultAsync())
|
||||||
|
: ParseDate(da.LitterDob);
|
||||||
|
|
||||||
// Count what will change
|
if (animalDob is null) { skipped++; continue; }
|
||||||
bool willLinkLitter = litterId is not null && gerbilSnap.LitterId is null;
|
|
||||||
bool willFillGoHome = goHomeDate is not null && gerbilSnap.GoHomeDate is null;
|
|
||||||
bool willFillDeath = deathDate is not null && gerbilSnap.DateOfDeath is null;
|
|
||||||
|
|
||||||
if (willLinkLitter) litterLinked++;
|
var goHomeDate = ParseDate(da.AbgabeDate);
|
||||||
if (willFillGoHome) goHomeFilled++;
|
var deathDate = ParseDate(da.DeathDate);
|
||||||
if (willFillDeath) deathFilled++;
|
|
||||||
|
|
||||||
if (execute)
|
// Resolve receiver contact (lookup-or-create; shared by all paths)
|
||||||
{
|
Guid? receiverId = null;
|
||||||
var row = await _db.Gerbils.FirstOrDefaultAsync(g => g.Id == gerbilSnap.Id);
|
if (!string.IsNullOrWhiteSpace(da.Owner))
|
||||||
if (row is null) continue;
|
|
||||||
|
|
||||||
if (willLinkLitter) row.LitterId = litterId;
|
|
||||||
if (receiverId is not null && row.ReceiverContactId is null)
|
|
||||||
row.ReceiverContactId = receiverId;
|
|
||||||
if (willFillGoHome) row.GoHomeDate = goHomeDate;
|
|
||||||
if (willFillDeath)
|
|
||||||
{
|
{
|
||||||
row.DateOfDeath = deathDate;
|
var normOwner = NormalizeName(da.Owner);
|
||||||
if (!string.IsNullOrWhiteSpace(da.DeathCause) && row.CauseOfDeath is null)
|
if (contactByNorm.TryGetValue(normOwner, out var existingContactId))
|
||||||
row.CauseOfDeath = da.DeathCause.Trim();
|
{
|
||||||
|
receiverId = existingContactId;
|
||||||
|
ownerLinked++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ownerCreated++;
|
||||||
|
if (execute)
|
||||||
|
{
|
||||||
|
var newContact = new Contact { Id = Guid.NewGuid(), Name = da.Owner.Trim() };
|
||||||
|
_db.Contacts.Add(newContact);
|
||||||
|
await _db.SaveChangesAsync(); // flush within the outer tx
|
||||||
|
receiverId = newContact.Id;
|
||||||
|
contactByNorm[normOwner] = receiverId.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PATH 1: previously docx-created animal (idempotent re-run) ──────────
|
||||||
|
if (docxExternalRefs.TryGetValue(externalRef, out var prevSnap))
|
||||||
|
{
|
||||||
|
bool willLink = litterId is not null && prevSnap.LitterId is null;
|
||||||
|
bool willHome = goHomeDate is not null && prevSnap.GoHomeDate is null;
|
||||||
|
bool willDeath = deathDate is not null && prevSnap.DateOfDeath is null;
|
||||||
|
|
||||||
|
if (willLink) litterLinked++;
|
||||||
|
if (willHome) goHomeFilled++;
|
||||||
|
if (willDeath) deathFilled++;
|
||||||
|
|
||||||
|
if (execute)
|
||||||
|
{
|
||||||
|
var row = await _db.Gerbils.FirstOrDefaultAsync(g => g.Id == prevSnap.Id);
|
||||||
|
if (row is null) continue;
|
||||||
|
if (willLink) row.LitterId = litterId;
|
||||||
|
if (receiverId is not null && row.ReceiverContactId is null) row.ReceiverContactId = receiverId;
|
||||||
|
if (willHome) row.GoHomeDate = goHomeDate;
|
||||||
|
if (willDeath)
|
||||||
|
{
|
||||||
|
row.DateOfDeath = deathDate;
|
||||||
|
if (!string.IsNullOrWhiteSpace(da.DeathCause) && row.CauseOfDeath is null)
|
||||||
|
row.CauseOfDeath = da.DeathCause.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PATH 2: main-import animal matched by name+dob ───────────────────────
|
||||||
|
var nameKey = NameDobKey(da.Name, animalDob.Value);
|
||||||
|
if (gerbilByKey.TryGetValue(nameKey, out var gerbilCands))
|
||||||
|
{
|
||||||
|
var gerbilSnap = gerbilCands.FirstOrDefault(g => g.LitterId == null)
|
||||||
|
?? gerbilCands.First();
|
||||||
|
|
||||||
|
bool willLink = litterId is not null && gerbilSnap.LitterId is null;
|
||||||
|
bool willHome = goHomeDate is not null && gerbilSnap.GoHomeDate is null;
|
||||||
|
bool willDeath = deathDate is not null && gerbilSnap.DateOfDeath is null;
|
||||||
|
|
||||||
|
if (willLink) litterLinked++;
|
||||||
|
if (willHome) goHomeFilled++;
|
||||||
|
if (willDeath) deathFilled++;
|
||||||
|
|
||||||
|
if (execute)
|
||||||
|
{
|
||||||
|
var row = await _db.Gerbils.FirstOrDefaultAsync(g => g.Id == gerbilSnap.Id);
|
||||||
|
if (row is null) continue;
|
||||||
|
if (willLink) row.LitterId = litterId;
|
||||||
|
if (receiverId is not null && row.ReceiverContactId is null) row.ReceiverContactId = receiverId;
|
||||||
|
if (willHome) row.GoHomeDate = goHomeDate;
|
||||||
|
if (willDeath)
|
||||||
|
{
|
||||||
|
row.DateOfDeath = deathDate;
|
||||||
|
if (!string.IsNullOrWhiteSpace(da.DeathCause) && row.CauseOfDeath is null)
|
||||||
|
row.CauseOfDeath = da.DeathCause.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PATH 3: no match → CREATE ────────────────────────────────────────────
|
||||||
|
// Belt-and-suspenders: ordinal should ensure uniqueness, but guard anyway
|
||||||
|
if (!batchRefs.Add(externalRef)) { skipped++; continue; }
|
||||||
|
|
||||||
|
animalsCreated++;
|
||||||
|
if (litterId is not null) litterLinked++;
|
||||||
|
if (goHomeDate is not null) goHomeFilled++;
|
||||||
|
if (deathDate is not null) deathFilled++;
|
||||||
|
|
||||||
|
if (execute)
|
||||||
|
{
|
||||||
|
colorVarietyByName.TryGetValue(NormalizeName(da.Farbschlag ?? ""), out var cvId);
|
||||||
|
|
||||||
|
_db.Gerbils.Add(new Gerbil
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = da.Name.Trim(),
|
||||||
|
DateOfBirth = animalDob,
|
||||||
|
Gender = ParseGender(da.Gender),
|
||||||
|
Status = deathDate is not null ? GerbilStatus.Deceased : GerbilStatus.GivenAway,
|
||||||
|
LitterId = litterId,
|
||||||
|
ReceiverContactId = receiverId,
|
||||||
|
GoHomeDate = goHomeDate,
|
||||||
|
DateOfDeath = deathDate,
|
||||||
|
CauseOfDeath = string.IsNullOrWhiteSpace(da.DeathCause) ? null : da.DeathCause.Trim(),
|
||||||
|
ColorVarietyId = cvId == default ? null : cvId,
|
||||||
|
OriginBreeder = "Zucht der Kleinen Chaoten",
|
||||||
|
IsResident = false,
|
||||||
|
ImportSource = "docx",
|
||||||
|
ExternalRef = externalRef,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flush all gerbil inserts + enrich updates in one shot (within the tx)
|
||||||
|
if (execute && (animalsCreated + litterLinked + goHomeFilled + deathFilled + ownerCreated) > 0)
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
if (tx is not null) await tx.CommitAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// tx.DisposeAsync (in finally) rolls back if not committed
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (tx is not null) await tx.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (execute && (litterLinked + goHomeFilled + deathFilled + ownerLinked + ownerCreated) > 0)
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
|
|
||||||
notes.Add($"Quelle: {docxLitters.Count} Würfe, {docxAnimals.Count} Tier-Zeilen aus der docx.");
|
notes.Add($"Quelle: {docxLitters.Count} Würfe, {docxAnimals.Count} Tier-Zeilen aus der docx.");
|
||||||
notes.Add($"Litter-Links: {litterLinked} Tiere einem Wurf zugeordnet (WS-Code → PairingCode).");
|
notes.Add($"Neu angelegt: {animalsCreated} Jungtiere (abgegeben, nicht in Stammbäumen).");
|
||||||
|
notes.Add($"Litter-Links: {litterLinked} Tiere einem Wurf zugeordnet (DOB-Match ±5 Tage, eindeutig).");
|
||||||
notes.Add($"Abnehmer: {ownerLinked} bestehende Kontakte verknüpft, {ownerCreated} neue Kontakte angelegt.");
|
notes.Add($"Abnehmer: {ownerLinked} bestehende Kontakte verknüpft, {ownerCreated} neue Kontakte angelegt.");
|
||||||
notes.Add($"GoHomeDate: {goHomeFilled} Abgabe-Daten nachgetragen.");
|
notes.Add($"GoHomeDate: {goHomeFilled} Abgabe-Daten nachgetragen.");
|
||||||
notes.Add($"Tod-Datum: {deathFilled} Todesdaten nachgetragen.");
|
notes.Add($"Tod-Datum: {deathFilled} Todesdaten nachgetragen.");
|
||||||
notes.Add($"Übersprungen: {skipped} Zeilen (kein Name oder kein DB-Match).");
|
notes.Add($"Übersprungen: {skipped} Zeilen (kein Name oder kein Datum).");
|
||||||
if (!execute) notes.Add("DRY-RUN: nichts gespeichert. /import/docx/execute schreibt die Änderungen.");
|
if (!execute) notes.Add("DRY-RUN: nichts gespeichert. /import/docx/execute schreibt die Änderungen.");
|
||||||
|
|
||||||
return new ImportDocxReport(execute, litterLinked, ownerLinked + ownerCreated,
|
return new ImportDocxReport(execute, animalsCreated, litterLinked, ownerLinked + ownerCreated,
|
||||||
goHomeFilled, deathFilled, ownerCreated, skipped, notes);
|
goHomeFilled, deathFilled, ownerCreated, skipped, notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +339,27 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
n = System.Text.RegularExpressions.Regex.Replace(n, @"\s+", " ").Trim();
|
n = System.Text.RegularExpressions.Regex.Replace(n, @"\s+", " ").Trim();
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base ExternalRef key (before ordinal disambiguation). Two animals sharing the same
|
||||||
|
/// ws+name+litterDob get this same base; the caller appends -2, -3 … for duplicates.
|
||||||
|
/// </summary>
|
||||||
|
internal static string DocxExternalRefBase(DocxAnimal da)
|
||||||
|
{
|
||||||
|
var ws = (da.WsCode ?? "").Replace(" ", "").ToLowerInvariant();
|
||||||
|
var name = System.Text.RegularExpressions.Regex.Replace(
|
||||||
|
(da.Name ?? "").ToLowerInvariant(), @"[^a-z0-9äöüß]", "");
|
||||||
|
return $"docx-{ws}-{name}-{da.LitterDob}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Gender ParseGender(string? s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(s)) return Gender.unknown;
|
||||||
|
var lower = s.ToLowerInvariant();
|
||||||
|
if (lower == "male" || lower == "m" || lower.Contains("männ")) return Gender.male;
|
||||||
|
if (lower == "female" || lower == "f" || lower == "w" || lower.Contains("weibl")) return Gender.female;
|
||||||
|
return Gender.unknown;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Source shapes (from extract_docx.py output) ----
|
// ---- Source shapes (from extract_docx.py output) ----
|
||||||
@@ -249,6 +394,7 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
|
|
||||||
public sealed record ImportDocxReport(
|
public sealed record ImportDocxReport(
|
||||||
bool Executed,
|
bool Executed,
|
||||||
|
int Created,
|
||||||
int LitterLinked,
|
int LitterLinked,
|
||||||
int OwnerLinked,
|
int OwnerLinked,
|
||||||
int GoHomeFilled,
|
int GoHomeFilled,
|
||||||
|
|||||||
@@ -289,10 +289,15 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
litterParents[lid] = (f, m);
|
litterParents[lid] = (f, m);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PASS 2: stage synthesized litters (parents already guarded above). DO NOT save them
|
// FIX-IMPORT-CYCLE: track deferred synth litter parent FKs (populated in PASS 2 below).
|
||||||
// before the animals — the offspring AND the parent gerbils are created in the loop
|
// Synth litters are added with null FatherId/MotherId to break the Gerbil↔Litter cycle;
|
||||||
// below, so a single SaveChanges at the end lets EF order parents→litters→offspring
|
// the actual FKs are applied AFTER SaveChanges once all gerbils are persisted.
|
||||||
// (all FKs are nullable). Saving litters first is exactly what caused the FK fault.
|
var synthLitterPendingParents = new Dictionary<Guid, (Guid? Father, Guid? Mother)>();
|
||||||
|
|
||||||
|
// PASS 2: stage synthesized litters (parents already guarded above). Litters are added
|
||||||
|
// with FatherId/MotherId = null (deferred) so that the SaveChanges below has only a
|
||||||
|
// one-directional Gerbil→Litter dependency — no Litter→Gerbil FKs in the same batch,
|
||||||
|
// which would cause EF's topo-sort to throw "circular dependency detected".
|
||||||
if (execute)
|
if (execute)
|
||||||
{
|
{
|
||||||
// reuse an existing litter with the same parents+date instead of duplicating.
|
// reuse an existing litter with the same parents+date instead of duplicating.
|
||||||
@@ -316,13 +321,17 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
derivedLitters--;
|
derivedLitters--;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Defer FatherId/MotherId: both parent gerbils and offspring gerbils may be [Added]
|
||||||
|
// in this same batch. Setting them now causes EF circular dependency
|
||||||
|
// (Gerbil[Added] ← Litter.MotherId [Added] ← Gerbil.LitterId [Added]).
|
||||||
|
synthLitterPendingParents[sl.Id] = (sl.Father, sl.Mother);
|
||||||
_db.Litters.Add(new Litter
|
_db.Litters.Add(new Litter
|
||||||
{
|
{
|
||||||
Id = sl.Id,
|
Id = sl.Id,
|
||||||
Name = $"Wurf (aus Diagramm) {sl.Date:yyyy-MM-dd}".Trim(),
|
Name = $"Wurf (aus Diagramm) {sl.Date:yyyy-MM-dd}".Trim(),
|
||||||
Date = sl.Date ?? default,
|
Date = sl.Date ?? default,
|
||||||
FatherId = sl.Father,
|
FatherId = null, // deferred — applied after gerbils SaveChanges
|
||||||
MotherId = sl.Mother,
|
MotherId = null, // deferred — applied after gerbils SaveChanges
|
||||||
Notes = $"aus Stammbaum-Diagramm abgeleitet (Konfidenz: {sl.Confidence})",
|
Notes = $"aus Stammbaum-Diagramm abgeleitet (Konfidenz: {sl.Confidence})",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -437,6 +446,20 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
}
|
}
|
||||||
if (execute) await _db.SaveChangesAsync();
|
if (execute) await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Apply deferred synth litter parent FKs — all new gerbils are now persisted in the DB,
|
||||||
|
// so no cycle. FK guard already applied above (persisted set); values in the dict are safe.
|
||||||
|
if (execute && synthLitterPendingParents.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var (litId, (f, m)) in synthLitterPendingParents)
|
||||||
|
{
|
||||||
|
var row = await _db.Litters.FindAsync(litId);
|
||||||
|
if (row is null) continue;
|
||||||
|
if (f is not null) row.FatherId = f;
|
||||||
|
if (m is not null) row.MotherId = m;
|
||||||
|
}
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
// ---- back-link Wurfchronik litter parents by name (best effort) ----
|
// ---- back-link Wurfchronik litter parents by name (best effort) ----
|
||||||
if (execute)
|
if (execute)
|
||||||
{
|
{
|
||||||
|
|||||||
1430
GerbilManagerWebAPI/Migrations/20260606202850_ReseedColorVarietiesAR5.Designer.cs
generated
Normal file
1430
GerbilManagerWebAPI/Migrations/20260606202850_ReseedColorVarietiesAR5.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,598 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
|
||||||
|
namespace GerbilManagerWebAPI.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ReseedColorVarietiesAR5 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000016"),
|
||||||
|
column: "Name",
|
||||||
|
value: "Dilute Gold");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000017"),
|
||||||
|
column: "Name",
|
||||||
|
value: "Dilute Platin");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000029"),
|
||||||
|
column: "Name",
|
||||||
|
value: "Dilute Agouti");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000030"),
|
||||||
|
column: "Name",
|
||||||
|
value: "Dilute Silberagouti");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000031"),
|
||||||
|
column: "Name",
|
||||||
|
value: "Dilute Kohlfuchs");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000032"),
|
||||||
|
column: "Name",
|
||||||
|
value: "Dilute Anthrazit");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000033"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 36);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000034"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 37);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000035"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 38);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000036"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 39);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000037"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 40);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000038"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 41);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000039"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 42);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000040"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 43);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000041"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 44);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000042"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 45);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000043"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 46);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000044"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 47);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000045"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 48);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000046"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 49);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000047"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 50);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000048"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 51);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000049"),
|
||||||
|
columns: new[] { "Name", "SortOrder" },
|
||||||
|
values: new object[] { "Dilute Topas", 52 });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000050"),
|
||||||
|
columns: new[] { "Name", "SortOrder" },
|
||||||
|
values: new object[] { "Dilute Blaufuchs", 53 });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000051"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 54);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000052"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 55);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000053"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 56);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000054"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 57);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000055"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 59);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000056"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 61);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000057"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 63);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000058"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 65);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000059"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 66);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000060"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 67);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000061"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 68);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000062"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 58);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000063"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 60);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000064"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 62);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000066"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 69);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
columns: new[] { "Id", "CanonicalGenotype", "Name", "SortOrder" },
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ new Guid("00000000-0000-0000-0000-000000000067"), "AA CC dd ee GG PP spsp rere", "Dilute Algierfuchs", 32 },
|
||||||
|
{ new Guid("00000000-0000-0000-0000-000000000068"), "AA CC dd ee GG pp spsp rere", "Dilute Goldfuchs", 33 },
|
||||||
|
{ new Guid("00000000-0000-0000-0000-000000000069"), "aa CC dd ee GG pp spsp rere", "Dilute Rotfuchs", 34 },
|
||||||
|
{ new Guid("00000000-0000-0000-0000-000000000070"), "AA CC dd ee gg PP spsp rere", "Dilute Polarfuchs", 35 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000067"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000068"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000069"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000070"));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000016"),
|
||||||
|
column: "Name",
|
||||||
|
value: "dd Gold");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000017"),
|
||||||
|
column: "Name",
|
||||||
|
value: "dd Platin");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000029"),
|
||||||
|
column: "Name",
|
||||||
|
value: "Agouti dd");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000030"),
|
||||||
|
column: "Name",
|
||||||
|
value: "Silberagouti dd");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000031"),
|
||||||
|
column: "Name",
|
||||||
|
value: "Kohlfuchs dd");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000032"),
|
||||||
|
column: "Name",
|
||||||
|
value: "Anthrazit dd");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000033"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 32);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000034"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 33);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000035"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 34);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000036"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 35);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000037"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 36);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000038"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 37);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000039"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 38);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000040"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 39);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000041"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 40);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000042"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 41);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000043"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 42);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000044"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 43);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000045"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 44);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000046"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 45);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000047"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 46);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000048"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 47);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000049"),
|
||||||
|
columns: new[] { "Name", "SortOrder" },
|
||||||
|
values: new object[] { "Topas dd", 48 });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000050"),
|
||||||
|
columns: new[] { "Name", "SortOrder" },
|
||||||
|
values: new object[] { "Blaufuchs dd", 49 });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000051"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 50);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000052"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 51);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000053"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 52);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000054"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 53);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000055"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 54);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000056"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 55);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000057"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 56);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000058"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 57);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000059"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 58);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000060"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 59);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000061"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 60);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000062"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 61);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000063"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 62);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000064"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 63);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "ColorVarieties",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("00000000-0000-0000-0000-000000000066"),
|
||||||
|
column: "SortOrder",
|
||||||
|
value: 65);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -321,14 +321,14 @@ namespace GerbilManagerWebAPI.Migrations
|
|||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000016"),
|
Id = new Guid("00000000-0000-0000-0000-000000000016"),
|
||||||
CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
|
CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
|
||||||
Name = "dd Gold",
|
Name = "Dilute Gold",
|
||||||
SortOrder = 15
|
SortOrder = 15
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000017"),
|
Id = new Guid("00000000-0000-0000-0000-000000000017"),
|
||||||
CanonicalGenotype = "aa CC dd EE GG pp spsp rere",
|
CanonicalGenotype = "aa CC dd EE GG pp spsp rere",
|
||||||
Name = "dd Platin",
|
Name = "Dilute Platin",
|
||||||
SortOrder = 16
|
SortOrder = 16
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
@@ -412,28 +412,28 @@ namespace GerbilManagerWebAPI.Migrations
|
|||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000029"),
|
Id = new Guid("00000000-0000-0000-0000-000000000029"),
|
||||||
CanonicalGenotype = "AA CC dd EE GG PP spsp rere",
|
CanonicalGenotype = "AA CC dd EE GG PP spsp rere",
|
||||||
Name = "Agouti dd",
|
Name = "Dilute Agouti",
|
||||||
SortOrder = 28
|
SortOrder = 28
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000030"),
|
Id = new Guid("00000000-0000-0000-0000-000000000030"),
|
||||||
CanonicalGenotype = "AA CC dd EE gg PP spsp rere",
|
CanonicalGenotype = "AA CC dd EE gg PP spsp rere",
|
||||||
Name = "Silberagouti dd",
|
Name = "Dilute Silberagouti",
|
||||||
SortOrder = 29
|
SortOrder = 29
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000031"),
|
Id = new Guid("00000000-0000-0000-0000-000000000031"),
|
||||||
CanonicalGenotype = "aa CC dd ee GG PP spsp rere",
|
CanonicalGenotype = "aa CC dd ee GG PP spsp rere",
|
||||||
Name = "Kohlfuchs dd",
|
Name = "Dilute Kohlfuchs",
|
||||||
SortOrder = 30
|
SortOrder = 30
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000032"),
|
Id = new Guid("00000000-0000-0000-0000-000000000032"),
|
||||||
CanonicalGenotype = "aa CC dd EE gg PP spsp rere",
|
CanonicalGenotype = "aa CC dd EE gg PP spsp rere",
|
||||||
Name = "Anthrazit dd",
|
Name = "Dilute Anthrazit",
|
||||||
SortOrder = 31
|
SortOrder = 31
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
@@ -441,224 +441,224 @@ namespace GerbilManagerWebAPI.Migrations
|
|||||||
Id = new Guid("00000000-0000-0000-0000-000000000033"),
|
Id = new Guid("00000000-0000-0000-0000-000000000033"),
|
||||||
CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
|
CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
|
||||||
Name = "Silberschimmel",
|
Name = "Silberschimmel",
|
||||||
SortOrder = 32
|
SortOrder = 36
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000034"),
|
Id = new Guid("00000000-0000-0000-0000-000000000034"),
|
||||||
CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
|
CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
|
||||||
Name = "Polarfuchsschimmel",
|
Name = "Polarfuchsschimmel",
|
||||||
SortOrder = 33
|
SortOrder = 37
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000035"),
|
Id = new Guid("00000000-0000-0000-0000-000000000035"),
|
||||||
CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
|
CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
|
||||||
Name = "Algierfuchsschimmel",
|
Name = "Algierfuchsschimmel",
|
||||||
SortOrder = 34
|
SortOrder = 38
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000036"),
|
Id = new Guid("00000000-0000-0000-0000-000000000036"),
|
||||||
CanonicalGenotype = "aa CC DD efef GG PP spsp rere",
|
CanonicalGenotype = "aa CC DD efef GG PP spsp rere",
|
||||||
Name = "Kohlfuchsschimmel",
|
Name = "Kohlfuchsschimmel",
|
||||||
SortOrder = 35
|
SortOrder = 39
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000037"),
|
Id = new Guid("00000000-0000-0000-0000-000000000037"),
|
||||||
CanonicalGenotype = "aa CC DD efef gg PP spsp rere",
|
CanonicalGenotype = "aa CC DD efef gg PP spsp rere",
|
||||||
Name = "Blaufuchsschimmel",
|
Name = "Blaufuchsschimmel",
|
||||||
SortOrder = 36
|
SortOrder = 40
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000038"),
|
Id = new Guid("00000000-0000-0000-0000-000000000038"),
|
||||||
CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
|
CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
|
||||||
Name = "Kohlfuchs, hell",
|
Name = "Kohlfuchs, hell",
|
||||||
SortOrder = 37
|
SortOrder = 41
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000039"),
|
Id = new Guid("00000000-0000-0000-0000-000000000039"),
|
||||||
CanonicalGenotype = "AA CC DD ee GG pp spsp rere",
|
CanonicalGenotype = "AA CC DD ee GG pp spsp rere",
|
||||||
Name = "Goldfuchs, hell",
|
Name = "Goldfuchs, hell",
|
||||||
SortOrder = 38
|
SortOrder = 42
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000040"),
|
Id = new Guid("00000000-0000-0000-0000-000000000040"),
|
||||||
CanonicalGenotype = "AA CC DD efef GG pp spsp rere",
|
CanonicalGenotype = "AA CC DD efef GG pp spsp rere",
|
||||||
Name = "Goldfuchsschimmel",
|
Name = "Goldfuchsschimmel",
|
||||||
SortOrder = 39
|
SortOrder = 43
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000041"),
|
Id = new Guid("00000000-0000-0000-0000-000000000041"),
|
||||||
CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
|
CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
|
||||||
Name = "Gold-Hell",
|
Name = "Gold-Hell",
|
||||||
SortOrder = 40
|
SortOrder = 44
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000042"),
|
Id = new Guid("00000000-0000-0000-0000-000000000042"),
|
||||||
CanonicalGenotype = "aa CC DD ee gg PP spsp rere",
|
CanonicalGenotype = "aa CC DD ee gg PP spsp rere",
|
||||||
Name = "Blaufuchs, hell",
|
Name = "Blaufuchs, hell",
|
||||||
SortOrder = 41
|
SortOrder = 45
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000043"),
|
Id = new Guid("00000000-0000-0000-0000-000000000043"),
|
||||||
CanonicalGenotype = "aa CC DD efef GG pp spsp rere",
|
CanonicalGenotype = "aa CC DD efef GG pp spsp rere",
|
||||||
Name = "Rotfuchsschimmel",
|
Name = "Rotfuchsschimmel",
|
||||||
SortOrder = 42
|
SortOrder = 46
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000044"),
|
Id = new Guid("00000000-0000-0000-0000-000000000044"),
|
||||||
CanonicalGenotype = "AA CC DD ee gg PP spsp rere",
|
CanonicalGenotype = "AA CC DD ee gg PP spsp rere",
|
||||||
Name = "Polarfuchs, hell",
|
Name = "Polarfuchs, hell",
|
||||||
SortOrder = 43
|
SortOrder = 47
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000045"),
|
Id = new Guid("00000000-0000-0000-0000-000000000045"),
|
||||||
CanonicalGenotype = "aa CC DD efef GG PP spsp rere",
|
CanonicalGenotype = "aa CC DD efef GG PP spsp rere",
|
||||||
Name = "Kohlfuchsschimmel, hell",
|
Name = "Kohlfuchsschimmel, hell",
|
||||||
SortOrder = 44
|
SortOrder = 48
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000046"),
|
Id = new Guid("00000000-0000-0000-0000-000000000046"),
|
||||||
CanonicalGenotype = "aa CC DD ee GG pp spsp rere",
|
CanonicalGenotype = "aa CC DD ee GG pp spsp rere",
|
||||||
Name = "Rotfuchs, hell",
|
Name = "Rotfuchs, hell",
|
||||||
SortOrder = 45
|
SortOrder = 49
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000047"),
|
Id = new Guid("00000000-0000-0000-0000-000000000047"),
|
||||||
CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
|
CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
|
||||||
Name = "Kohlfuchs-Hell",
|
Name = "Kohlfuchs-Hell",
|
||||||
SortOrder = 46
|
SortOrder = 50
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000048"),
|
Id = new Guid("00000000-0000-0000-0000-000000000048"),
|
||||||
CanonicalGenotype = "AA CC DD ee GG PP spsp rere",
|
CanonicalGenotype = "AA CC DD ee GG PP spsp rere",
|
||||||
Name = "Algierfuchs, hell",
|
Name = "Algierfuchs, hell",
|
||||||
SortOrder = 47
|
SortOrder = 51
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000049"),
|
Id = new Guid("00000000-0000-0000-0000-000000000049"),
|
||||||
CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
|
CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
|
||||||
Name = "Topas dd",
|
Name = "Dilute Topas",
|
||||||
SortOrder = 48
|
SortOrder = 52
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000050"),
|
Id = new Guid("00000000-0000-0000-0000-000000000050"),
|
||||||
CanonicalGenotype = "aa CC dd ee gg pp spsp rere",
|
CanonicalGenotype = "aa CC dd ee gg pp spsp rere",
|
||||||
Name = "Blaufuchs dd",
|
Name = "Dilute Blaufuchs",
|
||||||
SortOrder = 49
|
SortOrder = 53
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000051"),
|
Id = new Guid("00000000-0000-0000-0000-000000000051"),
|
||||||
CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere",
|
CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere",
|
||||||
Name = "Marder",
|
Name = "Marder",
|
||||||
SortOrder = 50
|
SortOrder = 54
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000052"),
|
Id = new Guid("00000000-0000-0000-0000-000000000052"),
|
||||||
CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere",
|
CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere",
|
||||||
Name = "Siam",
|
Name = "Siam",
|
||||||
SortOrder = 51
|
SortOrder = 55
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000053"),
|
Id = new Guid("00000000-0000-0000-0000-000000000053"),
|
||||||
CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere",
|
CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere",
|
||||||
Name = "Zobel-Hell",
|
Name = "Zobel-Hell",
|
||||||
SortOrder = 52
|
SortOrder = 56
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000054"),
|
Id = new Guid("00000000-0000-0000-0000-000000000054"),
|
||||||
CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere",
|
CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere",
|
||||||
Name = "CP-Agouti",
|
Name = "CP-Agouti",
|
||||||
SortOrder = 53
|
SortOrder = 57
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000055"),
|
Id = new Guid("00000000-0000-0000-0000-000000000055"),
|
||||||
CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere",
|
CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere",
|
||||||
Name = "CP-Silberagouti",
|
Name = "CP-Silberagouti",
|
||||||
SortOrder = 54
|
SortOrder = 59
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000056"),
|
Id = new Guid("00000000-0000-0000-0000-000000000056"),
|
||||||
CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere",
|
CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere",
|
||||||
Name = "CP-Algierfuchs",
|
Name = "CP-Algierfuchs",
|
||||||
SortOrder = 55
|
SortOrder = 61
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000057"),
|
Id = new Guid("00000000-0000-0000-0000-000000000057"),
|
||||||
CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere",
|
CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere",
|
||||||
Name = "CP-Polarfuchs",
|
Name = "CP-Polarfuchs",
|
||||||
SortOrder = 56
|
SortOrder = 63
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000058"),
|
Id = new Guid("00000000-0000-0000-0000-000000000058"),
|
||||||
CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere",
|
CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere",
|
||||||
Name = "CP-Fuchs",
|
Name = "CP-Fuchs",
|
||||||
SortOrder = 57
|
SortOrder = 65
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000059"),
|
Id = new Guid("00000000-0000-0000-0000-000000000059"),
|
||||||
CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere",
|
CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere",
|
||||||
Name = "CP-Fuchs-Hell",
|
Name = "CP-Fuchs-Hell",
|
||||||
SortOrder = 58
|
SortOrder = 66
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000060"),
|
Id = new Guid("00000000-0000-0000-0000-000000000060"),
|
||||||
CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere",
|
CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere",
|
||||||
Name = "CP-Blaufuchs",
|
Name = "CP-Blaufuchs",
|
||||||
SortOrder = 59
|
SortOrder = 67
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000061"),
|
Id = new Guid("00000000-0000-0000-0000-000000000061"),
|
||||||
CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere",
|
CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere",
|
||||||
Name = "CP-Orangeschimmel",
|
Name = "CP-Orangeschimmel",
|
||||||
SortOrder = 60
|
SortOrder = 68
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000062"),
|
Id = new Guid("00000000-0000-0000-0000-000000000062"),
|
||||||
CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere",
|
CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere",
|
||||||
Name = "CP-Agouti-Hell",
|
Name = "CP-Agouti-Hell",
|
||||||
SortOrder = 61
|
SortOrder = 58
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000063"),
|
Id = new Guid("00000000-0000-0000-0000-000000000063"),
|
||||||
CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere",
|
CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere",
|
||||||
Name = "CP-Silberagouti-Hell",
|
Name = "CP-Silberagouti-Hell",
|
||||||
SortOrder = 62
|
SortOrder = 60
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("00000000-0000-0000-0000-000000000064"),
|
Id = new Guid("00000000-0000-0000-0000-000000000064"),
|
||||||
CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere",
|
CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere",
|
||||||
Name = "CP-Algierfuchs-Hell",
|
Name = "CP-Algierfuchs-Hell",
|
||||||
SortOrder = 63
|
SortOrder = 62
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
@@ -672,7 +672,35 @@ namespace GerbilManagerWebAPI.Migrations
|
|||||||
Id = new Guid("00000000-0000-0000-0000-000000000066"),
|
Id = new Guid("00000000-0000-0000-0000-000000000066"),
|
||||||
CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere",
|
CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere",
|
||||||
Name = "CP-Orangeschimmel-Hell",
|
Name = "CP-Orangeschimmel-Hell",
|
||||||
SortOrder = 65
|
SortOrder = 69
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000067"),
|
||||||
|
CanonicalGenotype = "AA CC dd ee GG PP spsp rere",
|
||||||
|
Name = "Dilute Algierfuchs",
|
||||||
|
SortOrder = 32
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000068"),
|
||||||
|
CanonicalGenotype = "AA CC dd ee GG pp spsp rere",
|
||||||
|
Name = "Dilute Goldfuchs",
|
||||||
|
SortOrder = 33
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000069"),
|
||||||
|
CanonicalGenotype = "aa CC dd ee GG pp spsp rere",
|
||||||
|
Name = "Dilute Rotfuchs",
|
||||||
|
SortOrder = 34
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("00000000-0000-0000-0000-000000000070"),
|
||||||
|
CanonicalGenotype = "AA CC dd ee gg PP spsp rere",
|
||||||
|
Name = "Dilute Polarfuchs",
|
||||||
|
SortOrder = 35
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -79,3 +79,34 @@ test('Namenloser Ahne zeigt Platzhalter in der Stammbaum-Karte (UI-POLISH-2)', a
|
|||||||
// Karte selbst zeigt '(ohne Namen)' statt leer
|
// Karte selbst zeigt '(ohne Namen)' statt leer
|
||||||
await expect(page.locator('.pedigree-card__nametext')).toHaveText(de.pages.gerbils.nameless)
|
await expect(page.locator('.pedigree-card__nametext')).toHaveText(de.pages.gerbils.nameless)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Mini-Legende zeigt alle drei Interaktionshinweise (STAMMBAUM-EXPAND)', async ({ page }) => {
|
||||||
|
skipUnlessMock()
|
||||||
|
await page.goto('/rennmaeuse/kruemel/stammbaum')
|
||||||
|
await expect(page.locator('.pedigree-card').first()).toBeVisible()
|
||||||
|
|
||||||
|
const hints = page.locator('.stammbaum-hints')
|
||||||
|
await expect(hints).toBeVisible()
|
||||||
|
await expect(hints).toContainText(t.tapHint)
|
||||||
|
await expect(hints).toContainText(t.hintName)
|
||||||
|
await expect(hints).toContainText(t.hintExpand)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('+-Knopf ist sichtbar und lädt weitere Vorfahren nach (STAMMBAUM-EXPAND)', async ({ page }) => {
|
||||||
|
skipUnlessMock()
|
||||||
|
await page.goto('/rennmaeuse/kruemel/stammbaum')
|
||||||
|
await expect(page.locator('.pedigree-card').first()).toBeVisible()
|
||||||
|
|
||||||
|
// Einpassen, damit die 4. Generation (Emil mit +) im Viewport liegt.
|
||||||
|
const fit = page.getByRole('button', { name: t.zoomFit })
|
||||||
|
if (await fit.isVisible()) await fit.click()
|
||||||
|
await page.waitForTimeout(600)
|
||||||
|
|
||||||
|
// +-Button muss an der Tiefengrenze erscheinen (Emil hat litterId w-emil).
|
||||||
|
const expandBtn = page.getByRole('button', { name: t.expand }).first()
|
||||||
|
await expect(expandBtn).toBeVisible()
|
||||||
|
|
||||||
|
// Klick auf +: Emil wird aufgeklappt → Max (sein Vater) taucht als Link auf.
|
||||||
|
await expandBtn.click({ force: true })
|
||||||
|
await expect(page.getByRole('link', { name: 'Max' })).toBeVisible({ timeout: 8000 })
|
||||||
|
})
|
||||||
|
|||||||
@@ -335,7 +335,11 @@ export default function StammbaumPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="stammbaum-hint">{t.tapHint}</p>
|
<ul className="stammbaum-hints">
|
||||||
|
<li>{t.tapHint}</li>
|
||||||
|
<li>{t.hintName}</li>
|
||||||
|
<li>{t.hintExpand}</li>
|
||||||
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Druckansicht: am Bildschirm unsichtbar, ersetzt beim Drucken alles. */}
|
{/* Druckansicht: am Bildschirm unsichtbar, ersetzt beim Drucken alles. */}
|
||||||
|
|||||||
@@ -34,10 +34,15 @@
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stammbaum-hint {
|
.stammbaum-hints {
|
||||||
font-size: 0.8rem;
|
list-style: none;
|
||||||
color: var(--color-text-muted);
|
padding: 0;
|
||||||
margin: 0.4rem 0 0;
|
margin: 0.4rem 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.1rem 1rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Zeichenfläche ────────────────────────────────────────────── */
|
/* ── Zeichenfläche ────────────────────────────────────────────── */
|
||||||
@@ -175,20 +180,27 @@
|
|||||||
.pedigree-card__expand {
|
.pedigree-card__expand {
|
||||||
flex: none;
|
flex: none;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
width: 32px;
|
width: 40px;
|
||||||
height: 32px;
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1px solid var(--color-accent);
|
border: none;
|
||||||
background: var(--color-accent-soft);
|
|
||||||
color: var(--color-accent);
|
|
||||||
font-size: 1.05rem;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pedigree-card__expand:hover {
|
|
||||||
background: var(--color-accent);
|
background: var(--color-accent);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 1px 5px rgb(0 0 0 / 28%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pedigree-card__expand:hover,
|
||||||
|
.pedigree-card__expand:focus-visible {
|
||||||
|
opacity: 0.82;
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Druckansicht (Ahnentafel als CSS-Grid, Hunde-Zertifikat-Optik) ──
|
/* ── Druckansicht (Ahnentafel als CSS-Grid, Hunde-Zertifikat-Optik) ──
|
||||||
|
|||||||
@@ -355,6 +355,9 @@ export const de = {
|
|||||||
/** Auf Karten am Rand: weitere Vorfahren nachladen. */
|
/** Auf Karten am Rand: weitere Vorfahren nachladen. */
|
||||||
expand: 'Vorfahren laden',
|
expand: 'Vorfahren laden',
|
||||||
tapHint: 'Tippe auf ein Tier, um dessen Stammbaum anzuzeigen.',
|
tapHint: 'Tippe auf ein Tier, um dessen Stammbaum anzuzeigen.',
|
||||||
|
/** Mini-Legende unter dem Baum (STAMMBAUM-EXPAND). */
|
||||||
|
hintName: 'Namenslink: Tierakte öffnen',
|
||||||
|
hintExpand: '+: weitere Vorfahren nachladen',
|
||||||
zoomIn: 'Vergrößern',
|
zoomIn: 'Vergrößern',
|
||||||
zoomOut: 'Verkleinern',
|
zoomOut: 'Verkleinern',
|
||||||
zoomFit: 'Ansicht einpassen',
|
zoomFit: 'Ansicht einpassen',
|
||||||
|
|||||||
Reference in New Issue
Block a user