From dbc51aa39e3656e5d77e6d02eb8cf2093a5a2efc Mon Sep 17 00:00:00 2001 From: Dave Walker Date: Tue, 20 Aug 2024 10:20:44 +0100 Subject: [PATCH] Implement albums by genre report --- docker/api/Dockerfile | 4 +- docker/ui/Dockerfile | 4 +- .../Controllers/ExportController.cs | 17 +- .../Entities/GenreAlbumsExportWorkItem.cs | 8 + .../MusicCatalogue.Api.csproj | 6 +- src/MusicCatalogue.Api/Program.cs | 4 + .../Services/GenreAlbumsExportService.cs | 45 ++++++ .../Exceptions/ArtistInUseException.cs | 5 - .../Exceptions/DuplicateOptionException.cs | 5 - .../Exceptions/EquipmentTypeInUseException.cs | 5 - .../Exceptions/GenreInUseException.cs | 5 - .../InvalidRecordFormatException.cs | 5 - .../MalformedCommandLineException.cs | 1 - .../Exceptions/ManufacturerInUseException.cs | 5 - .../Exceptions/MultipleOperationsException.cs | 5 - .../Exceptions/RetailerInUseException.cs | 5 - .../Exceptions/TooFewValuesException.cs | 5 - .../Exceptions/TooManyValuesException.cs | 12 +- .../UnrecognisedCommandLineOptionException.cs | 5 - .../Reporting/GenreAlbum.cs | 9 +- src/MusicCatalogue.Logic/Sql/GenreAlbum.sql | 30 ++-- .../components/componentPicker.js | 3 + src/music-catalogue-ui/components/menuBar.js | 3 + .../components/reports/genreAlbumRow.js | 38 +++++ .../components/reports/genreAlbumsReport.js | 146 ++++++++++++++++++ .../components/reports/reports.module.css | 4 + src/music-catalogue-ui/config.json | 2 +- .../helpers/api/apiDataExchange.js | 30 ++++ .../helpers/api/apiReports.js | 20 +++ src/music-catalogue-ui/helpers/navigation.js | 1 + 30 files changed, 349 insertions(+), 88 deletions(-) create mode 100644 src/MusicCatalogue.Api/Entities/GenreAlbumsExportWorkItem.cs create mode 100644 src/MusicCatalogue.Api/Services/GenreAlbumsExportService.cs create mode 100644 src/music-catalogue-ui/components/reports/genreAlbumRow.js create mode 100644 src/music-catalogue-ui/components/reports/genreAlbumsReport.js 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/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 827615d..057532d 100644 --- a/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj +++ b/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj @@ -2,9 +2,9 @@ net8.0 - 1.26.0.0 - 1.26.0.0 - 1.26.0 + 1.30.0.0 + 1.30.0.0 + 1.30.0 enable enable 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.Entities/Exceptions/ArtistInUseException.cs b/src/MusicCatalogue.Entities/Exceptions/ArtistInUseException.cs index 7c8f556..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,9 +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) - { - } } } diff --git a/src/MusicCatalogue.Entities/Exceptions/DuplicateOptionException.cs b/src/MusicCatalogue.Entities/Exceptions/DuplicateOptionException.cs index 56ed91b..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,9 +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) - { - } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Exceptions/EquipmentTypeInUseException.cs b/src/MusicCatalogue.Entities/Exceptions/EquipmentTypeInUseException.cs index 0f13f68..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,9 +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) - { - } } } diff --git a/src/MusicCatalogue.Entities/Exceptions/GenreInUseException.cs b/src/MusicCatalogue.Entities/Exceptions/GenreInUseException.cs index 70b7e8c..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,9 +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) - { - } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Exceptions/InvalidRecordFormatException.cs b/src/MusicCatalogue.Entities/Exceptions/InvalidRecordFormatException.cs index 5f7d3b1..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,10 +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) - { - } } } diff --git a/src/MusicCatalogue.Entities/Exceptions/MalformedCommandLineException.cs b/src/MusicCatalogue.Entities/Exceptions/MalformedCommandLineException.cs index 3e28a8e..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 { diff --git a/src/MusicCatalogue.Entities/Exceptions/ManufacturerInUseException.cs b/src/MusicCatalogue.Entities/Exceptions/ManufacturerInUseException.cs index 794ad8b..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,9 +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) - { - } } } diff --git a/src/MusicCatalogue.Entities/Exceptions/MultipleOperationsException.cs b/src/MusicCatalogue.Entities/Exceptions/MultipleOperationsException.cs index 684ba94..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,10 +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) - { - } } } diff --git a/src/MusicCatalogue.Entities/Exceptions/RetailerInUseException.cs b/src/MusicCatalogue.Entities/Exceptions/RetailerInUseException.cs index 9e0f484..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,9 +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) - { - } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Exceptions/TooFewValuesException.cs b/src/MusicCatalogue.Entities/Exceptions/TooFewValuesException.cs index 81a31ef..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,9 +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) - { - } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Exceptions/TooManyValuesException.cs b/src/MusicCatalogue.Entities/Exceptions/TooManyValuesException.cs index 8486564..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,9 +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) - { - } } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Exceptions/UnrecognisedCommandLineOptionException.cs b/src/MusicCatalogue.Entities/Exceptions/UnrecognisedCommandLineOptionException.cs index 06efa7c..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,9 +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) - { - } } } diff --git a/src/MusicCatalogue.Entities/Reporting/GenreAlbum.cs b/src/MusicCatalogue.Entities/Reporting/GenreAlbum.cs index 7f559f6..8454d4c 100644 --- a/src/MusicCatalogue.Entities/Reporting/GenreAlbum.cs +++ b/src/MusicCatalogue.Entities/Reporting/GenreAlbum.cs @@ -17,13 +17,16 @@ public class GenreAlbum : ReportEntityBase [Export("Genre", 3)] public string Genre { get; set; } = ""; - [Export("Purchased", 4)] + [Export("Released", 4)] + public int Released { get; set; } + + [Export("Purchased", 5)] public DateTime Purchased { get; set; } - [Export("Price", 5)] + [Export("Price", 6)] public Decimal Price { get; set; } - [Export("Retailer", 6)] + [Export("Retailer", 7)] public string Retailer { get; set; } = ""; } } diff --git a/src/MusicCatalogue.Logic/Sql/GenreAlbum.sql b/src/MusicCatalogue.Logic/Sql/GenreAlbum.sql index af3208d..52887ea 100644 --- a/src/MusicCatalogue.Logic/Sql/GenreAlbum.sql +++ b/src/MusicCatalogue.Logic/Sql/GenreAlbum.sql @@ -1,14 +1,16 @@ -SELECT RANK() OVER ( ORDER BY ar.Name, al.Title ) AS 'Id', - ar.Name AS 'Artist', - al.Title, g.Name AS 'Genre', - IFNULL( DATE(al.Purchased), DATE('1900-01-01')) AS 'Purchased', - IFNULL( al.Price, 0 ) AS 'Price', - 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 -INNER 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 +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/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",