diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml index b2d1315..045dd4c 100644 --- a/.github/workflows/dotnet-desktop.yml +++ b/.github/workflows/dotnet-desktop.yml @@ -12,7 +12,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.302 + dotnet-version: 8.0.204 - name: Set up JDK 11 uses: actions/setup-java@v1 with: diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 3a7cca8..834cbdf 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -1,4 +1,4 @@ FROM mcr.microsoft.com/dotnet/core/aspnet:latest -COPY musiccatalogue.api-1.25.0.0 /opt/musiccatalogue.api-1.25.0.0 -WORKDIR /opt/musiccatalogue.api-1.25.0.0/bin +COPY musiccatalogue.api-1.30.0.0 /opt/musiccatalogue.api-1.30.0.0 +WORKDIR /opt/musiccatalogue.api-1.30.0.0/bin ENTRYPOINT [ "./MusicCatalogue.Api" ] diff --git a/docker/ui/Dockerfile b/docker/ui/Dockerfile index a141dca..7ba218c 100644 --- a/docker/ui/Dockerfile +++ b/docker/ui/Dockerfile @@ -1,6 +1,6 @@ FROM node:20-alpine -COPY musiccatalogue.ui-1.29.0.0 /opt/musiccatalogue.ui-1.29.0.0 -WORKDIR /opt/musiccatalogue.ui-1.29.0.0 +COPY musiccatalogue.ui-1.30.0.0 /opt/musiccatalogue.ui-1.30.0.0 +WORKDIR /opt/musiccatalogue.ui-1.30.0.0 RUN npm install RUN npm run build ENTRYPOINT [ "npm", "start" ] diff --git a/src/MusicCatalogue.Api/Controllers/ExportController.cs b/src/MusicCatalogue.Api/Controllers/ExportController.cs index 8f1e6da..c2f57ec 100644 --- a/src/MusicCatalogue.Api/Controllers/ExportController.cs +++ b/src/MusicCatalogue.Api/Controllers/ExportController.cs @@ -17,6 +17,7 @@ public class ExportController : Controller private readonly IBackgroundQueue _genreStatisticsQueue; private readonly IBackgroundQueue _monthlySpendQueue; private readonly IBackgroundQueue _retailerStatisticsQueue; + private readonly IBackgroundQueue _genreAlbumsQueue; public ExportController( IBackgroundQueue catalogueQueue, @@ -24,7 +25,8 @@ public ExportController( IBackgroundQueue artistStatisticsQueue, IBackgroundQueue genreStatisticsQueue, IBackgroundQueue monthlySpendQueue, - IBackgroundQueue retailerStatisticsQueue) + IBackgroundQueue retailerStatisticsQueue, + IBackgroundQueue genreAlbumsQueue) { _catalogueQueue = catalogueQueue; _equipmentQueue = equipmentQueue; @@ -32,6 +34,7 @@ public ExportController( _genreStatisticsQueue = genreStatisticsQueue; _monthlySpendQueue = monthlySpendQueue; _retailerStatisticsQueue = retailerStatisticsQueue; + _genreAlbumsQueue = genreAlbumsQueue; } [HttpPost] @@ -105,5 +108,17 @@ public IActionResult ExportRetailerStatisticsReport([FromBody] RetailerStatistic _retailerStatisticsQueue.Enqueue(item); return Accepted(); } + + [HttpPost] + [Route("genrealbums")] + public IActionResult ExportGenreAlbumsReport([FromBody] GenreAlbumsExportWorkItem item) + { + // Set the job name used in the job status record + item.JobName = "Albums by Genre Export"; + + // Queue the work item + _genreAlbumsQueue.Enqueue(item); + return Accepted(); + } } } diff --git a/src/MusicCatalogue.Api/Controllers/ReportsController.cs b/src/MusicCatalogue.Api/Controllers/ReportsController.cs index ee8b7dd..4ee92d1 100644 --- a/src/MusicCatalogue.Api/Controllers/ReportsController.cs +++ b/src/MusicCatalogue.Api/Controllers/ReportsController.cs @@ -137,5 +137,21 @@ public async Task>> GetRetailerStatisticsR // Convert to a list and return the results return results.ToList(); } + + [HttpGet] + [Route("genreAlbums/{genreId}")] + public async Task>> GetGenreAlbumsReportAsync(int genreId) + { + // Get the report content + var results = await _factory.GenreAlbums.GenerateReportAsync(genreId, 1, int.MaxValue); + + if (!results.Any()) + { + return NoContent(); + } + + // Convert to a list and return the results + return results.ToList(); + } } } diff --git a/src/MusicCatalogue.Api/Entities/GenreAlbumsExportWorkItem.cs b/src/MusicCatalogue.Api/Entities/GenreAlbumsExportWorkItem.cs new file mode 100644 index 0000000..16eba01 --- /dev/null +++ b/src/MusicCatalogue.Api/Entities/GenreAlbumsExportWorkItem.cs @@ -0,0 +1,8 @@ +namespace MusicCatalogue.Api.Entities +{ + public class GenreAlbumsExportWorkItem : BackgroundWorkItem + { + public string FileName { get; set; } = ""; + public int GenreId { get; set; } + } +} diff --git a/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj b/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj index 8d3c7f5..057532d 100644 --- a/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj +++ b/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj @@ -1,10 +1,10 @@  - net7.0 - 1.25.0.0 - 1.25.0.0 - 1.25.0 + net8.0 + 1.30.0.0 + 1.30.0.0 + 1.30.0 enable enable @@ -14,13 +14,13 @@ - - - - - - - + + + + + + + diff --git a/src/MusicCatalogue.Api/MusicCatalogue.Api.sln b/src/MusicCatalogue.Api/MusicCatalogue.Api.sln new file mode 100644 index 0000000..8ea3557 --- /dev/null +++ b/src/MusicCatalogue.Api/MusicCatalogue.Api.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MusicCatalogue.Api", "MusicCatalogue.Api.csproj", "{161E1F59-74B9-4B7F-A41D-124733824FA2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {161E1F59-74B9-4B7F-A41D-124733824FA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {161E1F59-74B9-4B7F-A41D-124733824FA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {161E1F59-74B9-4B7F-A41D-124733824FA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {161E1F59-74B9-4B7F-A41D-124733824FA2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B6903EC5-48B3-48BA-9C2D-F0F52303EE9E} + EndGlobalSection +EndGlobal diff --git a/src/MusicCatalogue.Api/Program.cs b/src/MusicCatalogue.Api/Program.cs index 1c3b5fc..47a775a 100644 --- a/src/MusicCatalogue.Api/Program.cs +++ b/src/MusicCatalogue.Api/Program.cs @@ -124,6 +124,10 @@ public static void Main(string[] args) builder.Services.AddSingleton, BackgroundQueue>(); builder.Services.AddHostedService(); + // Add the albums by genre exporter hosted service + builder.Services.AddSingleton, BackgroundQueue>(); + builder.Services.AddHostedService(); + // Configure JWT byte[] key = Encoding.ASCII.GetBytes(settings!.Secret); builder.Services.AddAuthentication(x => diff --git a/src/MusicCatalogue.Api/Services/GenreAlbumsExportService.cs b/src/MusicCatalogue.Api/Services/GenreAlbumsExportService.cs new file mode 100644 index 0000000..52dc4db --- /dev/null +++ b/src/MusicCatalogue.Api/Services/GenreAlbumsExportService.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Options; +using MusicCatalogue.Api.Entities; +using MusicCatalogue.Api.Interfaces; +using MusicCatalogue.Entities.Config; +using MusicCatalogue.Entities.Interfaces; +using MusicCatalogue.Entities.Reporting; +using MusicCatalogue.Logic.DataExchange.Generic; + +namespace MusicCatalogue.Api.Services +{ + public class GenreAlbumsExportService : BackgroundQueueProcessor + { + private readonly MusicApplicationSettings _settings; + public GenreAlbumsExportService( + ILogger> logger, + IBackgroundQueue queue, + IServiceScopeFactory serviceScopeFactory, + IOptions settings) + : base(logger, queue, serviceScopeFactory) + { + _settings = settings.Value; + } + + /// + /// Export the albums by genre report + /// + /// + /// + /// + protected override async Task ProcessWorkItem(GenreAlbumsExportWorkItem item, IMusicCatalogueFactory factory) + { + // Get the report data + MessageLogger.LogInformation("Retrieving the albums by genre report for export"); + var records = await factory.GenreAlbums.GenerateReportAsync(item.GenreId, 1, int.MaxValue); + + // Construct the full path to the export file + var filePath = Path.Combine(_settings.ReportsExportPath, item.FileName); + + // Export the report + var exporter = new CsvExporter(); + exporter.Export(records, filePath, ','); + MessageLogger.LogInformation("Albums by genre report export completed"); + } + } +} \ No newline at end of file diff --git a/src/MusicCatalogue.Data/Migrations/20240516104534_GenreAlbums.Designer.cs b/src/MusicCatalogue.Data/Migrations/20240516104534_GenreAlbums.Designer.cs new file mode 100644 index 0000000..9ceaa7d --- /dev/null +++ b/src/MusicCatalogue.Data/Migrations/20240516104534_GenreAlbums.Designer.cs @@ -0,0 +1,519 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MusicCatalogue.Data; + +#nullable disable + +namespace MusicCatalogue.Data.Migrations +{ + [DbContext(typeof(MusicCatalogueDbContext))] + [Migration("20240516104534_GenreAlbums")] + partial class GenreAlbums + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.Album", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("Id"); + + b.Property("ArtistId") + .HasColumnType("INTEGER") + .HasColumnName("ArtistId"); + + b.Property("CoverUrl") + .HasColumnType("TEXT") + .HasColumnName("CoverUrl"); + + b.Property("GenreId") + .HasColumnType("INTEGER") + .HasColumnName("GenreId"); + + b.Property("IsWishListItem") + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("TEXT") + .HasColumnName("Price"); + + b.Property("Purchased") + .HasColumnType("TEXT") + .HasColumnName("Purchased"); + + b.Property("Released") + .HasColumnType("INTEGER") + .HasColumnName("Released"); + + b.Property("RetailerId") + .HasColumnType("INTEGER") + .HasColumnName("RetailerId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("Title"); + + b.HasKey("Id"); + + b.HasIndex("ArtistId"); + + b.HasIndex("GenreId"); + + b.HasIndex("RetailerId"); + + b.ToTable("ALBUMS", (string)null); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.Artist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("Id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("Name"); + + b.Property("SearchableName") + .HasColumnType("TEXT") + .HasColumnName("SearchableName"); + + b.HasKey("Id"); + + b.ToTable("ARTISTS", (string)null); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.Equipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("Id"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("Description"); + + b.Property("EquipmentTypeId") + .HasColumnType("INTEGER") + .HasColumnName("EquipmentTypeId"); + + b.Property("IsWishListItem") + .HasColumnType("INTEGER"); + + b.Property("ManufacturerId") + .HasColumnType("INTEGER") + .HasColumnName("ManufacturerId"); + + b.Property("Model") + .HasColumnType("TEXT") + .HasColumnName("Model"); + + b.Property("Price") + .HasColumnType("TEXT") + .HasColumnName("Price"); + + b.Property("Purchased") + .HasColumnType("TEXT") + .HasColumnName("Purchased"); + + b.Property("RetailerId") + .HasColumnType("INTEGER") + .HasColumnName("RetailerId"); + + b.Property("SerialNumber") + .HasColumnType("TEXT") + .HasColumnName("SerialNumber"); + + b.HasKey("Id"); + + b.HasIndex("EquipmentTypeId"); + + b.HasIndex("ManufacturerId"); + + b.HasIndex("RetailerId"); + + b.ToTable("EQUIPMENT", (string)null); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.EquipmentType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("Id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("Name"); + + b.HasKey("Id"); + + b.ToTable("EQUIPMENT_TYPES", (string)null); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("Id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("Name"); + + b.HasKey("Id"); + + b.ToTable("GENRES", (string)null); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.JobStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("End") + .HasColumnType("DATETIME") + .HasColumnName("end"); + + b.Property("Error") + .HasColumnType("TEXT") + .HasColumnName("error"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("Parameters") + .HasColumnType("TEXT") + .HasColumnName("parameters"); + + b.Property("Start") + .HasColumnType("DATETIME") + .HasColumnName("start"); + + b.HasKey("Id"); + + b.ToTable("JOB_STATUS", (string)null); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.Manufacturer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("Id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("Name"); + + b.HasKey("Id"); + + b.ToTable("MANUFACTURERS", (string)null); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.Retailer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("Id"); + + b.Property("Address1") + .HasColumnType("TEXT"); + + b.Property("Address2") + .HasColumnType("TEXT"); + + b.Property("Country") + .HasColumnType("TEXT"); + + b.Property("County") + .HasColumnType("TEXT"); + + b.Property("Latitude") + .HasColumnType("TEXT"); + + b.Property("Longitude") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("Name"); + + b.Property("PostCode") + .HasColumnType("TEXT"); + + b.Property("Town") + .HasColumnType("TEXT"); + + b.Property("WebSite") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("RETAILERS", (string)null); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("Id"); + + b.Property("AlbumId") + .HasColumnType("INTEGER") + .HasColumnName("AlbumId"); + + b.Property("Duration") + .HasColumnType("INTEGER") + .HasColumnName("Duration"); + + b.Property("Number") + .HasColumnType("INTEGER") + .HasColumnName("Number"); + + b.Property("Purchased") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("Title"); + + b.HasKey("Id"); + + b.HasIndex("AlbumId"); + + b.ToTable("TRACKS", (string)null); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("Password"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.ToTable("USER", (string)null); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Reporting.ArtistStatistics", b => + { + b.Property("Albums") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Spend") + .HasColumnType("TEXT"); + + b.Property("Tracks") + .HasColumnType("INTEGER"); + + b.ToTable("ArtistStatistics"); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Reporting.GenreAlbum", b => + { + b.Property("Artist") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Genre") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("Purchased") + .HasColumnType("TEXT"); + + b.Property("Retailer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.ToTable("GenreAlbums"); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Reporting.GenreStatistics", b => + { + b.Property("Albums") + .HasColumnType("INTEGER"); + + b.Property("Artists") + .HasColumnType("INTEGER"); + + b.Property("Genre") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Spend") + .HasColumnType("TEXT"); + + b.Property("Tracks") + .HasColumnType("INTEGER"); + + b.ToTable("GenreStatistics"); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Reporting.MonthlySpend", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Month") + .HasColumnType("INTEGER"); + + b.Property("Spend") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.ToTable("MonthlySpend"); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Reporting.RetailerStatistics", b => + { + b.Property("Albums") + .HasColumnType("INTEGER"); + + b.Property("Artists") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Spend") + .HasColumnType("TEXT"); + + b.Property("Tracks") + .HasColumnType("INTEGER"); + + b.ToTable("RetailerStatistics"); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.Album", b => + { + b.HasOne("MusicCatalogue.Entities.Database.Artist", null) + .WithMany("Albums") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MusicCatalogue.Entities.Database.Genre", "Genre") + .WithMany() + .HasForeignKey("GenreId"); + + b.HasOne("MusicCatalogue.Entities.Database.Retailer", "Retailer") + .WithMany() + .HasForeignKey("RetailerId"); + + b.Navigation("Genre"); + + b.Navigation("Retailer"); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.Equipment", b => + { + b.HasOne("MusicCatalogue.Entities.Database.EquipmentType", "EquipmentType") + .WithMany() + .HasForeignKey("EquipmentTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MusicCatalogue.Entities.Database.Manufacturer", "Manufacturer") + .WithMany() + .HasForeignKey("ManufacturerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MusicCatalogue.Entities.Database.Retailer", "Retailer") + .WithMany() + .HasForeignKey("RetailerId"); + + b.Navigation("EquipmentType"); + + b.Navigation("Manufacturer"); + + b.Navigation("Retailer"); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.Track", b => + { + b.HasOne("MusicCatalogue.Entities.Database.Album", null) + .WithMany("Tracks") + .HasForeignKey("AlbumId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.Album", b => + { + b.Navigation("Tracks"); + }); + + modelBuilder.Entity("MusicCatalogue.Entities.Database.Artist", b => + { + b.Navigation("Albums"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/MusicCatalogue.Data/Migrations/20240516104534_GenreAlbums.cs b/src/MusicCatalogue.Data/Migrations/20240516104534_GenreAlbums.cs new file mode 100644 index 0000000..3727a8b --- /dev/null +++ b/src/MusicCatalogue.Data/Migrations/20240516104534_GenreAlbums.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MusicCatalogue.Data.Migrations +{ + /// + public partial class GenreAlbums : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "GenreAlbums", + columns: table => new + { + Artist = table.Column(type: "TEXT", nullable: false), + Title = table.Column(type: "TEXT", nullable: false), + Genre = table.Column(type: "TEXT", nullable: false), + Purchased = table.Column(type: "TEXT", nullable: false), + Price = table.Column(type: "TEXT", nullable: false), + Retailer = table.Column(type: "TEXT", nullable: false), + Id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GenreAlbums"); + } + } +} diff --git a/src/MusicCatalogue.Data/Migrations/MusicCatalogueDbContextModelSnapshot.cs b/src/MusicCatalogue.Data/Migrations/MusicCatalogueDbContextModelSnapshot.cs index a365b44..b66d1f0 100644 --- a/src/MusicCatalogue.Data/Migrations/MusicCatalogueDbContextModelSnapshot.cs +++ b/src/MusicCatalogue.Data/Migrations/MusicCatalogueDbContextModelSnapshot.cs @@ -1,6 +1,5 @@ // using System; -using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -10,7 +9,6 @@ namespace MusicCatalogue.Data.Migrations { - [ExcludeFromCodeCoverage] [DbContext(typeof(MusicCatalogueDbContext))] partial class MusicCatalogueDbContextModelSnapshot : ModelSnapshot { @@ -353,6 +351,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ArtistStatistics"); }); + modelBuilder.Entity("MusicCatalogue.Entities.Reporting.GenreAlbum", b => + { + b.Property("Artist") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Genre") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("Purchased") + .HasColumnType("TEXT"); + + b.Property("Retailer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.ToTable("GenreAlbums"); + }); + modelBuilder.Entity("MusicCatalogue.Entities.Reporting.GenreStatistics", b => { b.Property("Albums") diff --git a/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj b/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj index 9c01b02..4f0fe9c 100644 --- a/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj +++ b/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj @@ -1,11 +1,11 @@ - net7.0 + net8.0 enable enable MusicCatalogue.Data - 1.23.0.0 + 1.24.0.0 Dave Walker Copyright (c) Dave Walker 2023 Dave Walker @@ -17,7 +17,7 @@ https://github.com/davewalker5/MusicCatalogue MIT false - 1.23.0.0 + 1.24.0.0 @@ -31,16 +31,16 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + diff --git a/src/MusicCatalogue.Data/MusicCatalogueDbContext.cs b/src/MusicCatalogue.Data/MusicCatalogueDbContext.cs index 850ceea..0e0b9aa 100644 --- a/src/MusicCatalogue.Data/MusicCatalogueDbContext.cs +++ b/src/MusicCatalogue.Data/MusicCatalogueDbContext.cs @@ -22,6 +22,7 @@ public class MusicCatalogueDbContext : DbContext public virtual DbSet ArtistStatistics { get; set; } public virtual DbSet MonthlySpend { get; set; } public virtual DbSet RetailerStatistics { get; set; } + public virtual DbSet GenreAlbums { get; set; } public MusicCatalogueDbContext(DbContextOptions options) : base(options) { diff --git a/src/MusicCatalogue.Entities/Exceptions/ArtistInUseException.cs b/src/MusicCatalogue.Entities/Exceptions/ArtistInUseException.cs index a08966c..5a29c5a 100644 --- a/src/MusicCatalogue.Entities/Exceptions/ArtistInUseException.cs +++ b/src/MusicCatalogue.Entities/Exceptions/ArtistInUseException.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; namespace MusicCatalogue.Entities.Exceptions { @@ -18,14 +17,5 @@ public ArtistInUseException(string message) : base(message) public ArtistInUseException(string message, Exception inner) : base(message, inner) { } - - protected ArtistInUseException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) - { - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } diff --git a/src/MusicCatalogue.Entities/Exceptions/DuplicateOptionException.cs b/src/MusicCatalogue.Entities/Exceptions/DuplicateOptionException.cs index b2eaa3e..213e840 100644 --- a/src/MusicCatalogue.Entities/Exceptions/DuplicateOptionException.cs +++ b/src/MusicCatalogue.Entities/Exceptions/DuplicateOptionException.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; namespace MusicCatalogue.Entities.Exceptions { @@ -19,14 +18,5 @@ public DuplicateOptionException(string message) : base(message) public DuplicateOptionException(string message, Exception inner) : base(message, inner) { } - - protected DuplicateOptionException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) - { - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Exceptions/EquipmentTypeInUseException.cs b/src/MusicCatalogue.Entities/Exceptions/EquipmentTypeInUseException.cs index 32aaa60..3c88513 100644 --- a/src/MusicCatalogue.Entities/Exceptions/EquipmentTypeInUseException.cs +++ b/src/MusicCatalogue.Entities/Exceptions/EquipmentTypeInUseException.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; namespace MusicCatalogue.Entities.Exceptions { @@ -18,14 +17,5 @@ public EquipmentTypeInUseException(string message) : base(message) public EquipmentTypeInUseException(string message, Exception inner) : base(message, inner) { } - - protected EquipmentTypeInUseException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) - { - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } diff --git a/src/MusicCatalogue.Entities/Exceptions/GenreInUseException.cs b/src/MusicCatalogue.Entities/Exceptions/GenreInUseException.cs index 14a3f9d..58f8a66 100644 --- a/src/MusicCatalogue.Entities/Exceptions/GenreInUseException.cs +++ b/src/MusicCatalogue.Entities/Exceptions/GenreInUseException.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; namespace MusicCatalogue.Entities.Exceptions { @@ -18,14 +17,5 @@ public GenreInUseException(string message) : base(message) public GenreInUseException(string message, Exception inner) : base(message, inner) { } - - protected GenreInUseException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) - { - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Exceptions/InvalidRecordFormatException.cs b/src/MusicCatalogue.Entities/Exceptions/InvalidRecordFormatException.cs index 163382b..cb52364 100644 --- a/src/MusicCatalogue.Entities/Exceptions/InvalidRecordFormatException.cs +++ b/src/MusicCatalogue.Entities/Exceptions/InvalidRecordFormatException.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; namespace MusicCatalogue.Entities.Exceptions { @@ -18,15 +17,6 @@ public InvalidRecordFormatException(string message) : base(message) public InvalidRecordFormatException(string message, Exception inner) : base(message, inner) { } - - protected InvalidRecordFormatException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) - { - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } diff --git a/src/MusicCatalogue.Entities/Exceptions/MalformedCommandLineException.cs b/src/MusicCatalogue.Entities/Exceptions/MalformedCommandLineException.cs index 8338624..da0371f 100644 --- a/src/MusicCatalogue.Entities/Exceptions/MalformedCommandLineException.cs +++ b/src/MusicCatalogue.Entities/Exceptions/MalformedCommandLineException.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; namespace MusicCatalogue.Entities.Exceptions { @@ -18,14 +17,5 @@ public MalformedCommandLineException(string message) : base(message) public MalformedCommandLineException(string message, Exception inner) : base(message, inner) { } - - protected MalformedCommandLineException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) - { - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Exceptions/ManufacturerInUseException.cs b/src/MusicCatalogue.Entities/Exceptions/ManufacturerInUseException.cs index 4301ad4..07eb31c 100644 --- a/src/MusicCatalogue.Entities/Exceptions/ManufacturerInUseException.cs +++ b/src/MusicCatalogue.Entities/Exceptions/ManufacturerInUseException.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; namespace MusicCatalogue.Entities.Exceptions { @@ -19,14 +18,5 @@ public ManufacturerInUseException(string message) : base(message) public ManufacturerInUseException(string message, Exception inner) : base(message, inner) { } - - protected ManufacturerInUseException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) - { - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } diff --git a/src/MusicCatalogue.Entities/Exceptions/MultipleOperationsException.cs b/src/MusicCatalogue.Entities/Exceptions/MultipleOperationsException.cs index 20d777d..2054b0c 100644 --- a/src/MusicCatalogue.Entities/Exceptions/MultipleOperationsException.cs +++ b/src/MusicCatalogue.Entities/Exceptions/MultipleOperationsException.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; namespace MusicCatalogue.Entities.Exceptions { @@ -18,15 +17,6 @@ public MultipleOperationsException(string message) : base(message) public MultipleOperationsException(string message, Exception inner) : base(message, inner) { } - - protected MultipleOperationsException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) - { - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } diff --git a/src/MusicCatalogue.Entities/Exceptions/RetailerInUseException.cs b/src/MusicCatalogue.Entities/Exceptions/RetailerInUseException.cs index 5ad1d0b..788329e 100644 --- a/src/MusicCatalogue.Entities/Exceptions/RetailerInUseException.cs +++ b/src/MusicCatalogue.Entities/Exceptions/RetailerInUseException.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; namespace MusicCatalogue.Entities.Exceptions { @@ -19,14 +18,5 @@ public RetailerInUseException(string message) : base(message) public RetailerInUseException(string message, Exception inner) : base(message, inner) { } - - protected RetailerInUseException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) - { - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Exceptions/TooFewValuesException.cs b/src/MusicCatalogue.Entities/Exceptions/TooFewValuesException.cs index 8f58997..8ff78d3 100644 --- a/src/MusicCatalogue.Entities/Exceptions/TooFewValuesException.cs +++ b/src/MusicCatalogue.Entities/Exceptions/TooFewValuesException.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; namespace MusicCatalogue.Entities.Exceptions { @@ -18,14 +17,5 @@ public TooFewValuesException(string message) : base(message) public TooFewValuesException(string message, Exception inner) : base(message, inner) { } - - protected TooFewValuesException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) - { - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Exceptions/TooManyValuesException.cs b/src/MusicCatalogue.Entities/Exceptions/TooManyValuesException.cs index 35f3b3f..5ba87c4 100644 --- a/src/MusicCatalogue.Entities/Exceptions/TooManyValuesException.cs +++ b/src/MusicCatalogue.Entities/Exceptions/TooManyValuesException.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; namespace MusicCatalogue.Entities.Exceptions { @@ -23,14 +17,5 @@ public TooManyValuesException(string message) : base(message) public TooManyValuesException(string message, Exception inner) : base(message, inner) { } - - protected TooManyValuesException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) - { - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Exceptions/UnrecognisedCommandLineOptionException.cs b/src/MusicCatalogue.Entities/Exceptions/UnrecognisedCommandLineOptionException.cs index 9df0feb..f3d5c8a 100644 --- a/src/MusicCatalogue.Entities/Exceptions/UnrecognisedCommandLineOptionException.cs +++ b/src/MusicCatalogue.Entities/Exceptions/UnrecognisedCommandLineOptionException.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; namespace MusicCatalogue.Entities.Exceptions { @@ -18,14 +17,5 @@ public UnrecognisedCommandLineOptionException(string message) : base(message) public UnrecognisedCommandLineOptionException(string message, Exception inner) : base(message, inner) { } - - protected UnrecognisedCommandLineOptionException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) - { - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } diff --git a/src/MusicCatalogue.Entities/Interfaces/IGenreBasedReport.cs b/src/MusicCatalogue.Entities/Interfaces/IGenreBasedReport.cs new file mode 100644 index 0000000..cebc0c5 --- /dev/null +++ b/src/MusicCatalogue.Entities/Interfaces/IGenreBasedReport.cs @@ -0,0 +1,7 @@ +namespace MusicCatalogue.Entities.Interfaces +{ + public interface IGenreBasedReport where T : class + { + Task> GenerateReportAsync(int genreId, int pageNumber, int pageSize); + } +} diff --git a/src/MusicCatalogue.Entities/Interfaces/IMusicCatalogueFactory.cs b/src/MusicCatalogue.Entities/Interfaces/IMusicCatalogueFactory.cs index 4254254..e04fd1f 100644 --- a/src/MusicCatalogue.Entities/Interfaces/IMusicCatalogueFactory.cs +++ b/src/MusicCatalogue.Entities/Interfaces/IMusicCatalogueFactory.cs @@ -26,5 +26,6 @@ public interface IMusicCatalogueFactory IWishListBasedReport ArtistStatistics { get; } IWishListBasedReport MonthlySpend { get; } IWishListBasedReport RetailerStatistics { get; } + IGenreBasedReport GenreAlbums { get; } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj b/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj index 3b4092e..77e96bd 100644 --- a/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj +++ b/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj @@ -1,11 +1,11 @@  - net7.0 + net8.0 enable enable MusicCatalogue.Entities - 1.23.0.0 + 1.24.0.0 Dave Walker Copyright (c) Dave Walker 2023 Dave Walker @@ -17,11 +17,11 @@ https://github.com/davewalker5/MusicCatalogue MIT false - 1.23.0.0 + 1.24.0.0 - + diff --git a/src/MusicCatalogue.Entities/Reporting/GenreAlbum.cs b/src/MusicCatalogue.Entities/Reporting/GenreAlbum.cs new file mode 100644 index 0000000..8454d4c --- /dev/null +++ b/src/MusicCatalogue.Entities/Reporting/GenreAlbum.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore; +using MusicCatalogue.Entities.Attributes; +using System.Diagnostics.CodeAnalysis; + +namespace MusicCatalogue.Entities.Reporting +{ + [Keyless] + [ExcludeFromCodeCoverage] + public class GenreAlbum : ReportEntityBase + { + [Export("Artist", 1)] + public string Artist { get; set; } = ""; + + [Export("Title", 2)] + public string Title { get; set; } = ""; + + [Export("Genre", 3)] + public string Genre { get; set; } = ""; + + [Export("Released", 4)] + public int Released { get; set; } + + [Export("Purchased", 5)] + public DateTime Purchased { get; set; } + + [Export("Price", 6)] + public Decimal Price { get; set; } + + [Export("Retailer", 7)] + public string Retailer { get; set; } = ""; + } +} diff --git a/src/MusicCatalogue.Logic/Factory/MusicCatalogueFactory.cs b/src/MusicCatalogue.Logic/Factory/MusicCatalogueFactory.cs index 77dde8a..213a9ea 100644 --- a/src/MusicCatalogue.Logic/Factory/MusicCatalogueFactory.cs +++ b/src/MusicCatalogue.Logic/Factory/MusicCatalogueFactory.cs @@ -32,6 +32,7 @@ public class MusicCatalogueFactory : IMusicCatalogueFactory private readonly Lazy> _artistStatistics; private readonly Lazy> _monthlySpend; private readonly Lazy> _retailerStatistics; + private readonly Lazy> _genreAlbums; public DbContext Context { get; private set; } public IGenreManager Genres { get { return _genres.Value; } } @@ -63,6 +64,9 @@ public class MusicCatalogueFactory : IMusicCatalogueFactory [ExcludeFromCodeCoverage] public IWishListBasedReport RetailerStatistics { get { return _retailerStatistics.Value; } } + [ExcludeFromCodeCoverage] + public IGenreBasedReport GenreAlbums { get { return _genreAlbums.Value; } } + public MusicCatalogueFactory(MusicCatalogueDbContext context) { Context = context; @@ -86,6 +90,7 @@ public MusicCatalogueFactory(MusicCatalogueDbContext context) _artistStatistics = new Lazy>(() => new WishListBasedReport(context)); _monthlySpend = new Lazy>(() => new WishListBasedReport(context)); _retailerStatistics = new Lazy>(() => new WishListBasedReport(context)); + _genreAlbums = new Lazy>(() => new GenreBasedReport(context)); } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj b/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj index 9a33a9c..9f39ee9 100644 --- a/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj +++ b/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj @@ -1,11 +1,11 @@ - net7.0 + net8.0 enable enable MusicCatalogue.Logic - 1.23.0.0 + 1.24.0.0 Dave Walker Copyright (c) Dave Walker 2023 Dave Walker @@ -17,11 +17,12 @@ https://github.com/davewalker5/MusicCatalogue MIT false - 1.23.0.0 + 1.24.0.0 + @@ -29,18 +30,19 @@ + - + - - - - + + + + diff --git a/src/MusicCatalogue.Logic/Reporting/GenreBasedReport.cs b/src/MusicCatalogue.Logic/Reporting/GenreBasedReport.cs new file mode 100644 index 0000000..66c7651 --- /dev/null +++ b/src/MusicCatalogue.Logic/Reporting/GenreBasedReport.cs @@ -0,0 +1,54 @@ +using MusicCatalogue.Data; +using MusicCatalogue.Entities.Interfaces; +using MusicCatalogue.Entities.Reporting; +using System.Diagnostics.CodeAnalysis; + +namespace MusicCatalogue.Logic.Reporting +{ + [ExcludeFromCodeCoverage] + internal class GenreBasedReport : ReportManagerBase, IGenreBasedReport where T : ReportEntityBase + { + private const string GenreIdPlaceHoder = "$genreId"; + + internal GenreBasedReport(MusicCatalogueDbContext context) : base(context) + { + } + + /// + /// Generate a genre based report for reporting entity type T + /// + /// + /// + /// + /// + public async Task> GenerateReportAsync(int genreId, int pageNumber, int pageSize) + { + // SQL report files are named after the keyless entity type they map to with a .sql extension + var sqlFile = $"{typeof(T).Name}.sql"; + + // Load the SQL file and perform date range place-holder replacements + var query = ReadGenreSqlReportResource(sqlFile, genreId); + + // Run the query and return the results + var results = await GenerateReportAsync(query, pageNumber, pageSize); + return results; + } + + /// + /// Read the SQL report file for a sightings-based report with a wish list flag in it + /// + /// + /// + /// + private static string ReadGenreSqlReportResource(string reportFile, int genreId) + { + // Read and return the query, replacing the date range parameters + var query = ReadSqlResource(reportFile, new Dictionary + { + { GenreIdPlaceHoder, genreId.ToString() } + }); + + return query; + } + } +} diff --git a/src/MusicCatalogue.Logic/Sql/GenreAlbum.sql b/src/MusicCatalogue.Logic/Sql/GenreAlbum.sql new file mode 100644 index 0000000..52887ea --- /dev/null +++ b/src/MusicCatalogue.Logic/Sql/GenreAlbum.sql @@ -0,0 +1,16 @@ +SELECT RANK() OVER ( ORDER BY ar.Name, al.Title ) AS 'Id', + ar.Name AS 'Artist', + al.Title, + g.Name AS 'Genre', + IFNULL( al.Released, 0 ) AS 'Released', + IFNULL( DATE(al.Purchased), DATE('1900-01-01')) AS 'Purchased', + IFNULL( al.Price, 0.0 ) AS 'Price', + IFNULL( r.Name, '') AS 'Retailer' +FROM ALBUMS al +INNER JOIN GENRES g ON g.Id = al.GenreId +INNER JOIN ARTISTS ar ON ar.Id = al.ArtistId +LEFT OUTER JOIN RETAILERS r ON r.Id = al.RetailerId +WHERE g.Id = $genreId +AND IFNULL( al.IsWishListItem, 0 ) = 0 +ORDER BY ar.Name ASC, + al.Title ASC diff --git a/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj b/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj index 8848ecd..b3584da 100644 --- a/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj +++ b/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj @@ -2,10 +2,10 @@ Exe - net7.0 - 1.23.0.0 - 1.23.0.0 - 1.23.0 + net8.0 + 1.24.0.0 + 1.24.0.0 + 1.24.0 enable enable false @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/MusicCatalogue.Tests/MusicCatalogue.Tests.csproj b/src/MusicCatalogue.Tests/MusicCatalogue.Tests.csproj index f5508ba..667b669 100644 --- a/src/MusicCatalogue.Tests/MusicCatalogue.Tests.csproj +++ b/src/MusicCatalogue.Tests/MusicCatalogue.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable @@ -40,14 +40,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/music-catalogue-ui/components/componentPicker.js b/src/music-catalogue-ui/components/componentPicker.js index 97c5e96..f4fc354 100644 --- a/src/music-catalogue-ui/components/componentPicker.js +++ b/src/music-catalogue-ui/components/componentPicker.js @@ -9,6 +9,7 @@ import JobStatusReport from "./reports/jobStatusReport"; import AlbumPurchaseDetails from "./albums/albumPurchaseDetails"; import ArtistStatisticsReport from "./reports/artistStatisticsReport"; import MonthlySpendReport from "./reports/monthlySpendReport"; +import GenreAlbumsReport from "./reports/genreAlbumsReport"; import GenreList from "./genres/genreList"; import RetailerList from "./retailers/retailerList"; import RetailerDetails from "./retailers/retailerDetails"; @@ -175,6 +176,8 @@ const ComponentPicker = ({ context, navigate, logout }) => { return ; case pages.genreStatisticsReport: return ; + case pages.genreAlbumsReport: + return ; case pages.retailerStatisticsReport: return ; case pages.jobStatusReport: diff --git a/src/music-catalogue-ui/components/menuBar.js b/src/music-catalogue-ui/components/menuBar.js index 872dfa9..f85a2c6 100644 --- a/src/music-catalogue-ui/components/menuBar.js +++ b/src/music-catalogue-ui/components/menuBar.js @@ -42,6 +42,9 @@ const MenuBar = ({ navigate, logout }) => { > Retailer Statistics + navigate({ page: pages.genreAlbumsReport })}> + Albums By Genre + navigate({ page: pages.jobStatusReport })}> Job Status diff --git a/src/music-catalogue-ui/components/reports/genreAlbumRow.js b/src/music-catalogue-ui/components/reports/genreAlbumRow.js new file mode 100644 index 0000000..a89cf92 --- /dev/null +++ b/src/music-catalogue-ui/components/reports/genreAlbumRow.js @@ -0,0 +1,38 @@ +import CurrencyFormatter from "../common/currencyFormatter"; +import DateFormatter from "../common/dateFormatter"; + +/** + * Component to render a row containing the details of a single album in a + * genre + * @param {*} record + * @returns + */ +const GenreAlbumRow = ({ record }) => { + const purchaseDate = new Date(record.purchased); + console.log(purchaseDate.getFullYear()); + return ( + + {record.artist} + {record.title} + {record.genre} + {record.released > 0 ? {record.released} : } + {purchaseDate > 1900 ? ( + + + + ) : ( + + )} + {record.price > 0 ? ( + + + + ) : ( + + )} + {record.retailer} + + ); +}; + +export default GenreAlbumRow; diff --git a/src/music-catalogue-ui/components/reports/genreAlbumsReport.js b/src/music-catalogue-ui/components/reports/genreAlbumsReport.js new file mode 100644 index 0000000..3f75a1e --- /dev/null +++ b/src/music-catalogue-ui/components/reports/genreAlbumsReport.js @@ -0,0 +1,146 @@ +import React, { useCallback, useState } from "react"; +import styles from "./reports.module.css"; +import "react-datepicker/dist/react-datepicker.css"; +import { apiGenreAlbumsReport } from "@/helpers/api/apiReports"; +import GenreAlbumRow from "./genreAlbumRow"; +import ReportExportControls from "./reportExportControls"; +import { apiRequestGenreAlbumsExport } from "@/helpers/api/apiDataExchange"; +import GenreSelector from "../genres/genreSelector"; + +/** + * Component to display the albums by genre report page and its results + * @param {*} logout + * @returns + */ +const GenreAlbumsReport = ({ logout }) => { + const [genre, setGenre] = useState(null); + const [records, setRecords] = useState(null); + const [message, setMessage] = useState(""); + const [error, setError] = useState(""); + + // Callback to request the albums by genre report from the API + const getReportCallback = useCallback( + async (e) => { + // Prevent the default action associated with the click event + e.preventDefault(); + + // Clear pre-existing errors and messages + setMessage(""); + setError(""); + + // If there's a genre selected, request the report + if (genre != null) { + const fetchedRecords = await apiGenreAlbumsReport(genre.id, logout); + setRecords(fetchedRecords); + } + }, + [genre, logout] + ); + + /* Callback to export the report */ + const exportReportCallback = useCallback( + async (e, fileName) => { + // Prevent the default action associated with the click event + e.preventDefault(); + + // Clear pre-existing errors and messages + setMessage(""); + setError(""); + + // Set the wishlist flag from the drop-down selection + const genreId = genre != null ? genre.id : null; + + // Request an export via the API + const isOK = await apiRequestGenreAlbumsExport(fileName, genreId, logout); + + // If all's well, display a confirmation message. Otherwise, show an error + if (isOK) { + setMessage( + `A background export of the genre statistics report to ${fileName} has been requested` + ); + } else { + setError( + "An error occurred requesting an export of the genre statistics report" + ); + } + }, + [genre, logout] + ); + + return ( + <> +
+
Albums by Genre Report
+
+
+
+ {message != "" ? ( +
{message}
+ ) : ( + <> + )} + {error != "" ? ( +
{error}
+ ) : ( + <> + )} +
+
+
+
+ +
+
+
+ +
+
+
+ +
+ {records != null ? ( + + ) : ( + <> + )} +
+
+
+
+
+ + + + + + + + + + + + + {records != null && ( + + {records.map((r) => ( + + ))} + + )} +
ArtistTitleGenreReleasedPurchasedPriceRetailer
+ + ); +}; + +export default GenreAlbumsReport; diff --git a/src/music-catalogue-ui/components/reports/reports.module.css b/src/music-catalogue-ui/components/reports/reports.module.css index c2f3a2f..e4b6fef 100644 --- a/src/music-catalogue-ui/components/reports/reports.module.css +++ b/src/music-catalogue-ui/components/reports/reports.module.css @@ -44,3 +44,7 @@ color: red; text-align: center; } + +.genreSelector { + width: 200px; +} diff --git a/src/music-catalogue-ui/config.json b/src/music-catalogue-ui/config.json index 0887cac..c6e3c8c 100644 --- a/src/music-catalogue-ui/config.json +++ b/src/music-catalogue-ui/config.json @@ -1,6 +1,6 @@ { "api": { - "baseUrl": "http://localhost:8098" + "baseUrl": "http://localhost:5294" }, "region": { "locale": "en-GB", diff --git a/src/music-catalogue-ui/helpers/api/apiDataExchange.js b/src/music-catalogue-ui/helpers/api/apiDataExchange.js index dea75c1..573ef53 100644 --- a/src/music-catalogue-ui/helpers/api/apiDataExchange.js +++ b/src/music-catalogue-ui/helpers/api/apiDataExchange.js @@ -181,6 +181,35 @@ const apiRequestRetailerStatisticsExport = async ( return response.ok; }; +/** + * Request an export of the albums by genre report + * @param {*} fileName + * @param {*} genreId + * @param {*} logout + */ +const apiRequestGenreAlbumsExport = async (fileName, genreId, logout) => { + // Create a JSON body containing the file name to export to + const body = JSON.stringify({ + fileName: fileName, + genreId: genreId, + }); + + // Call the API to request the export + const url = `${config.api.baseUrl}/export/genrealbums`; + const response = await fetch(url, { + method: "POST", + headers: apiGetPostHeaders(), + body: body, + }); + + if (response.status == 401) { + // Unauthorized so the token's likely expired - force a login + logout(); + } + + return response.ok; +}; + export { apiRequestCatalogueExport, apiRequestEquipmentExport, @@ -188,4 +217,5 @@ export { apiRequestGenreStatisticsExport, apiRequestMonthlySpendingExport, apiRequestRetailerStatisticsExport, + apiRequestGenreAlbumsExport, }; diff --git a/src/music-catalogue-ui/helpers/api/apiReports.js b/src/music-catalogue-ui/helpers/api/apiReports.js index 29ae1a6..cd4ceaa 100644 --- a/src/music-catalogue-ui/helpers/api/apiReports.js +++ b/src/music-catalogue-ui/helpers/api/apiReports.js @@ -125,10 +125,30 @@ const apiRetailerStatisticsReport = async (wishlist, logout) => { return records; }; +/** + * Call the API to retrieve the albums by genre report + * @param {*} genreId + * @param {*} logout + */ +const apiGenreAlbumsReport = async (genreId, logout) => { + // Construct the route + const url = `${config.api.baseUrl}/reports/genreAlbums/${genreId}`; + + // Call the API to get content for the report + const response = await fetch(url, { + method: "GET", + headers: apiGetHeaders(), + }); + + const records = await apiReadResponseData(response, logout); + return records; +}; + export { apiJobStatusReport, apiGenreStatisticsReport, apiArtistStatisticsReport, apiMonthlySpendReport, apiRetailerStatisticsReport, + apiGenreAlbumsReport, }; diff --git a/src/music-catalogue-ui/helpers/navigation.js b/src/music-catalogue-ui/helpers/navigation.js index 4689e45..68b092b 100644 --- a/src/music-catalogue-ui/helpers/navigation.js +++ b/src/music-catalogue-ui/helpers/navigation.js @@ -23,6 +23,7 @@ const pages = { export: "Export", artistStatisticsReport: "ArtistStatisticsReport", genreStatisticsReport: "GenreStatisticsReport", + genreAlbumsReport: "GenreAlbumsReport", jobStatusReport: "JobStatusReport", monthlySpendReport: "MonthlySpendReport", retailerStatisticsReport: "RetailerStatisticsReport",