diff --git a/FoodsharingSiegen.Contracts/Entity/Enums.cs b/FoodsharingSiegen.Contracts/Entity/Enums.cs index 9fac34e..057d277 100644 --- a/FoodsharingSiegen.Contracts/Entity/Enums.cs +++ b/FoodsharingSiegen.Contracts/Entity/Enums.cs @@ -17,7 +17,7 @@ namespace FoodsharingSiegen.Contracts.Entity /// The save profile audit type /// SaveProfile = 10, - + #region Usermanagement @@ -93,11 +93,11 @@ namespace FoodsharingSiegen.Contracts.Entity public enum ProspectStateFilter { All = 0, - + OnBoarding = 10, - + Verification = 20, - + Completed = 30 } @@ -127,6 +127,16 @@ namespace FoodsharingSiegen.Contracts.Entity Ambassador = 400 } + /// + /// Represents the state of a record within the system. + /// + public enum RecordState + { + Default = 10, + + Deleted = 20 + } + /// /// The fs network type enum /// @@ -202,12 +212,12 @@ namespace FoodsharingSiegen.Contracts.Entity /// The complete interaction type /// Complete = 70, - + /// /// The StepInBriefing interaction type /// StepInBriefing = 80, - + /// /// The StepInBriefing interaction type /// diff --git a/FoodsharingSiegen.Contracts/Entity/Prospect.cs b/FoodsharingSiegen.Contracts/Entity/Prospect.cs index cf5632c..c51eb9f 100644 --- a/FoodsharingSiegen.Contracts/Entity/Prospect.cs +++ b/FoodsharingSiegen.Contracts/Entity/Prospect.cs @@ -47,6 +47,11 @@ namespace FoodsharingSiegen.Contracts.Entity /// public string Name { get; set; } + /// + /// Gets or sets the state of the record within the system. + /// + public RecordState RecordState { get; set; } + /// /// Gets or sets the value of the warning (ab) /// diff --git a/FoodsharingSiegen.Contracts/Model/Parameters.cs b/FoodsharingSiegen.Contracts/Model/Parameters.cs index 5e1181f..e6b0921 100644 --- a/FoodsharingSiegen.Contracts/Model/Parameters.cs +++ b/FoodsharingSiegen.Contracts/Model/Parameters.cs @@ -5,5 +5,5 @@ namespace FoodsharingSiegen.Contracts.Model /// /// The get prospects parameter /// - public record GetProspectsParameter(List? MustHaveInteractions = null, List? CannotHaveInteractions = null); + public record GetProspectsParameter(List? MustHaveInteractions = null, List? CannotHaveInteractions = null, bool IncludeDeleted = false); } \ No newline at end of file diff --git a/FoodsharingSiegen.Server/Controls/ProspectContainer.razor b/FoodsharingSiegen.Server/Controls/ProspectContainer.razor index b166a90..3d1e9de 100644 --- a/FoodsharingSiegen.Server/Controls/ProspectContainer.razor +++ b/FoodsharingSiegen.Server/Controls/ProspectContainer.razor @@ -5,19 +5,34 @@ var divClass = $"{CssClass} pc-main"; if (Prospect is { Complete: true }) divClass += " complete"; if (Prospect is { Warning: true }) divClass += " warning"; + if (Prospect is { RecordState: RecordState.Deleted }) divClass += " deleted"; }
-
- @if (CurrentUser.IsInGroup(UserGroup.WelcomeTeam, UserGroup.Ambassador)) - { - - } - - @Prospect?.Name - - Profil öffnen - +
+
+ @Prospect?.Name + @Prospect?.FsId +
+
+ @if (CurrentUser.IsInGroup(UserGroup.WelcomeTeam, UserGroup.Ambassador)) + { + + } + + @if (CurrentUser.IsInGroup(UserGroup.WelcomeTeam, UserGroup.Ambassador)) + { + if (Prospect?.RecordState != RecordState.Deleted) + { + + } + else if(CurrentUser.IsAdmin()) + { + + } + + } +
@if (!string.IsNullOrWhiteSpace(Prospect?.Memo)) diff --git a/FoodsharingSiegen.Server/Controls/ProspectContainer.razor.cs b/FoodsharingSiegen.Server/Controls/ProspectContainer.razor.cs index 5dceb2c..b591559 100644 --- a/FoodsharingSiegen.Server/Controls/ProspectContainer.razor.cs +++ b/FoodsharingSiegen.Server/Controls/ProspectContainer.razor.cs @@ -48,6 +48,26 @@ namespace FoodsharingSiegen.Server.Controls #endregion + #region Private Method DeleteProspectAsync + + /// + /// Deletes the currently selected prospect after user confirmation. This action is irreversible and will update the record state to "Deleted". + /// + /// A task that represents the asynchronous delete operation. + private async Task DeleteProspectAsync() + { + if (Prospect == null) return; + + await ConfirmDialog.ShowAsync(ModalService, $"⚠️ {Prospect.Name} löschen", $"Soll {Prospect.Name} mit der FS-ID {Prospect.FsId} wirklich gelöscht werden? Das kann nicht rückgängig gemacht werden!!", async () => + { + Prospect.RecordState = RecordState.Deleted; + var updateR = await ProspectService.UpdateAsync(Prospect); + if (updateR.Success && OnDataChanged != null) await OnDataChanged(); + }); + } + + #endregion + #region Private Method EditProspectAsync private async Task EditProspectAsync() @@ -86,5 +106,25 @@ namespace FoodsharingSiegen.Server.Controls } #endregion + + #region Private Method RestoreProspectAsync + + /// + /// Restores the currently selected prospect after user confirmation. This action will update the record state to "Default". + /// + /// A task that represents the asynchronous restore operation. + private async Task RestoreProspectAsync() + { + if (Prospect == null) return; + + await ConfirmDialog.ShowAsync(ModalService, $"{Prospect.Name} wiederherstellen", $"Soll {Prospect.Name} mit der FS-ID {Prospect.FsId} wiederhergestellt werden?", async () => + { + Prospect.RecordState = RecordState.Default; + var updateR = await ProspectService.UpdateAsync(Prospect); + if (updateR.Success && OnDataChanged != null) await OnDataChanged(); + }); + } + + #endregion } } \ No newline at end of file diff --git a/FoodsharingSiegen.Server/Controls/ProspectContainer.razor.css b/FoodsharingSiegen.Server/Controls/ProspectContainer.razor.css index c7ce67c..0e089da 100644 --- a/FoodsharingSiegen.Server/Controls/ProspectContainer.razor.css +++ b/FoodsharingSiegen.Server/Controls/ProspectContainer.razor.css @@ -29,6 +29,20 @@ box-shadow: 0 0 9px 4px rgba(214,100,23,0.87); } +.pc-main.deleted { + -webkit-box-shadow: 0 0 9px 4px rgb(214 23 23 / 87%); + -moz-box-shadow: 0 0 9px 4px rgb(214 23 23 / 87%); + box-shadow: 0 0 9px 4px rgb(214 23 23 / 87%); +} + .complete { background: #76ff003b; +} + +i.link { + cursor: pointer; color: #64ae24; +} + +i.link:hover { + color: #000; } \ No newline at end of file diff --git a/FoodsharingSiegen.Server/Data/FsContext.cs b/FoodsharingSiegen.Server/Data/FsContext.cs index bb02793..bc1e123 100644 --- a/FoodsharingSiegen.Server/Data/FsContext.cs +++ b/FoodsharingSiegen.Server/Data/FsContext.cs @@ -1,42 +1,43 @@ using FoodsharingSiegen.Contracts.Entity; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; namespace FoodsharingSiegen.Server.Data { /// - /// The fs context class (a. beging, 21.05.2022) + /// The fs context class (a. beging, 21.05.2022) /// - /// + /// public sealed class FsContext : DbContext { #region Public Properties /// - /// Gets or sets the value of the interactions (ab) + /// Gets or sets the value of the audits (ab) + /// + public DbSet? Audits { get; set; } + + /// + /// Gets or sets the value of the interactions (ab) /// public DbSet? Interactions { get; set; } /// - /// Gets or sets the value of the prospects (ab) + /// Gets or sets the value of the prospects (ab) /// public DbSet? Prospects { get; set; } /// - /// Gets or sets the value of the users (ab) + /// Gets or sets the value of the users (ab) /// public DbSet? Users { get; set; } - - /// - /// Gets or sets the value of the audits (ab) - /// - public DbSet? Audits { get; set; } #endregion #region Setup/Teardown /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class /// /// The options (ab) public FsContext(DbContextOptions options) : base(options) @@ -45,11 +46,26 @@ namespace FoodsharingSiegen.Server.Data } #endregion - + + #region Override OnConfiguring + + /// + /// Configures the database context options. + /// + /// A builder used to create or modify options for the context. + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.ConfigureWarnings(builder => { builder.Ignore(RelationalEventId.PendingModelChangesWarning); }); + + base.OnConfiguring(optionsBuilder); + } + + #endregion + #region Public Method HasChanges /// - /// Describes whether this instance has changes + /// Describes whether this instance has changes /// /// The bool public bool HasChanges() diff --git a/FoodsharingSiegen.Server/Data/Service/ProspectService.cs b/FoodsharingSiegen.Server/Data/Service/ProspectService.cs index c96d28a..79c2932 100644 --- a/FoodsharingSiegen.Server/Data/Service/ProspectService.cs +++ b/FoodsharingSiegen.Server/Data/Service/ProspectService.cs @@ -115,6 +115,9 @@ namespace FoodsharingSiegen.Server.Data.Service if(parameter.CannotHaveInteractions != null && parameter.CannotHaveInteractions.Any()) prospectsQuery = prospectsQuery.Where(x => x.Interactions.All(i => !parameter.CannotHaveInteractions.Contains(i.Type))); + + if (!parameter.IncludeDeleted) + prospectsQuery = prospectsQuery.Where(x => x.RecordState != RecordState.Deleted); var prospects = await prospectsQuery.ToListAsync(); @@ -172,6 +175,7 @@ namespace FoodsharingSiegen.Server.Data.Service entityProspect.Name = prospect.Name; entityProspect.FsId = prospect.FsId; entityProspect.Warning = prospect.Warning; + entityProspect.RecordState = prospect.RecordState; var saveR = await Context.SaveChangesAsync(); diff --git a/FoodsharingSiegen.Server/Dialogs/ConfirmDialog.razor.cs b/FoodsharingSiegen.Server/Dialogs/ConfirmDialog.razor.cs index 3d66a42..059657b 100644 --- a/FoodsharingSiegen.Server/Dialogs/ConfirmDialog.razor.cs +++ b/FoodsharingSiegen.Server/Dialogs/ConfirmDialog.razor.cs @@ -38,7 +38,7 @@ namespace FoodsharingSiegen.Server.Dialogs var options = new ModalInstanceOptions { - Size = ModalSize.Small + // Size = ModalSize.Small }; await modalService.Show(title, x, options); diff --git a/FoodsharingSiegen.Server/Extensions.cs b/FoodsharingSiegen.Server/Extensions.cs index cfa8d9a..f7e1916 100644 --- a/FoodsharingSiegen.Server/Extensions.cs +++ b/FoodsharingSiegen.Server/Extensions.cs @@ -28,6 +28,31 @@ namespace FoodsharingSiegen.Server #endregion + /// + /// Ensures that all pending database migrations are applied at runtime. + /// This method checks for pending migrations in the database context and applies them, + /// allowing the application to remain compatible with the latest schema changes. + /// + /// An instance of used to access application services and lifecycle methods. + public static void ApplyMigrations(this WebApplication app) + { + using var scope = app.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + // Check and apply pending migrations + var pendingMigrations = dbContext.Database.GetPendingMigrations(); + if (pendingMigrations.Any()) + { + Console.WriteLine("Applying pending migrations..."); + dbContext.Database.Migrate(); + Console.WriteLine("Migrations applied successfully."); + } + else + { + Console.WriteLine("No pending migrations found."); + } + } + #region Public Method LoadAppSettings /// diff --git a/FoodsharingSiegen.Server/Migrations/20250329121239_Proposal-RecordState.Designer.cs b/FoodsharingSiegen.Server/Migrations/20250329121239_Proposal-RecordState.Designer.cs new file mode 100644 index 0000000..b500295 --- /dev/null +++ b/FoodsharingSiegen.Server/Migrations/20250329121239_Proposal-RecordState.Designer.cs @@ -0,0 +1,205 @@ +// +using System; +using FoodsharingSiegen.Server.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FoodsharingSiegen.Server.Migrations +{ + [DbContext(typeof(FsContext))] + [Migration("20250329121239_Proposal-RecordState")] + partial class ProposalRecordState + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Audit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Data1") + .HasColumnType("TEXT"); + + b.Property("Data2") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserID") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserID"); + + b.ToTable("Audits"); + }); + + modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Interaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Alert") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Info") + .HasColumnType("TEXT"); + + b.Property("NotNeeded") + .HasColumnType("INTEGER"); + + b.Property("ProspectID") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserID") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProspectID"); + + b.HasIndex("UserID"); + + b.ToTable("Interactions"); + }); + + modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Prospect", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("FsId") + .HasColumnType("INTEGER"); + + b.Property("Memo") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RecordState") + .HasColumnType("INTEGER"); + + b.Property("Warning") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Prospects"); + }); + + modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("EncryptedPassword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ForceLogout") + .HasColumnType("INTEGER"); + + b.Property("Groups") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Mail") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Memo") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Network") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Verified") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Audit", b => + { + b.HasOne("FoodsharingSiegen.Contracts.Entity.User", "User") + .WithMany() + .HasForeignKey("UserID"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Interaction", b => + { + b.HasOne("FoodsharingSiegen.Contracts.Entity.Prospect", "Prospect") + .WithMany("Interactions") + .HasForeignKey("ProspectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FoodsharingSiegen.Contracts.Entity.User", "User") + .WithMany("Interactions") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prospect"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Prospect", b => + { + b.Navigation("Interactions"); + }); + + modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.User", b => + { + b.Navigation("Interactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FoodsharingSiegen.Server/Migrations/20250329121239_Proposal-RecordState.cs b/FoodsharingSiegen.Server/Migrations/20250329121239_Proposal-RecordState.cs new file mode 100644 index 0000000..ffdf1d4 --- /dev/null +++ b/FoodsharingSiegen.Server/Migrations/20250329121239_Proposal-RecordState.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FoodsharingSiegen.Server.Migrations +{ + /// + public partial class ProposalRecordState : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RecordState", + table: "Prospects", + type: "INTEGER", + nullable: false, + defaultValue: 10); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RecordState", + table: "Prospects"); + } + } +} diff --git a/FoodsharingSiegen.Server/Migrations/FsContextModelSnapshot.cs b/FoodsharingSiegen.Server/Migrations/FsContextModelSnapshot.cs index 6d661cc..9cc9d2e 100644 --- a/FoodsharingSiegen.Server/Migrations/FsContextModelSnapshot.cs +++ b/FoodsharingSiegen.Server/Migrations/FsContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace FoodsharingSiegen.Server.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Audit", b => { @@ -103,6 +103,9 @@ namespace FoodsharingSiegen.Server.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("RecordState") + .HasColumnType("INTEGER"); + b.Property("Warning") .HasColumnType("INTEGER"); diff --git a/FoodsharingSiegen.Server/Pages/ProspectsAll.razor.cs b/FoodsharingSiegen.Server/Pages/ProspectsAll.razor.cs index 35af4bd..1aec9f1 100644 --- a/FoodsharingSiegen.Server/Pages/ProspectsAll.razor.cs +++ b/FoodsharingSiegen.Server/Pages/ProspectsAll.razor.cs @@ -59,7 +59,11 @@ namespace FoodsharingSiegen.Server.Pages /// private async Task LoadProspects() { - var parameter = new GetProspectsParameter(); + var parameter = new GetProspectsParameter + { + IncludeDeleted = true + }; + var prospectsR = await ProspectService.GetProspectsAsync(parameter); if (prospectsR.Success) ProspectList = prospectsR.Data; diff --git a/FoodsharingSiegen.Server/Program.cs b/FoodsharingSiegen.Server/Program.cs index 374ff4b..59fc6ee 100644 --- a/FoodsharingSiegen.Server/Program.cs +++ b/FoodsharingSiegen.Server/Program.cs @@ -35,6 +35,7 @@ builder.Services .AddMaterialIcons(); var app = builder.Build(); +app.ApplyMigrations(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment())