feat: import leftover student fields into notes and show them on the roster

Store leftover CSV columns on hidden student notes, move catalog import to /events/import, and persist Students index columns from Chapter Settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 23:55:03 -04:00
co-authored by Cursor
parent 4c91db37c2
commit 4cfd85b902
39 changed files with 2437 additions and 166 deletions
+23 -11
View File
@@ -1,4 +1,5 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using WebApp.Models;
namespace WebApp.Services;
@@ -8,6 +9,11 @@ namespace WebApp.Services;
/// </summary>
public class ChapterSettingsWriter : IChapterSettingsWriter
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true
};
private readonly IWebHostEnvironment _environment;
private readonly IConfiguration _configuration;
private readonly ILogger<ChapterSettingsWriter> _logger;
@@ -29,35 +35,41 @@ public class ChapterSettingsWriter : IChapterSettingsWriter
var appSettingsPath = GetAppSettingsPath();
var dataDir = Path.GetDirectoryName(appSettingsPath);
if (dataDir != null && !Directory.Exists(dataDir))
{
Directory.CreateDirectory(dataDir);
}
Dictionary<string, object?> root;
JsonObject root;
if (File.Exists(appSettingsPath))
{
var existingJson = await File.ReadAllTextAsync(appSettingsPath, cancellationToken);
root = JsonSerializer.Deserialize<Dictionary<string, object?>>(existingJson)
?? [];
root = JsonNode.Parse(existingJson) as JsonObject ?? [];
}
else
{
root = [];
}
root["ChapterSettings"] = settings;
var incoming = JsonSerializer.SerializeToNode(settings, JsonOptions) as JsonObject ?? [];
if (root["ChapterSettings"] is JsonObject existing)
{
foreach (var property in incoming)
existing[property.Key] = property.Value?.DeepClone();
}
else
{
root["ChapterSettings"] = incoming;
}
var options = new JsonSerializerOptions { WriteIndented = true };
var json = JsonSerializer.Serialize(root, options);
await File.WriteAllTextAsync(appSettingsPath, json, cancellationToken);
await File.WriteAllTextAsync(appSettingsPath, root.ToJsonString(JsonOptions), cancellationToken);
if (_configuration is IConfigurationRoot configurationRoot)
configurationRoot.Reload();
_logger.LogInformation("Chapter settings written to {Path}", appSettingsPath);
}
public async Task UpdateCompetitionYearAsync(string competitionYear, CancellationToken cancellationToken = default)
{
var settings = _configuration.GetSection("ChapterSettings").Get<ChapterSettings>()
?? new ChapterSettings();
var settings = ChapterSettings.FromConfiguration(_configuration);
settings.CompetitionYear = competitionYear;
await WriteAsync(settings, cancellationToken);
}