Skip to content

Commit

Permalink
Add the ability to add new retailers with full details
Browse files Browse the repository at this point in the history
  • Loading branch information
davewalker5 committed Nov 22, 2023
1 parent 1c6e7bf commit debb5b5
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 53 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ MusicCatalogue.LookupTool --lookup "John Coltrane" "Blue Train" catalogue

<img src="diagrams/purchase-details.png" alt="Purchase Details" width="600">

- Note that retailers must be added using the retailer list and retailer editing page (see below) before they will appear in the drop-down on the purchase details page
- Clicking anywhere else on a row opens the track list for the album shown in that row:

<img src="diagrams/track-list.png" alt="Track List" width="600">
Expand Down Expand Up @@ -217,6 +218,9 @@ MusicCatalogue.LookupTool --lookup "John Coltrane" "Blue Train" catalogue

<img src="diagrams/retailer-list.png" alt="Retailers List" width="600">

- Clicking on the trash can icon in a row will prompt for confirmation and then attempt to delete the retailer on the selected row
- Retailers that are currently "in use" (associated with an album) cannot be deleted and attempting to delete them will result in an error message being displayed
- Clicking on the "Add" button opens the retailer details editing page (see below) to add a new retailer
- Clicking on the edit icon in a row navigates to the retailer details editing page for that retailer (see below)
- Clicking on a row in the table navigates to the details viewing page for that retailer:

Expand Down
Binary file modified diagrams/purchase-details.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified diagrams/retailer-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docker/ui/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM node:20-alpine
COPY musiccatalogue.ui-1.23.0.0 /opt/musiccatalogue.ui-1.23.0.0
WORKDIR /opt/musiccatalogue.ui-1.23.0.0
COPY musiccatalogue.ui-1.24.0.0 /opt/musiccatalogue.ui-1.24.0.0
WORKDIR /opt/musiccatalogue.ui-1.24.0.0
RUN npm install
RUN npm run build
ENTRYPOINT [ "npm", "start" ]
61 changes: 36 additions & 25 deletions src/music-catalogue-ui/components/albumPurchaseDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import pages from "../helpers/navigation";
import { apiCreateRetailer } from "@/helpers/apiRetailers";
import { apiSetAlbumPurchaseDetails } from "@/helpers/apiAlbums";
import FormInputField from "./formInputField";
import Select from "react-select";
import useRetailers from "@/hooks/useRetailers";

/**
* Form to set the album purchase details for an album
Expand All @@ -17,16 +19,18 @@ import FormInputField from "./formInputField";
* @param {*} logout
*/
const AlbumPurchaseDetails = ({ artist, album, navigate, logout }) => {
const { retailers: retailers, setRetailers } = useRetailers(logout);

// Get the retailer name and purchase date from the album
const initialRetailerName =
album["retailer"] != null ? album["retailer"]["name"] : "";
const initialRetailerId =
album["retailer"] != null ? album["retailer"]["id"] : null;
const initialPurchaseDate =
album.purchased != null ? new Date(album.purchased) : new Date();

// Set up state
const [purchaseDate, setPurchaseDate] = useState(initialPurchaseDate);
const [price, setPrice] = useState(album.price);
const [retailerName, setRetailerName] = useState(initialRetailerName);
const [retailerId, setRetailerId] = useState(initialRetailerId);
const [errorMessage, setErrorMessage] = useState("");

/* Callback to set album purchase details */
Expand All @@ -35,20 +39,7 @@ const AlbumPurchaseDetails = ({ artist, album, navigate, logout }) => {
// Prevent the default action associated with the click event
e.preventDefault();

// See if we have a retailer name. If so, create/retrieve the retailer and
// capture the retailer ID
var retailerId = null;
if (retailerName != "") {
const retailer = await apiCreateRetailer(retailerName, logout);
if (retailer != null) {
retailerId = retailer.id;
} else {
setErrorMessage(`Error creating retailer "${retailerName}"`);
return;
}
}

// Construct the remaining values to be passed to the API
// Construct the values to be passed to the API
const updatedPurchaseDate =
album.isWishListItem == true ? null : purchaseDate;
const updatedPrice = price == undefined ? null : price;
Expand All @@ -58,7 +49,7 @@ const AlbumPurchaseDetails = ({ artist, album, navigate, logout }) => {
album,
updatedPurchaseDate,
updatedPrice,
retailerId,
retailerId.value,
logout
);

Expand All @@ -75,9 +66,27 @@ const AlbumPurchaseDetails = ({ artist, album, navigate, logout }) => {
setErrorMessage("Error updating the album purchase details");
}
},
[artist, album, purchaseDate, price, retailerName, logout, navigate]
[artist, album, price, purchaseDate, retailerId, logout, navigate]
);

// Construct the options for the retailer drop-down
let options = [];
for (let i = 0; i < retailers.length; i++) {
options = [
...options,
{ value: retailers[i].id, label: retailers[i].name },
];
}

// Identify the default retailer for the drop-down
let defaultValue = null;
if (retailerId != null) {
const retailer = retailers.find((x) => x.id == retailerId);
if (retailer != null) {
defaultValue = retailer.name;
}
}

return (
<>
<div className="row mb-2 pageTitle">
Expand Down Expand Up @@ -121,12 +130,14 @@ const AlbumPurchaseDetails = ({ artist, album, navigate, logout }) => {
/>
</div>
</div>
<FormInputField
label="Retailer Name"
name="retailer"
value={retailerName}
setValue={setRetailerName}
/>
<div className="form-group mt-3">
<label className={styles.purchaseDetailsFormLabel}>
Retailer
</label>
<div>
<Select onChange={setRetailerId} options={options} />
</div>
</div>
<div className="d-grid gap-2 mt-3">
<span className={styles.purchaseDetailsError}>
{errorMessage}
Expand Down
59 changes: 59 additions & 0 deletions src/music-catalogue-ui/components/deleteRetailerActionIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useCallback } from "react";
import { apiDeleteRetailer, apiFetchRetailers } from "@/helpers/apiRetailers";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";

/**
* Icon and associated action to delete a retailer
* @param {*} retailer
* @param {*} logout
* @param {*} clearError
* @param {*} setError
* @param {*} setRetailers
* @returns
*/
const DeleteRetailerActionIcon = ({
retailer,
logout,
clearError,
setError,
setRetailers,
}) => {
/* Callback to prompt for confirmation and delete a retailer */
const confirmDeleteRetailer = useCallback(
async (e, album) => {
// Prevent the default action associated with the click event
e.preventDefault();

// Clear any pre-existing error messages
clearError();

// Show a confirmation message and get the user response
const message = `This will delete the retailer "${retailer.name}" - click OK to confirm`;
const result = confirm(message);

// If they've confirmed the deletion ...
if (result) {
// ... delete the retailer and confirm the API call was successful
const result = await apiDeleteRetailer(retailer.id, logout);
if (result) {
// Successful, so refresh the retailer list
const fetchedRetailers = await apiFetchRetailers(logout);
setRetailers(fetchedRetailers);
} else {
setError(`An error occurred deleting retailer "${retailer.name}"`);
}
}
},
[retailer, logout, clearError, setError, setRetailers]
);

return (
<FontAwesomeIcon
icon={faTrashAlt}
onClick={(e) => confirmDeleteRetailer(e, retailer)}
/>
);
};

export default DeleteRetailerActionIcon;
55 changes: 38 additions & 17 deletions src/music-catalogue-ui/components/retailerEditor.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import styles from "./retailerEditor.module.css";
import pages from "../helpers/navigation";
import { useState, useCallback } from "react";
import { apiUpdateRetailer } from "@/helpers/apiRetailers";
import { apiCreateRetailer, apiUpdateRetailer } from "@/helpers/apiRetailers";
import FormInputField from "./formInputField";
import { geocodeAddress } from "@/helpers/geocoder";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
Expand Down Expand Up @@ -54,25 +54,46 @@ const RetailerEditor = ({ retailer, navigate, logout }) => {
// Clear pre-existing errors
setError("");

// Update the retailer
const updatedRetailer = await apiUpdateRetailer(
retailer.id,
name,
address1,
address2,
town,
county,
postCode,
country,
webSite,
latitude,
longitude,
logout
);
// Either add or update the retailer, depending on whether they currently
// have an ID
let updatedRetailer = null;
if (retailer.id <= 0) {
// Invalid ID, so create a new retailer
updatedRetailer = await apiCreateRetailer(
name,
address1,
address2,
town,
county,
postCode,
country,
webSite,
latitude,
longitude,
logout
);
} else {
// Has a valid ID, so update an existing retailer
updatedRetailer = await apiUpdateRetailer(
retailer.id,
name,
address1,
address2,
town,
county,
postCode,
country,
webSite,
latitude,
longitude,
logout
);
}

// If all's well, display a confirmation message. Otherwise, show an error
if (updatedRetailer == null) {
setError("An error occurred updating the retailer details");
const action = retailer.Id <= 0 ? "adding" : "updating";
setError(`An error occurred ${action} the retailer`);
} else {
navigate({ page: pages.retailers });
}
Expand Down
54 changes: 53 additions & 1 deletion src/music-catalogue-ui/components/retailerList.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import styles from "./retailerList.module.css";
import pages from "@/helpers/navigation";
import useRetailers from "@/hooks/useRetailers";
import RetailerRow from "./retailerRow";
import { useCallback, useState } from "react";

/**
* Component to render a table listing all the retailers in the catalogue
Expand All @@ -9,12 +12,28 @@ import RetailerRow from "./retailerRow";
*/
const RetailerList = ({ navigate, logout }) => {
const { retailers: retailers, setRetailers } = useRetailers(logout);
const [errorMessage, setError] = useState(null);

// Callback passed to child components to set the error message
const setErrorCallback = useCallback((message) => {
setError(message);
}, []);

// Callback passed to child components to clear the error message
const clearErrorCallback = useCallback(() => {
setError(null);
}, []);

return (
<>
<div className="row mb-2 pageTitle">
<h5 className="themeFontColor text-center">Retailers</h5>
</div>
{errorMessage != null ? (
<div className={styles.retailerListErrorContainer}>{errorMessage}</div>
) : (
<></>
)}
<table className="table table-hover">
<thead>
<tr>
Expand All @@ -29,11 +48,44 @@ const RetailerList = ({ navigate, logout }) => {
{retailers != null && (
<tbody>
{retailers.map((r) => (
<RetailerRow key={r.id} retailer={r} navigate={navigate} />
<RetailerRow
key={r.id}
retailer={r}
navigate={navigate}
logout={logout}
setRetailers={setRetailers}
clearError={clearErrorCallback}
setError={setErrorCallback}
/>
))}
</tbody>
)}
</table>
<div className={styles.retailerListButton}>
<button
className="btn btn-primary"
onClick={() =>
navigate({
page: pages.retailerEditor,
retailer: {
id: 0,
name: null,
address1: null,
address2: null,
town: null,
county: null,
postCode: null,
country: null,
webSite: null,
latitude: null,
longitude: null,
},
})
}
>
Add
</button>
</div>
</>
);
};
Expand Down
12 changes: 12 additions & 0 deletions src/music-catalogue-ui/components/retailerList.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.retailerListErrorContainer {
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
font-weight: bold;
color: red;
}

.retailerListButton {
margin-left: 10px;
float: right;
}
Loading

0 comments on commit debb5b5

Please sign in to comment.