feat: add a Tools page printer for note merge and print presets

Chapter officers can merge a markdown note onto filtered students, teams, or events and save the recipe. Extra student-note columns are additional fields (any ## … fields heading) so they work as print tokens whether imported or typed by hand.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 15:38:43 -04:00
co-authored by Cursor
parent 4cfd85b902
commit 3f50d6e635
39 changed files with 2199 additions and 69 deletions
+1
View File
@@ -15,6 +15,7 @@ namespace Data
public DbSet<Note> Notes { get; set; }
public DbSet<NoteHistory> NoteHistories { get; set; }
public DbSet<TeamMeetingHistory> TeamMeetingHistories { get; set; }
public DbSet<PrintPreset> PrintPresets { get; set; }
public AppDbContext()
{
@@ -0,0 +1,35 @@
using Core.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Data.Configurations;
public class PrintPresetConfiguration : IEntityTypeConfiguration<PrintPreset>
{
public void Configure(EntityTypeBuilder<PrintPreset> builder)
{
builder.HasKey(p => p.Id);
builder.Property(p => p.Name)
.IsRequired()
.HasMaxLength(100);
builder.HasIndex(p => p.Name)
.IsUnique();
builder.Property(p => p.EntityType)
.HasConversion<string>()
.HasMaxLength(32)
.IsRequired();
builder.Property(p => p.FiltersJson)
.IsRequired()
.HasColumnType("TEXT");
builder.HasOne(p => p.Note)
.WithMany()
.HasForeignKey(p => p.NoteId)
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
}
}
@@ -0,0 +1,60 @@
using System;
using Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations
{
/// <inheritdoc />
[DbContext(typeof(AppDbContext))]
[Migration("20260830040000_AddPrintPresets")]
public partial class AddPrintPresets : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PrintPresets",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
NoteId = table.Column<int>(type: "INTEGER", nullable: false),
EntityType = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
FiltersJson = table.Column<string>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PrintPresets", x => x.Id);
table.ForeignKey(
name: "FK_PrintPresets_Notes_NoteId",
column: x => x.NoteId,
principalTable: "Notes",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_PrintPresets_Name",
table: "PrintPresets",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PrintPresets_NoteId",
table: "PrintPresets",
column: "NoteId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PrintPresets");
}
}
}
@@ -177,6 +177,42 @@ namespace Data.Migrations
b.ToTable("EventOccurrences");
});
modelBuilder.Entity("Core.Entities.PrintPreset", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("EntityType")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<string>("FiltersJson")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("NoteId")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.HasIndex("NoteId");
b.ToTable("PrintPresets");
});
modelBuilder.Entity("Core.Entities.Note", b =>
{
b.Property<int>("Id")
@@ -467,6 +503,17 @@ namespace Data.Migrations
b.Navigation("Note");
});
modelBuilder.Entity("Core.Entities.PrintPreset", b =>
{
b.HasOne("Core.Entities.Note", "Note")
.WithMany()
.HasForeignKey("NoteId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Note");
});
modelBuilder.Entity("Core.Entities.StudentEventRanking", b =>
{
b.HasOne("Core.Entities.EventDefinition", "EventDefinition")