Commit: 481ab72
Parent: 4ab1161

Make list of participants

Mårten Åsberg committed on 2026-05-04 at 17:31
MatDenDagen/Components/Pages/Admin/Participants.razor +91 -0
diff --git a/MatDenDagen/Components/Pages/Admin/Participants.razor b/MatDenDagen/Components/Pages/Admin/Participants.razor
new file mode 100644
index 0000000..a6522bd
@@ -0,0 +1,91 @@
@page "/admin/participants"
@using MatDenDagen.Infrastructure.Storage.Database
@using MatDenDagen.Models
@using Microsoft.EntityFrameworkCore
@inject TimeProvider timeProvider
@inject QuestionnaireContext questionnaireContext
<ul>
@foreach (var participant in questionnaireContext.Participants)
{
<li>
<span>@participant.Name (@participant.PhoneNumber)</span>
<EditForm FormName="@($"RemoveParticipant-{participant.Id}")" Model="@removeParticipantModel" OnSubmit="@RemoveParticipant"
Enhance>
<input type="hidden" name="removeParticipantModel.ParticipantId" value="@participant.Id" />
<input type="submit" value="Ta bort" />
</EditForm>
</li>
}
</ul>
<hr />
<EditForm FormName="AddParticipant" Model="@addParticipantModel" OnSubmit="@AddParticipant" Enhance>
<p>
<label>
<span>Namn:</span>
<input type="text" name="addParticipantModel.Name" required />
</label>
</p>
<p>
<label>
<span>Telefonnummer:</span>
<input type="text" name="addParticipantModel.PhoneNumber" required />
</label>
</p>
<p>
<input type="submit" value="Lägg till" />
</p>
</EditForm>
@code {
[SupplyParameterFromForm]
private AddParticipantModel? addParticipantModel { get; set; }
[SupplyParameterFromForm]
private RemoveParticipantModel? removeParticipantModel { get; set; }
protected override void OnInitialized()
{
addParticipantModel ??= new();
removeParticipantModel ??= new();
}
private async Task AddParticipant()
{
if (addParticipantModel?.Name is not string name || addParticipantModel?.PhoneNumber is not string phoneNumber)
{
return;
}
var participant = new Participant { Id = Guid.CreateVersion7(timeProvider.GetUtcNow()), Name = name, PhoneNumber = phoneNumber };
questionnaireContext.Participants.Add(participant);
await questionnaireContext.SaveChangesAsync();
}
private async Task RemoveParticipant()
{
if (removeParticipantModel?.ParticipantId is not string participantId || !Guid.TryParse(participantId, out var id))
{
return;
}
var participant = questionnaireContext.Participants.SingleOrDefault(p => p.Id == id);
if (participant is null)
{
return;
}
questionnaireContext.Participants.Remove(participant);
await questionnaireContext.SaveChangesAsync();
}
private sealed class AddParticipantModel
{
public string? Name { get; set; }
public string? PhoneNumber { get; set; }
}
private sealed class RemoveParticipantModel
{
public string? ParticipantId { get; set; }
}
}
\ No newline at end of file
MatDenDagen/Components/Pages/Submission.razor +30 -3
diff --git a/MatDenDagen/Components/Pages/Submission.razor b/MatDenDagen/Components/Pages/Submission.razor
index aabd6b9..7c27cd6 100644
@@ -16,6 +16,13 @@
<p>Ditt svar har skickats in.</p>
}
@if (errorMessage is not null)
{
<div class="error">
<p>@errorMessage</p>
</div>
}
@if (questions.Count == 0)
{
<p>Inga frågor hittades.</p>
@@ -27,14 +34,20 @@ else
{
<p>
<label>
<span>@question.Text</span><br/>
<span>@question.Text</span><br />
<textarea name="answer[@question.Id]"></textarea>
</label>
</p>
}
<p>
<label>
<span>Bilagor</span><br/>
<span>Telefonnummer:</span><br />
<input type="text" name="phoneNumber" required placeholder="072XXXXXXX" />
</label>
</p>
<p>
<label>
<span>Bilagor</span><br />
<input type="file" name="files" multiple />
</label>
</p>
@@ -52,6 +65,8 @@ else
private List<Question> questions = [];
private string? errorMessage = null;
protected override async Task OnInitializedAsync()
{
questions = await questionnaireContext.Questions.ToListAsync(HttpContext.RequestAborted);
@@ -61,6 +76,17 @@ else
var form = HttpContext.Request.Form;
var phoneNumber = form["phoneNumber"].ToString();
var participant = await questionnaireContext.Participants
.SingleOrDefaultAsync(p => p.PhoneNumber == phoneNumber, HttpContext.RequestAborted);
if (participant is null)
{
errorMessage = "Okänt telefonnummer. Var god kontakta administratören för att registrera ditt telefonnummer.";
return;
}
var uploads = new List<Upload>();
foreach (var file in form.Files)
{
@@ -78,7 +104,8 @@ else
QuestionId = q.Id,
Text = form[$"answer[{q.Id}]"].ToString()
}).ToList(),
Uploads = uploads
Uploads = uploads,
Participant = participant.Id
});
await questionnaireContext.SaveChangesAsync(HttpContext.RequestAborted);
MatDenDagen/Infrastructure/Storage/Database/Migrations/20260504152716_CreateParticipants.Designer.cs +158 -0
diff --git a/MatDenDagen/Infrastructure/Storage/Database/Migrations/20260504152716_CreateParticipants.Designer.cs b/MatDenDagen/Infrastructure/Storage/Database/Migrations/20260504152716_CreateParticipants.Designer.cs
new file mode 100644
index 0000000..32ba326
@@ -0,0 +1,158 @@
// <auto-generated />
using System;
using MatDenDagen.Infrastructure.Storage.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace MatDenDagen.Infrastructure.Storage.Database.Migrations
{
[DbContext(typeof(QuestionnaireContext))]
[Migration("20260504152716_CreateParticipants")]
partial class CreateParticipants
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
modelBuilder.Entity("MatDenDagen.Models.Answer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("QuestionId")
.HasColumnType("TEXT");
b.Property<Guid?>("SubmissionId")
.HasColumnType("TEXT");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("QuestionId");
b.HasIndex("SubmissionId");
b.ToTable("Answer");
});
modelBuilder.Entity("MatDenDagen.Models.Participant", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("PhoneNumber")
.IsUnique();
b.ToTable("Participants");
});
modelBuilder.Entity("MatDenDagen.Models.Question", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Questions");
});
modelBuilder.Entity("MatDenDagen.Models.Submission", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("Participant")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Participant");
b.ToTable("Submissions");
});
modelBuilder.Entity("MatDenDagen.Models.Upload", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid?>("SubmissionId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("SubmissionId");
b.ToTable("Upload");
});
modelBuilder.Entity("MatDenDagen.Models.Answer", b =>
{
b.HasOne("MatDenDagen.Models.Question", null)
.WithMany()
.HasForeignKey("QuestionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MatDenDagen.Models.Submission", null)
.WithMany("Answers")
.HasForeignKey("SubmissionId");
});
modelBuilder.Entity("MatDenDagen.Models.Submission", b =>
{
b.HasOne("MatDenDagen.Models.Participant", null)
.WithMany()
.HasForeignKey("Participant")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MatDenDagen.Models.Upload", b =>
{
b.HasOne("MatDenDagen.Models.Submission", null)
.WithMany("Uploads")
.HasForeignKey("SubmissionId");
});
modelBuilder.Entity("MatDenDagen.Models.Submission", b =>
{
b.Navigation("Answers");
b.Navigation("Uploads");
});
#pragma warning restore 612, 618
}
}
}
MatDenDagen/Infrastructure/Storage/Database/Migrations/20260504152716_CreateParticipants.cs +71 -0
diff --git a/MatDenDagen/Infrastructure/Storage/Database/Migrations/20260504152716_CreateParticipants.cs b/MatDenDagen/Infrastructure/Storage/Database/Migrations/20260504152716_CreateParticipants.cs
new file mode 100644
index 0000000..50d7abc
@@ -0,0 +1,71 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MatDenDagen.Infrastructure.Storage.Database.Migrations
{
/// <inheritdoc />
public partial class CreateParticipants : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "Participant",
table: "Submissions",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000")
);
migrationBuilder.CreateTable(
name: "Participants",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
PhoneNumber = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_Participants", x => x.Id);
}
);
migrationBuilder.CreateIndex(
name: "IX_Submissions_Participant",
table: "Submissions",
column: "Participant"
);
migrationBuilder.CreateIndex(
name: "IX_Participants_PhoneNumber",
table: "Participants",
column: "PhoneNumber",
unique: true
);
migrationBuilder.AddForeignKey(
name: "FK_Submissions_Participants_Participant",
table: "Submissions",
column: "Participant",
principalTable: "Participants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(name: "FK_Submissions_Participants_Participant", table: "Submissions");
migrationBuilder.DropTable(name: "Participants");
migrationBuilder.DropIndex(name: "IX_Submissions_Participant", table: "Submissions");
migrationBuilder.DropColumn(name: "Participant", table: "Submissions");
}
}
}
MatDenDagen/Infrastructure/Storage/Database/Migrations/QuestionnaireContextModelSnapshot.cs +36 -0
diff --git a/MatDenDagen/Infrastructure/Storage/Database/Migrations/QuestionnaireContextModelSnapshot.cs b/MatDenDagen/Infrastructure/Storage/Database/Migrations/QuestionnaireContextModelSnapshot.cs
index f03f96c..5660200 100644
@@ -42,6 +42,28 @@ namespace MatDenDagen.Infrastructure.Storage.Database.Migrations
b.ToTable("Answer");
});
modelBuilder.Entity("MatDenDagen.Models.Participant", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("PhoneNumber")
.IsUnique();
b.ToTable("Participants");
});
modelBuilder.Entity("MatDenDagen.Models.Question", b =>
{
b.Property<Guid>("Id")
@@ -63,8 +85,13 @@ namespace MatDenDagen.Infrastructure.Storage.Database.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("Participant")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Participant");
b.ToTable("Submissions");
});
@@ -100,6 +127,15 @@ namespace MatDenDagen.Infrastructure.Storage.Database.Migrations
.HasForeignKey("SubmissionId");
});
modelBuilder.Entity("MatDenDagen.Models.Submission", b =>
{
b.HasOne("MatDenDagen.Models.Participant", null)
.WithMany()
.HasForeignKey("Participant")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MatDenDagen.Models.Upload", b =>
{
b.HasOne("MatDenDagen.Models.Submission", null)
MatDenDagen/Infrastructure/Storage/Database/QuestionnaireContext.cs +7 -0
diff --git a/MatDenDagen/Infrastructure/Storage/Database/QuestionnaireContext.cs b/MatDenDagen/Infrastructure/Storage/Database/QuestionnaireContext.cs
index 60f8dbf..792b702 100644
@@ -7,6 +7,7 @@ public sealed class QuestionnaireContext(DbContextOptions<QuestionnaireContext>
{
public required DbSet<Question> Questions { get; init; }
public required DbSet<Submission> Submissions { get; init; }
public required DbSet<Participant> Participants { get; init; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -18,6 +19,7 @@ public sealed class QuestionnaireContext(DbContextOptions<QuestionnaireContext>
submissionBuilder.HasKey(s => s.Id);
submissionBuilder.HasMany(s => s.Answers).WithOne();
submissionBuilder.HasMany(s => s.Uploads).WithOne();
submissionBuilder.HasOne<Participant>().WithMany().HasForeignKey(s => s.Participant);
var answerBuilder = modelBuilder.Entity<Answer>();
answerBuilder.HasKey(a => a.Id);
@@ -27,5 +29,10 @@ public sealed class QuestionnaireContext(DbContextOptions<QuestionnaireContext>
var uploadBuilder = modelBuilder.Entity<Upload>();
uploadBuilder.HasKey(u => u.Id);
uploadBuilder.Property(u => u.Name);
var participantBuilder = modelBuilder.Entity<Participant>();
participantBuilder.HasKey(p => p.Id);
participantBuilder.Property(p => p.Name);
participantBuilder.HasIndex(p => p.PhoneNumber).IsUnique();
}
}
MatDenDagen/Models/Participant.cs +10 -0
diff --git a/MatDenDagen/Models/Participant.cs b/MatDenDagen/Models/Participant.cs
new file mode 100644
index 0000000..0ff9ae1
@@ -0,0 +1,10 @@
using System;
namespace MatDenDagen.Models;
public sealed class Participant
{
public required Guid Id { get; init; }
public required string PhoneNumber { get; init; }
public required string Name { get; init; }
}
MatDenDagen/Models/Submission.cs +1 -0
diff --git a/MatDenDagen/Models/Submission.cs b/MatDenDagen/Models/Submission.cs
index a01a0b9..8f32014 100644
@@ -8,4 +8,5 @@ public sealed class Submission
public required Guid Id { get; init; }
public required List<Answer> Answers { get; init; }
public required List<Upload> Uploads { get; init; }
public required Guid Participant { get; init; }
}
README.md +3 -3
diff --git a/README.md b/README.md
index 2e2cc5b..8bfa7bf 100644
@@ -5,13 +5,13 @@
- [ ] User pages
- [ ] See the date of **the day**
- [x] Submit questionnaire
- [ ] Questionnaire only accepts phone numbers from configured participants
- [x] Questionnaire only accepts phone numbers from configured participants
- [ ] Admin pages
- [ ] Password protection
- [ ] Configure possible date span for **the day**
- [ ] (Re)roll date of **the day**
- [ ] Configure participants
- [ ] Phone number for notifications
- [x] Configure participants
- [x] Phone number for notifications
- [ ] Time of day the want to be notified
- [x] Configure questions
- [ ] Export answers