diff --git a/GerbilManagerWebAPI/Dtos/FeedbackDtos.cs b/GerbilManagerWebAPI/Dtos/FeedbackDtos.cs index bda0b49..850ee3d 100644 --- a/GerbilManagerWebAPI/Dtos/FeedbackDtos.cs +++ b/GerbilManagerWebAPI/Dtos/FeedbackDtos.cs @@ -47,7 +47,11 @@ namespace GerbilManagerWebAPI.Dtos /// When a genuinely-resolved ticket (no pending Rückfrage) was reopened; else null. DateTimeOffset? ReopenedAt = null, /// Soft-delete marker: when the ticket was deleted via the UI (recoverable); else null. - DateTimeOffset? DeletedAt = null); + DateTimeOffset? DeletedAt = null, + /// Optional AI-set category/topic for filtering (e.g. "Genetik", "Import"); null = none. + string? Category = null, + /// Was the resolution helpful? true=👍, false=👎, null=no feedback yet. + bool? Helpful = null); /// /// FEEDBACK: payload for PUT /feedback/{id}. Edit the message and/or toggle status, @@ -60,5 +64,9 @@ namespace GerbilManagerWebAPI.Dtos string? Question, string? Answer, string? FixNote, - string? AgentContext); + string? AgentContext, + /// Set/clear the ticket category. Empty/whitespace clears it. + string? Category = null, + /// 👍/👎 on a resolved ticket. null leaves it unchanged. + bool? Helpful = null); } diff --git a/GerbilManagerWebAPI/Endpoints/FeedbackEndpoints.cs b/GerbilManagerWebAPI/Endpoints/FeedbackEndpoints.cs index 78042eb..0166feb 100644 --- a/GerbilManagerWebAPI/Endpoints/FeedbackEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/FeedbackEndpoints.cs @@ -178,6 +178,17 @@ namespace GerbilManagerWebAPI.Endpoints entity.AgentContext = c.Length == 0 ? null : c; } + // KATEGORIE: frei wählbares Thema für Filter/Übersicht; leer = löschen. + if (input.Category is not null) + { + var cat = input.Category.Trim(); + entity.Category = cat.Length == 0 ? null : cat; + } + + // HILFREICH (👍/👎): nur setzen, wenn übermittelt (null = unverändert). + if (input.Helpful is not null) + entity.Helpful = input.Helpful; + await db.SaveChangesAsync(); return TypedResults.Ok(ToDto(entity)); }); @@ -264,6 +275,6 @@ namespace GerbilManagerWebAPI.Endpoints new(f.Id, f.Message, f.Context, f.GerbilId, f.LitterId, f.ContactId, f.EntityName, f.Url, f.ClientTimestamp, f.UserAgent, f.CreatedAt, f.Status, f.ResolvedAt, f.Question, f.Answer, f.AnsweredAt, f.FixNote, f.AgentContext, - DeserializeThread(f.Thread), f.ReopenedAt, f.DeletedAt); + DeserializeThread(f.Thread), f.ReopenedAt, f.DeletedAt, f.Category, f.Helpful); } } diff --git a/GerbilManagerWebAPI/Migrations/20260623091325_FeedbackCategoryAndHelpful.Designer.cs b/GerbilManagerWebAPI/Migrations/20260623091325_FeedbackCategoryAndHelpful.Designer.cs new file mode 100644 index 0000000..3763d23 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260623091325_FeedbackCategoryAndHelpful.Designer.cs @@ -0,0 +1,1800 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + [DbContext(typeof(ApplicationContext))] + [Migration("20260623091325_FeedbackCategoryAndHelpful")] + partial class FeedbackCategoryAndHelpful + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.AcquisitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("SourceContactId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("AcquisitionRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PageId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.ToTable("Blocks"); + + b.HasData( + new + { + Id = new Guid("51720002-0000-0000-0000-000000000001"), + Data = "{\"text\":\"Startseite\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000001"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000002"), + Data = "{\"text\":\"Über die Zucht\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000002"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000003"), + Data = "{\"text\":\"Abgabetiere\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000004"), + Data = "{\"text\":\"Abgabebedingungen\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000004"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000005"), + Data = "{\"text\":\"Farben & Genetik\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000005"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000006"), + Data = "{\"text\":\"Kontakt\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000006"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000010"), + Data = "{\"mode\":\"auto\",\"intro\":\"\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "AbgabetiereList" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000007"), + Data = "{\"text\":\"Impressum\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000070"), + Data = "{\"markdown\":\"**Angaben gemäß § 5 TMG**\\n\\nSeitenbetreiber: [Name und vollständige Adresse eintragen]\\n\\nE-Mail: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "RichText" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000008"), + Data = "{\"text\":\"Datenschutz\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000080"), + Data = "{\"markdown\":\"**Datenschutzerklärung**\\n\\nDiese Webseite dient der Vorstellung unserer Rennmauszucht. Es werden keine personenbezogenen Daten gespeichert oder weitergegeben.\\n\\nBei datenschutzbezogenen Fragen: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "RichText" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Homepage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameSuffix") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ZuchtName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BreederSettings"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-000000000001"), + Address = "", + City = "", + Email = "", + Homepage = "", + Name = "", + NameSuffix = "", + Phone = "", + ZuchtName = "" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanonicalGenotype") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ColorVarieties"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0000-000000000001"), + CanonicalGenotype = "AA chch DD EE GG pp spsp rere", + Name = "REW", + SortOrder = 0 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000002"), + CanonicalGenotype = "aa chch DD EE GG PP spsp rere", + Name = "Hermelin", + SortOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000003"), + CanonicalGenotype = "AA chch DD EE GG PP spsp rere", + Name = "Himalaya", + SortOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000004"), + CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere", + Name = "Zobel", + SortOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000005"), + CanonicalGenotype = "AA CC DD efef GG pp spsp rere", + Name = "Rotaugenschimmel", + SortOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000006"), + CanonicalGenotype = "AA CC DD EE GG PP spsp rere", + Name = "Agouti", + SortOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000007"), + CanonicalGenotype = "aa CC DD EE GG PP spsp rere", + Name = "Schwarz", + SortOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000008"), + CanonicalGenotype = "AA CC DD EE gg PP spsp rere", + Name = "Silberagouti", + SortOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000009"), + CanonicalGenotype = "aa CC DD EE gg PP spsp rere", + Name = "Anthrazit", + SortOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000010"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs", + SortOrder = 9 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000011"), + CanonicalGenotype = "aa CC dd EE GG PP spsp rere", + Name = "Blau", + SortOrder = 10 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000012"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold", + SortOrder = 11 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000013"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin", + SortOrder = 12 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000014"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs", + SortOrder = 13 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000015"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs", + SortOrder = 14 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000016"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Gold", + SortOrder = 15 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000017"), + CanonicalGenotype = "aa CC dd EE GG pp spsp rere", + Name = "Dilute Platin", + SortOrder = 16 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000018"), + CanonicalGenotype = "aa CC DD EE gg pp spsp rere", + Name = "Altweiss (REW)", + SortOrder = 17 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000019"), + CanonicalGenotype = "AA CC DD ee gg pp spsp rere", + Name = "Apricot (Blassfuchs)", + SortOrder = 18 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000020"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs", + SortOrder = 19 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000021"), + CanonicalGenotype = "aa CC DD ee gg pp spsp rere", + Name = "C-Separator", + SortOrder = 20 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000022"), + CanonicalGenotype = "AA CC DD EE gg pp spsp rere", + Name = "Elfenbein", + SortOrder = 21 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000023"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs", + SortOrder = 22 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000024"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs", + SortOrder = 23 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000025"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Saphir", + SortOrder = 24 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000026"), + CanonicalGenotype = "AA CC DD efef GG PP spsp rere", + Name = "Orangeschimmel", + SortOrder = 25 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000027"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Topas", + SortOrder = 26 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000028"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin-Hell", + SortOrder = 27 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000029"), + CanonicalGenotype = "AA CC dd EE GG PP spsp rere", + Name = "Dilute Agouti", + SortOrder = 28 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000030"), + CanonicalGenotype = "AA CC dd EE gg PP spsp rere", + Name = "Dilute Silberagouti", + SortOrder = 29 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000031"), + CanonicalGenotype = "aa CC dd ee GG PP spsp rere", + Name = "Dilute Kohlfuchs", + SortOrder = 30 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000032"), + CanonicalGenotype = "aa CC dd EE gg PP spsp rere", + Name = "Dilute Anthrazit", + SortOrder = 31 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000033"), + CanonicalGenotype = "AA CC DD efef gg PP spsp rere", + Name = "Silberschimmel", + SortOrder = 36 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000034"), + CanonicalGenotype = "AA CC DD efe gg PP spsp rere", + Name = "Polarfuchsschimmel", + SortOrder = 37 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000035"), + CanonicalGenotype = "AA CC DD efe GG PP spsp rere", + Name = "Algierfuchsschimmel", + SortOrder = 38 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000036"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel", + SortOrder = 39 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000037"), + CanonicalGenotype = "aa CC DD efe gg PP spsp rere", + Name = "Blaufuchsschimmel", + SortOrder = 40 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000038"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs, hell", + SortOrder = 41 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000039"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs, hell", + SortOrder = 42 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000040"), + CanonicalGenotype = "AA CC DD efe GG pp spsp rere", + Name = "Goldfuchsschimmel", + SortOrder = 43 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000041"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold-Hell", + SortOrder = 44 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000042"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs, hell", + SortOrder = 45 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000043"), + CanonicalGenotype = "aa CC DD efe GG pp spsp rere", + Name = "Rotfuchsschimmel", + SortOrder = 46 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000044"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs, hell", + SortOrder = 47 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000045"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel, hell", + SortOrder = 48 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000046"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs, hell", + SortOrder = 49 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000047"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs-Hell", + SortOrder = 50 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000048"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs, hell", + SortOrder = 51 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000049"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Topas", + SortOrder = 52 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000050"), + CanonicalGenotype = "aa CC dd ee gg PP spsp rere", + Name = "Dilute Blaufuchs", + SortOrder = 53 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000051"), + CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere", + Name = "Marder", + SortOrder = 54 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000052"), + CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere", + Name = "Siam", + SortOrder = 55 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000053"), + CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere", + Name = "Zobel-Hell", + SortOrder = 56 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000054"), + CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere", + Name = "CP-Agouti", + SortOrder = 57 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000055"), + CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere", + Name = "CP-Silberagouti", + SortOrder = 59 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000056"), + CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere", + Name = "CP-Algierfuchs", + SortOrder = 61 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000057"), + CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere", + Name = "CP-Polarfuchs", + SortOrder = 63 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000058"), + CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere", + Name = "CP-Fuchs", + SortOrder = 65 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000059"), + CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere", + Name = "CP-Fuchs-Hell", + SortOrder = 66 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000060"), + CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere", + Name = "CP-Blaufuchs", + SortOrder = 67 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000061"), + CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel", + SortOrder = 68 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000062"), + CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere", + Name = "CP-Agouti-Hell", + SortOrder = 58 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000063"), + CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere", + Name = "CP-Silberagouti-Hell", + SortOrder = 60 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000064"), + CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere", + Name = "CP-Algierfuchs-Hell", + SortOrder = 62 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000065"), + CanonicalGenotype = "AA cchmch DD ee gg PP spsp rere", + Name = "CP-Polarfuchs-Hell", + SortOrder = 64 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000066"), + CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel-Hell", + 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 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("IsBreeder") + .HasColumnType("boolean"); + + b.Property("IsReceiver") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSuffix") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CleaningCycleDays") + .HasColumnType("integer"); + + b.Property("LastCleanedDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Enclosures"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EnclosureId"); + + b.ToTable("EnclosurePhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ExhibitionResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Award") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("EventName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Placement") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("ExhibitionResults"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AgentContext") + .HasColumnType("text"); + + b.Property("Answer") + .HasColumnType("text"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ClientTimestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("Context") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("FixNote") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Helpful") + .HasColumnType("boolean"); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Question") + .HasColumnType("text"); + + b.Property("ReopenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Thread") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("Feedback"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CauseOfDeath") + .HasColumnType("text"); + + b.Property("CharacterNote") + .HasColumnType("text"); + + b.Property("CharacterTraits") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorVarietyId") + .HasColumnType("uuid"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DateOfDeath") + .HasColumnType("date"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text"); + + b.Property("Genotype") + .HasColumnType("text"); + + b.Property("GoHomeDate") + .HasColumnType("date"); + + b.Property("ImportSource") + .HasColumnType("text"); + + b.Property("IsCastrated") + .HasColumnType("boolean"); + + b.Property("IsDeaf") + .HasColumnType("boolean"); + + b.Property("IsResident") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSearch") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OriginBreeder") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("OriginContactId") + .HasColumnType("uuid"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("RawImportData") + .HasColumnType("text"); + + b.Property("ReceiverContactId") + .HasColumnType("uuid"); + + b.Property("SpottingType") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ColorVarietyId"); + + b.HasIndex("EnclosureId"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("LitterId"); + + b.HasIndex("OriginContactId"); + + b.HasIndex("ReceiverContactId"); + + b.ToTable("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("GerbilPhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Veterinarian") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("HealthRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeathsWithin8Weeks") + .HasColumnType("integer"); + + b.Property("ExpectedGoHomeDate") + .HasColumnType("date"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("FatherId") + .HasColumnType("uuid"); + + b.Property("LitterLetter") + .HasColumnType("text"); + + b.Property("MotherId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PairingCode") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("TotalBorn") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("FatherId"); + + b.HasIndex("MotherId"); + + b.ToTable("Litters"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppPasswordProtected") + .HasColumnType("text"); + + b.Property("BackgroundPollEnabled") + .HasColumnType("boolean"); + + b.Property("Folder") + .IsRequired() + .HasColumnType("text"); + + b.Property("GmailAddress") + .HasColumnType("text"); + + b.Property("LastUid") + .HasColumnType("bigint"); + + b.Property("PollIntervalMinutes") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MailSettings"); + + b.HasData( + new + { + Id = new Guid("ab0c0000-0000-0000-0000-000000000001"), + BackgroundPollEnabled = false, + Folder = "INBOX", + LastUid = 0L, + PollIntervalMinutes = 15 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Alt") + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SeoDescription") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Pages"); + + b.HasData( + new + { + Id = new Guid("51720001-0000-0000-0000-000000000001"), + Slug = "start", + Status = "Published", + Title = "Startseite" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000002"), + Slug = "ueber-die-zucht", + Status = "Published", + Title = "Über die Zucht" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000003"), + Slug = "abgabetiere", + Status = "Published", + Title = "Abgabetiere" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000004"), + Slug = "abgabebedingungen", + Status = "Published", + Title = "Abgabebedingungen" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000005"), + Slug = "farben-genetik", + Status = "Published", + Title = "Farben & Genetik" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000006"), + Slug = "kontakt", + Status = "Published", + Title = "Kontakt" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000007"), + Slug = "impressum", + Status = "Published", + Title = "Impressum" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000008"), + Slug = "datenschutz", + Status = "Published", + Title = "Datenschutz" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedContactId") + .HasColumnType("uuid"); + + b.Property("BodyText") + .HasColumnType("text"); + + b.Property("DraftReply") + .HasColumnType("text"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromName") + .HasColumnType("text"); + + b.Property("GmailMessageId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InReplyToMessageId") + .HasColumnType("text"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferencesHeader") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AssignedContactId"); + + b.HasIndex("GmailMessageId") + .IsUnique(); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ReturnRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FromContactId") + .HasColumnType("uuid"); + + b.Property("FromContactName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OriginalPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("OriginalSaleDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReturnPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("GerbilId"); + + b.ToTable("ReturnRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContractDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HandoverDate") + .HasColumnType("date"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("SaleContracts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.Property("SaleContractId") + .HasColumnType("uuid"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("PhotoId") + .HasColumnType("uuid"); + + b.HasKey("SaleContractId", "GerbilId"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleContractAnimal"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppointmentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("GerbilName") + .HasColumnType("text"); + + b.Property("HandedOverDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("ReservedForContactId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleReservations"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultLocale") + .IsRequired() + .HasColumnType("text"); + + b.Property("NavOrder") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sites"); + + b.HasData( + new + { + Id = new Guid("5172e000-0000-0000-0000-000000000001"), + DefaultLocale = "de", + NavOrder = "[\"51720001-0000-0000-0000-000000000001\",\"51720001-0000-0000-0000-000000000002\",\"51720001-0000-0000-0000-000000000003\",\"51720001-0000-0000-0000-000000000004\",\"51720001-0000-0000-0000-000000000005\",\"51720001-0000-0000-0000-000000000006\"]" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WaitingListEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContactName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("WishColor") + .HasColumnType("text"); + + b.Property("WishGender") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("WaitingListEntries"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("WeightGrams") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("WeightRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Page", null) + .WithMany("Blocks") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", null) + .WithMany() + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety") + .WithMany() + .HasForeignKey("ColorVarietyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure") + .WithMany("Gerbils") + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter") + .WithMany() + .HasForeignKey("LitterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact") + .WithMany() + .HasForeignKey("OriginContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact") + .WithMany() + .HasForeignKey("ReceiverContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ColorVariety"); + + b.Navigation("Enclosure"); + + b.Navigation("Litter"); + + b.Navigation("OriginContact"); + + b.Navigation("ReceiverContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father") + .WithMany() + .HasForeignKey("FatherId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother") + .WithMany() + .HasForeignKey("MotherId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Father"); + + b.Navigation("Mother"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "AssignedContact") + .WithMany() + .HasForeignKey("AssignedContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignedContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil") + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null) + .WithMany("Animals") + .HasForeignKey("SaleContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gerbil"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Navigation("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Navigation("Animals"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260623091325_FeedbackCategoryAndHelpful.cs b/GerbilManagerWebAPI/Migrations/20260623091325_FeedbackCategoryAndHelpful.cs new file mode 100644 index 0000000..4c01ded --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260623091325_FeedbackCategoryAndHelpful.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class FeedbackCategoryAndHelpful : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Category", + table: "Feedback", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Helpful", + table: "Feedback", + type: "boolean", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Category", + table: "Feedback"); + + migrationBuilder.DropColumn( + name: "Helpful", + table: "Feedback"); + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs index ea3976e..b680217 100644 --- a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs +++ b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs @@ -893,6 +893,9 @@ namespace GerbilManagerWebAPI.Migrations b.Property("AnsweredAt") .HasColumnType("timestamp with time zone"); + b.Property("Category") + .HasColumnType("text"); + b.Property("ClientTimestamp") .HasColumnType("timestamp with time zone"); @@ -918,6 +921,9 @@ namespace GerbilManagerWebAPI.Migrations b.Property("GerbilId") .HasColumnType("uuid"); + b.Property("Helpful") + .HasColumnType("boolean"); + b.Property("LitterId") .HasColumnType("uuid"); diff --git a/GerbilManagerWebAPI/Models/Feedback.cs b/GerbilManagerWebAPI/Models/Feedback.cs index 43c5428..41c41c4 100644 --- a/GerbilManagerWebAPI/Models/Feedback.cs +++ b/GerbilManagerWebAPI/Models/Feedback.cs @@ -70,6 +70,18 @@ namespace GerbilManagerWebAPI.Models /// public DateTimeOffset? DeletedAt { get; set; } + /// + /// Optionale Kategorie/Thema (von der KI gesetzt) für Filter + Übersicht, z. B. + /// "Genetik", "Import", "Stammbaum", "Daten", "Foto". Frei wählbar (kein FK), null = ohne. + /// + public string? Category { get; set; } + + /// + /// War die Lösung hilfreich? (Daumen hoch/runter auf gelösten Tickets.) true = 👍, + /// false = 👎 (löst i. d. R. ein Wiederöffnen aus), null = noch keine Rückmeldung. + /// + public bool? Helpful { get; set; } + /// A clarifying question (Rückfrage) a maintainer attaches to the ticket; null if none. public string? Question { get; set; } diff --git a/gerbil-manager-web/e2e/mock-api.ts b/gerbil-manager-web/e2e/mock-api.ts index 33ce97b..c56023c 100644 --- a/gerbil-manager-web/e2e/mock-api.ts +++ b/gerbil-manager-web/e2e/mock-api.ts @@ -433,6 +433,8 @@ export async function installMockApi(page: Page): Promise { thread: [], reopenedAt: null, deletedAt: null, + category: null, + helpful: null, } db.feedback.push(created) return json(route, 201, created) @@ -468,6 +470,8 @@ export async function installMockApi(page: Page): Promise { answer?: string fixNote?: string agentContext?: string + category?: string + helpful?: boolean } const row = db.feedback[idx] // Zustand vor den Mutationen (für die Wiederöffnen-Erkennung). @@ -536,6 +540,11 @@ export async function installMockApi(page: Page): Promise { const c = body.agentContext.trim() row.agentContext = c.length === 0 ? null : c } + if (typeof body.category === 'string') { + const cat = body.category.trim() + row.category = cat.length === 0 ? null : cat + } + if (typeof body.helpful === 'boolean') row.helpful = body.helpful return json(route, 200, row) } if (method === 'DELETE') { diff --git a/gerbil-manager-web/e2e/mock-data.ts b/gerbil-manager-web/e2e/mock-data.ts index 6258a40..19c8f9c 100644 --- a/gerbil-manager-web/e2e/mock-data.ts +++ b/gerbil-manager-web/e2e/mock-data.ts @@ -442,6 +442,7 @@ export function seedDb(): MockDb { userAgent: null, createdAt: '2026-06-10T09:00:00Z', status: 'Resolved', + category: 'Stammbaum', resolvedAt: '2026-06-12T14:00:00Z', question: null, answer: null, diff --git a/gerbil-manager-web/e2e/tickets.spec.ts b/gerbil-manager-web/e2e/tickets.spec.ts index 170aebc..5350434 100644 --- a/gerbil-manager-web/e2e/tickets.spec.ts +++ b/gerbil-manager-web/e2e/tickets.spec.ts @@ -248,4 +248,67 @@ test.describe('Meine Tickets', () => { await expect(reopened.getByText(tt.reopenRequest)).toBeVisible() await expect(reopened.getByText(tt.reopenedLabel)).toBeVisible() }) + + test('Volltextsuche filtert die Ticket-Liste', async ({ page }) => { + await page.goto('/hilfe/tickets') + await selectView(page, tt.filters.closed) + + const search = page.getByPlaceholder(tt.searchPlaceholder) + await search.fill('Krümel') + await expect( + page.getByText('Der Stammbaum zeigt den falschen Vater bei Krümel.'), + ).toBeVisible() + + await search.fill('xyzgibtsnicht') + await expect(page.getByText(tt.emptySearch)).toBeVisible() + }) + + test('Kategorie-Filter zeigt nur Tickets der gewählten Kategorie', async ({ page }) => { + await page.goto('/hilfe/tickets') + await selectView(page, tt.filters.closed) + + // Das Krümel-Ticket hat die Kategorie „Stammbaum" → als Chip sichtbar. + const card = page + .locator('.ticket-card') + .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) + await expect(card.locator('.ticket-card__category')).toHaveText('Stammbaum') + + // Über das Kategorie-Menü filtern. + await page.getByLabel(tt.categoryFilterLabel).selectOption('Stammbaum') + await expect( + page.getByText('Der Stammbaum zeigt den falschen Vater bei Krümel.'), + ).toBeVisible() + }) + + test('👍 auf gelöstem Ticket speichert Rückmeldung; 👎 öffnet es wieder', async ({ page }) => { + await page.goto('/hilfe/tickets') + await selectView(page, tt.filters.closed) + + const card = page + .locator('.ticket-card') + .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) + await expect(card.getByText(tt.helpfulQuestion)).toBeVisible() + + // 👍 → Dank-Text, kein Wiederöffnen. + await card.getByRole('button', { name: tt.helpfulYes }).click() + await expect(card.getByText(tt.helpfulThanks)).toBeVisible() + }) + + test('👎 auf gelöstem Ticket öffnet es als Rückfrage wieder', async ({ page }) => { + await page.goto('/hilfe/tickets') + await selectView(page, tt.filters.closed) + + const card = page + .locator('.ticket-card') + .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) + await card.getByRole('button', { name: tt.helpfulNo }).click() + + // Verlässt „Geschlossen", landet in „Rückfragen" mit Bitte um Infos. + await selectView(page, tt.filters.dialog) + const reopened = page + .locator('.ticket-card') + .filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' }) + await expect(reopened.locator('.ticket-badge--needsinfo')).toBeVisible() + await expect(reopened.getByText(tt.helpfulReopenNote)).toBeVisible() + }) }) diff --git a/gerbil-manager-web/src/api/feedback.ts b/gerbil-manager-web/src/api/feedback.ts index ef3b9e4..257bd91 100644 --- a/gerbil-manager-web/src/api/feedback.ts +++ b/gerbil-manager-web/src/api/feedback.ts @@ -60,6 +60,10 @@ export interface Feedback { * Solche Tickets erscheinen nur in der „Gelöscht"-Kategorie. null = nicht gelöscht. */ deletedAt: string | null + /** Optionale Kategorie/Thema (von der KI gesetzt), z. B. „Genetik", „Import". null = ohne. */ + category: string | null + /** War die Lösung hilfreich? true=👍, false=👎, null=keine Rückmeldung. */ + helpful: boolean | null /** Rückfrage einer/eines Betreuenden an die Züchterin (falls vorhanden). */ question: string | null /** Antwort der Züchterin auf die Rückfrage (falls vorhanden). */ @@ -95,6 +99,10 @@ export interface FeedbackUpdate { fixNote?: string /** INTERN: Arbeitsgedächtnis des Agenten (ändert den Status nicht). */ agentContext?: string + /** Kategorie/Thema setzen (leer ⇒ löschen). */ + category?: string + /** 👍/👎-Rückmeldung auf ein gelöstes Ticket. */ + helpful?: boolean } export function submitFeedback(body: FeedbackInput): Promise { diff --git a/gerbil-manager-web/src/components/ReportErrorDialog.tsx b/gerbil-manager-web/src/components/ReportErrorDialog.tsx index ca9e448..eb2474a 100644 --- a/gerbil-manager-web/src/components/ReportErrorDialog.tsx +++ b/gerbil-manager-web/src/components/ReportErrorDialog.tsx @@ -5,13 +5,19 @@ * dialog captures the current URL and a client timestamp itself and POSTs everything * to /feedback. Success/error is surfaced via the existing toast system. */ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' +import { Link } from 'react-router-dom' import { de } from '../strings/de' import { ApiError } from '../api/client' -import { submitFeedback, type FeedbackContext } from '../api/feedback' +import { listFeedback, submitFeedback, type FeedbackContext, type FeedbackTicket } from '../api/feedback' import { useToast } from './toast' import './reportErrorDialog.css' +/** Wörter (≥3 Zeichen) für einen einfachen Ähnlichkeits-Score. */ +function wordSet(s: string): Set { + return new Set((s.toLowerCase().match(/[a-zäöüß0-9]{3,}/g) ?? [])) +} + export interface ReportErrorContext { context: FeedbackContext gerbilId?: string | null @@ -60,6 +66,46 @@ function ReportErrorDialogBody({ const [submitting, setSubmitting] = useState(false) const textareaRef = useRef(null) + // Bereits gelöste Tickets einmalig laden, um beim Tippen ähnliche vorzuschlagen. + const [resolvedTickets, setResolvedTickets] = useState([]) + useEffect(() => { + let cancelled = false + listFeedback() + .then((all) => { + if (!cancelled) + setResolvedTickets(all.filter((tk) => tk.status === 'Resolved' && !tk.deletedAt)) + }) + .catch(() => { + /* ohne Vorschläge weiter */ + }) + return () => { + cancelled = true + } + }, []) + + // Ähnliche gelöste Tickets: Wort-Überlappung + Bonus bei gleichem Tier/Wurf/Kontakt. + const similar = useMemo(() => { + const msgWords = wordSet(message) + if (msgWords.size < 2) return [] + const scored = resolvedTickets.map((tk) => { + const hay = wordSet(`${tk.message} ${tk.fixNote ?? ''} ${tk.entityName ?? ''}`) + let score = 0 + for (const w of msgWords) if (hay.has(w)) score++ + if ( + (context.gerbilId && tk.gerbilId === context.gerbilId) || + (context.litterId && tk.litterId === context.litterId) || + (context.contactId && tk.contactId === context.contactId) + ) + score += 2 + return { tk, score } + }) + return scored + .filter((s) => s.score >= 2) + .sort((a, b) => b.score - a.score) + .slice(0, 3) + .map((s) => s.tk) + }, [message, resolvedTickets, context.gerbilId, context.litterId, context.contactId]) + // Entwurf laufend sichern (bei jeder Eingabe) bzw. entfernen, wenn leer. useEffect(() => { try { @@ -161,6 +207,21 @@ function ReportErrorDialogBody({ rows={5} /> + {similar.length > 0 && ( +
+
{t.similarTitle}
+
    + {similar.map((tk) => ( +
  • + + {tk.message.length > 80 ? `${tk.message.slice(0, 80)}…` : tk.message} + +
  • + ))} +
+
+ )} +
{t.debugTitle}
diff --git a/gerbil-manager-web/src/components/reportErrorDialog.css b/gerbil-manager-web/src/components/reportErrorDialog.css index 56526ae..4fe2bb7 100644 --- a/gerbil-manager-web/src/components/reportErrorDialog.css +++ b/gerbil-manager-web/src/components/reportErrorDialog.css @@ -147,3 +147,24 @@ gap: 0.5rem; margin-top: 1.1rem; } + +/* Vorschlag: ähnliche bereits gelöste Tickets. */ +.report-error__similar { + margin-top: 0.85rem; + padding: 0.6rem 0.75rem; + background: var(--color-accent-bg, #eef2ff); + border-radius: 8px; +} +.report-error__similar-title { + font-size: 0.85rem; + font-weight: 600; + margin-bottom: 0.35rem; +} +.report-error__similar-list { + margin: 0; + padding-left: 1.1rem; + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.9rem; +} diff --git a/gerbil-manager-web/src/pages/TicketsPage.tsx b/gerbil-manager-web/src/pages/TicketsPage.tsx index 9e2e7c5..db2a983 100644 --- a/gerbil-manager-web/src/pages/TicketsPage.tsx +++ b/gerbil-manager-web/src/pages/TicketsPage.tsx @@ -319,6 +319,29 @@ export default function TicketsPage() { } } + // 👍/👎 auf einem gelösten Ticket. 👍 = nur Rückmeldung speichern; 👎 = Ticket wieder öffnen + // (zurück in die Rückfragen) und um die fehlende Info bitten. + async function handleHelpful(ticket: FeedbackTicket, helpful: boolean) { + try { + if (helpful) { + await updateFeedback(ticket.id, { helpful: true }) + tickets.reload() + toast.success(t.helpfulThanks) + } else { + const hadRueckfrage = !!ticket.question && ticket.question.trim().length > 0 + await updateFeedback(ticket.id, { + helpful: false, + status: 'NeedsInfo', + question: hadRueckfrage ? ticket.question! : t.helpfulReopenNote, + }) + tickets.reload() + toast.success(t.reopenedToast) + } + } catch (err) { + toast.error(err instanceof ApiError ? err.message : t.updateError) + } + } + const rows = tickets.data // Verlinkung von Ticket zu Ticket: /hilfe/tickets?focus= wechselt in die passende @@ -355,6 +378,29 @@ export default function TicketsPage() { const visible = (rows ?? []).filter((ticket) => viewOf(ticket) === activeView) + // Suche (Volltext über Nachricht/Frage/Antwort/Name/Kategorie) + Kategorie-Filter, jeweils + // INNERHALB der aktiven Ansicht. + const [query, setQuery] = useState('') + const [categoryFilter, setCategoryFilter] = useState('') + // Vorhandene Kategorien (über alle nicht-gelöschten Tickets) für das Auswahlmenü. + const categories = useMemo(() => { + const set = new Set() + for (const tk of rows ?? []) if (!tk.deletedAt && tk.category) set.add(tk.category) + return [...set].sort((a, b) => a.localeCompare(b, 'de')) + }, [rows]) + const displayed = useMemo(() => { + const q = query.trim().toLowerCase() + return visible.filter((tk) => { + if (categoryFilter && tk.category !== categoryFilter) return false + if (!q) return true + const hay = [tk.message, tk.question, tk.answer, tk.fixNote, tk.entityName, tk.category] + .filter(Boolean) + .join(' ') + .toLowerCase() + return hay.includes(q) + }) + }, [visible, query, categoryFilter]) + // Nach dem Rendern der sichtbaren Liste zum fokussierten Ticket scrollen + kurz hervorheben. useEffect(() => { if (!focusId) return @@ -364,7 +410,7 @@ export default function TicketsPage() { el.classList.add('ticket-card--focus') const timer = setTimeout(() => el.classList.remove('ticket-card--focus'), 2500) return () => clearTimeout(timer) - }, [focusId, visible]) + }, [focusId, displayed]) // Scrollposition laufend in der Session merken (für die Zurück-Navigation). const SCROLL_KEY = 'tickets-scroll-y' @@ -523,12 +569,38 @@ export default function TicketsPage() { ))}
- {visible.length === 0 ? ( -

{emptyText[activeView]}

+
+ setQuery(e.target.value)} + /> + {categories.length > 0 && ( + + )} +
+ + {displayed.length === 0 ? ( +

{query || categoryFilter ? t.emptySearch : emptyText[activeView]}

) : (
    - {visible.map((ticket) => ( + {displayed.map((ticket) => ( handleAnswer(ticket, answer)} onDelete={() => handleDelete(ticket)} onRestore={() => handleRestore(ticket)} + onHelpful={(helpful) => handleHelpful(ticket, helpful)} /> ))}
@@ -555,6 +628,7 @@ interface TicketCardProps { onAnswer: (answer: string) => Promise onDelete: () => void onRestore: () => void + onHelpful: (helpful: boolean) => void } /** Status-Badge: Beschriftung + Modifier-Klasse je Lebenszyklus-Zustand. */ @@ -574,7 +648,15 @@ function statusBadge(status: FeedbackTicket['status']): { label: string; modifie /** Tage bis zur endgültigen Löschung (Soft-Delete-Aufbewahrung). */ const TRASH_RETENTION_DAYS = 30 -function TicketCard({ ticket, onToggleStatus, onSaveMessage, onAnswer, onDelete, onRestore }: TicketCardProps) { +function TicketCard({ + ticket, + onToggleStatus, + onSaveMessage, + onAnswer, + onDelete, + onRestore, + onHelpful, +}: TicketCardProps) { const toast = useToast() const [editing, setEditing] = useState(false) const [draft, setDraft] = useState(ticket.message) @@ -684,6 +766,7 @@ function TicketCard({ ticket, onToggleStatus, onSaveMessage, onAnswer, onDelete,
{badge.label} {contextLabel(ticket.context)} + {ticket.category && {ticket.category}} {ticket.entityName && (entityLink ? ( @@ -814,6 +897,24 @@ function TicketCard({ ticket, onToggleStatus, onSaveMessage, onAnswer, onDelete,
)} + {resolved && !deleted && ( +
+ {ticket.helpful === true ? ( + {t.helpfulThanks} + ) : ( + <> + {t.helpfulQuestion} + + + + )} +
+ )} + {deleted && trashDaysLeft !== null && (

{trashDaysLeft === 0 diff --git a/gerbil-manager-web/src/pages/tickets.css b/gerbil-manager-web/src/pages/tickets.css index 8415840..b3de01b 100644 --- a/gerbil-manager-web/src/pages/tickets.css +++ b/gerbil-manager-web/src/pages/tickets.css @@ -311,6 +311,60 @@ word-break: break-word; } +/* Suche + Kategorie-Filter über der Ticket-Liste. */ +.tickets-search { + display: flex; + gap: 0.5rem; + margin: 0 0 1rem; + flex-wrap: wrap; +} +.tickets-search__input { + flex: 1 1 16rem; + min-width: 0; + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border, #d1d5db); + border-radius: 8px; + font-size: 0.95rem; +} +.tickets-search__category { + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border, #d1d5db); + border-radius: 8px; + font-size: 0.95rem; + background: var(--color-surface, #fff); +} + +/* Kategorie-Chip im Ticket-Kopf. */ +.ticket-card__category { + font-size: 0.75rem; + font-weight: 600; + padding: 0.1rem 0.5rem; + border-radius: 999px; + background: var(--color-accent-bg, #eef2ff); + color: var(--color-accent-text, #3730a3); +} + +/* „Hat das geholfen?"-Leiste auf gelösten Tickets. */ +.ticket-card__helpful { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + margin: 0 0 0.85rem; + font-size: 0.9rem; +} +.ticket-card__helpful-q { + font-weight: 600; +} +.ticket-card__helpful-thanks { + color: var(--color-success-text, #166534); + font-weight: 600; +} +.btn--small { + padding: 0.25rem 0.6rem; + font-size: 0.85rem; +} + /* Papierkorb-Countdown: Hinweis, wann das Ticket endgültig gelöscht wird. */ .ticket-card__trash-notice { border-left: 3px solid var(--color-danger, #ef4444); diff --git a/gerbil-manager-web/src/strings/de.ts b/gerbil-manager-web/src/strings/de.ts index 75be3d8..4697558 100644 --- a/gerbil-manager-web/src/strings/de.ts +++ b/gerbil-manager-web/src/strings/de.ts @@ -1201,6 +1201,9 @@ export const de = { success: 'Danke! Dein Fehlerbericht wurde gesendet.', error: 'Fehlerbericht konnte nicht gesendet werden.', emptyMessage: 'Bitte beschreibe den Fehler.', + /** „Ähnliche bereits gelöste Tickets" im Melde-Fenster. */ + similarTitle: 'Schon mal gelöst? Vielleicht hilft eines davon:', + similarOpen: 'ansehen', /** Toast nach erfolgreichem "ID kopieren". */ idCopied: 'ID kopiert', idCopyFailed: 'ID konnte nicht kopiert werden.', @@ -1276,6 +1279,17 @@ export const de = { emptyOpen: 'Keine offenen Tickets.', emptyClosed: 'Noch keine geschlossenen Tickets.', emptyDeleted: 'Der Papierkorb ist leer.', + /** Suche + Kategorie-Filter. */ + searchPlaceholder: 'Tickets durchsuchen …', + categoryFilterLabel: 'Nach Kategorie filtern', + categoryAll: 'Alle Kategorien', + emptySearch: 'Keine Tickets passen zu Suche/Filter.', + /** Daumen-Rückmeldung auf gelösten Tickets. */ + helpfulQuestion: 'Hat dir das geholfen?', + helpfulYes: '👍 Ja', + helpfulNo: '👎 Nein', + helpfulThanks: 'Danke für die Rückmeldung!', + helpfulReopenNote: 'Schade — ich öffne das Ticket wieder. Bitte schreib kurz, was noch fehlt.', /** Changelog (fixNote) auf geschlossenen Tickets. */ changelogLabel: 'Was wurde geändert', /** Überschrift des Frage/Antwort-Verlaufs (frühere Runden). */