feat(import): RPRO3-Importer um 6 Feature-Tabellen erweitern

becken→Gehege-Reinigung, herktier→Erwerb, abgeben/abstat→Reservierung,
getback→Rücknahmen, nachfrage→Warteliste, ausz→Ausstellungen.
Deterministische GUIDs, idempotenter Re-Import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 07:05:01 +02:00
parent 00dc724605
commit c7b054e909
6 changed files with 661 additions and 8 deletions

View File

@@ -213,7 +213,7 @@ namespace GerbilManagerWebAPI.Import.Rpro3
// stamm_tb (eigene Tiere)
Query(conn,
"SELECT id,_NAME,_SEX,_BIRTH,_HERKUNFT,_ZB,_STATUS,_FEHLER,_KASTRAT_DATE FROM stamm_tb",
"SELECT id,_NAME,_SEX,_BIRTH,_HERKUNFT,_ZB,_STATUS,_FEHLER,_KASTRAT_DATE,_BECKEN FROM stamm_tb",
(r, ord) =>
{
var sid = IntOrNull(r, ord(r, "id"));
@@ -221,6 +221,8 @@ namespace GerbilManagerWebAPI.Import.Rpro3
var rid = sid.Value.ToString();
var name = Str(r, ord(r, "_NAME"));
stammName[sid.Value] = name;
var beckenId = IntOrNull(r, ord(r, "_BECKEN"));
if (beckenId is > 0) data.StammBecken[rid] = beckenId.Value;
var (farbe, fcode) = ColorFor(rid);
baum.TryGetValue(rid, out var par);
var herkId = IntOrNull(r, ord(r, "_HERKUNFT"));
@@ -406,9 +408,133 @@ namespace GerbilManagerWebAPI.Import.Rpro3
if (set.FileNames.Count > 0) data.Photos[set.Tid] = set;
});
ParseFeatureTables(conn, data);
return data;
}
/// <summary>Parst die Feature-Tabellen (becken/herktier/abgeben/abstat/getback/nachfrage/ausz).
/// Jede Tabelle ist optional — fehlt sie in einem älteren Backup, wird sie still übersprungen.</summary>
private void ParseFeatureTables(SqliteConnection conn, Rpro3Data data)
{
// becken_tb: Gehege.
QueryOptional(conn, "SELECT id,_BEZ,_SIZE,_MENGE,_CLEANED,_CYCLUS FROM becken_tb", (r, ord) =>
{
var id = IntOrNull(r, ord(r, "id"));
if (id is null) return;
data.Becken.Add(new Rpro3Becken
{
Id = id.Value,
Name = Str(r, ord(r, "_BEZ")),
Size = Str(r, ord(r, "_SIZE")),
Capacity = IntOrNull(r, ord(r, "_MENGE")),
LastCleaned = JdnToDate(Real(r, ord(r, "_CLEANED"))),
CycleDays = IntOrNull(r, ord(r, "_CYCLUS")),
});
});
// herktier_tb: Erwerb.
QueryOptional(conn, "SELECT id,_TID,_DATE,_PRICE,_BEM FROM herktier_tb", (r, ord) =>
{
var id = IntOrNull(r, ord(r, "id"));
if (id is null) return;
data.Acquisitions.Add(new Rpro3Acquisition
{
Id = id.Value,
Tid = Str(r, ord(r, "_TID")),
Date = JdnToDate(Real(r, ord(r, "_DATE"))),
Price = DecimalOrNull(r, ord(r, "_PRICE")),
Note = Str(r, ord(r, "_BEM")),
});
});
// abgeben_tb: Reservierung/Vormerkung.
QueryOptional(conn, "SELECT tid,_RES,_TERMIN,_ABN,_BEM FROM abgeben_tb", (r, ord) =>
{
var tid = Str(r, ord(r, "tid"));
if (string.IsNullOrWhiteSpace(tid)) return;
data.Abgeben.Add(new Rpro3Abgeben
{
Tid = tid,
Reserved = (IntOrNull(r, ord(r, "_RES")) ?? 0) != 0,
Appointment = JdnToDate(Real(r, ord(r, "_TERMIN"))),
AbnId = IntOrNull(r, ord(r, "_ABN")),
Note = Str(r, ord(r, "_BEM")),
});
});
// abstat_tb: Abgabe-Abschluss.
QueryOptional(conn, "SELECT tid,_IDABN,_AM,_PRICE,_BEM FROM abstat_tb", (r, ord) =>
{
var tid = Str(r, ord(r, "tid"));
if (string.IsNullOrWhiteSpace(tid)) return;
data.Abstat.Add(new Rpro3Abstat
{
Tid = tid,
AbnId = IntOrNull(r, ord(r, "_IDABN")),
HandedOver = JdnToDate(Real(r, ord(r, "_AM"))),
Price = DecimalOrNull(r, ord(r, "_PRICE")),
Note = Str(r, ord(r, "_BEM")),
});
});
// getback_tb: Rücknahme.
QueryOptional(conn, "SELECT id,_TID,_ZAM,_ZPREIS,_AM,_PREIS,_ABN,_BEM FROM getback_tb", (r, ord) =>
{
var id = IntOrNull(r, ord(r, "id"));
if (id is null) return;
data.Getback.Add(new Rpro3Getback
{
Id = id.Value,
Tid = Str(r, ord(r, "_TID")),
ReturnDate = JdnToDate(Real(r, ord(r, "_ZAM"))),
ReturnPrice = DecimalOrNull(r, ord(r, "_ZPREIS")),
OriginalSaleDate = JdnToDate(Real(r, ord(r, "_AM"))),
OriginalPrice = DecimalOrNull(r, ord(r, "_PREIS")),
AbnId = IntOrNull(r, ord(r, "_ABN")),
Note = Str(r, ord(r, "_BEM")),
});
});
// nachfrage_tb: Warteliste. _VOM ist das Anfragedatum (_DATE ist hier i. d. R. 0).
// Hat keine eigene _BEM-Spalte; _CODE (z. B. "egal") dient als Zusatznotiz.
QueryOptional(conn, "SELECT id,_COLOR,_SEX,_ABN,_VOM,_DATE,_STATUS,_CODE FROM nachfrage_tb", (r, ord) =>
{
var id = IntOrNull(r, ord(r, "id"));
if (id is null) return;
var code = Str(r, ord(r, "_CODE"));
data.Nachfrage.Add(new Rpro3Nachfrage
{
Id = id.Value,
WishColor = Str(r, ord(r, "_COLOR")),
WishGender = ParseGender(Str(r, ord(r, "_SEX"))),
AbnId = IntOrNull(r, ord(r, "_ABN")),
RequestedAt = JdnToDate(Real(r, ord(r, "_VOM"))) ?? JdnToDate(Real(r, ord(r, "_DATE"))),
Status = Str(r, ord(r, "_STATUS")),
Note = string.IsNullOrWhiteSpace(code) || code == "egal" ? null : code,
});
});
// ausz_tb: Ausstellungsergebnisse (in vielen Backups leer — robust gegen 0 Zeilen).
QueryOptional(conn, "SELECT id,_TID,_VERANSTALTUNG,_DATE,_ORT,_PLATZ,_AUSZ,_JUROR,_BEM FROM ausz_tb", (r, ord) =>
{
var id = IntOrNull(r, ord(r, "id"));
if (id is null) return;
data.Ausstellungen.Add(new Rpro3Ausstellung
{
Id = id.Value,
Tid = Str(r, ord(r, "_TID")),
EventName = Str(r, ord(r, "_VERANSTALTUNG")),
Date = JdnToDate(Real(r, ord(r, "_DATE"))),
Place = Str(r, ord(r, "_ORT")),
Placement = Str(r, ord(r, "_PLATZ")),
Award = Str(r, ord(r, "_AUSZ")),
Juror = Str(r, ord(r, "_JUROR")),
Note = Str(r, ord(r, "_BEM")),
});
});
}
private static Dictionary<string, (string farbe, string fcode)> ReadColors(SqliteConnection conn, string table)
{
var dict = new Dictionary<string, (string, string)>();
@@ -468,6 +594,16 @@ namespace GerbilManagerWebAPI.Import.Rpro3
onRow(r, ord);
}
}
/// <summary>Wie <see cref="Query"/>, ignoriert aber fehlende Tabellen/Spalten
/// (ältere Backups ohne die Feature-Tabellen). SQLite-Fehler werden geschluckt.</summary>
private static void QueryOptional(
SqliteConnection conn, string sql,
Action<SqliteDataReader, Func<SqliteDataReader, string, int>> onRow)
{
try { Query(conn, sql, onRow); }
catch (SqliteException) { /* Tabelle/Spalte fehlt in diesem Backup → überspringen */ }
}
}
public sealed class Rpro3FormatException : Exception