diff --git a/README.md b/README.md index 2de8b62..11e0fb3 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,7 @@ To be implemented in a future release. ### Equipment Types - To view a list of equipment types in the database, click on the "Equipment > Equipment Types" menu item -- A page listing the equpiment types in the database is displayed: +- A page listing the equipment types in the database is displayed: Equipment Types List @@ -281,7 +281,17 @@ To be implemented in a future release. ### Manufacturers -To be implemented in a future release. +- To view a list of manufacturers in the database, click on the "Equipment > Manufacturers" menu item +- A page listing the manufacturers in the database is displayed: + +Equipment Types List + +- Clicking on the trash can icon in a row will prompt for confirmation and then attempt to delete the manufacturer on the selected row +- Manufacturers that are currently "in use" (associated with an item of equpiment) cannot be deleted and attempting to delete them will result in an error message being displayed +- Clicking on the "Add" button opens the manufacturer editing page (see below) to add a new manufacturer +- Clicking on the edit icon in a row navigates to the manufacturer editing page for that manufacturer (see below) + +Equipment Type Editor #### The Retailers List diff --git a/diagrams/manufacturer-editor.png b/diagrams/manufacturer-editor.png new file mode 100644 index 0000000..de9297f Binary files /dev/null and b/diagrams/manufacturer-editor.png differ diff --git a/diagrams/manufacturer-list.png b/diagrams/manufacturer-list.png new file mode 100644 index 0000000..d0eafd2 Binary files /dev/null and b/diagrams/manufacturer-list.png differ diff --git a/src/music-catalogue-ui/components/app.js b/src/music-catalogue-ui/components/app.js index ac3037d..77db2c1 100644 --- a/src/music-catalogue-ui/components/app.js +++ b/src/music-catalogue-ui/components/app.js @@ -15,14 +15,18 @@ const defaultContext = { // Current page page: pages.artists, - // Artist, album, track and retailer context + // Music catalogue artist: null, album: null, track: null, + genre: null, retailer: null, + // Equipment registry + equipmentType: null, + manufacturer: null, + // Data retrieval/filering criteria - genre: null, filter: "A", isWishList: false, }; @@ -43,6 +47,7 @@ const App = () => { retailer = null, genre = null, equipmentType = null, + manufacturer = null, filter = "A", isWishList = false, } = {}) => { @@ -55,6 +60,7 @@ const App = () => { retailer: typeof retailer != "undefined" ? retailer : null, genre: typeof genre != "undefined" ? genre : null, equipmentType: typeof equipmentType != "undefined" ? equipmentType : null, + manufacturer: typeof manufacturer != "undefined" ? manufacturer : null, filter: typeof filter != "undefined" ? filter : "A", isWishList: typeof isWishList != "undefined" ? isWishList : false, }; diff --git a/src/music-catalogue-ui/components/componentPicker.js b/src/music-catalogue-ui/components/componentPicker.js index 1f79f69..df4a0d6 100644 --- a/src/music-catalogue-ui/components/componentPicker.js +++ b/src/music-catalogue-ui/components/componentPicker.js @@ -19,6 +19,8 @@ import ArtistEditor from "./artists/artistEditor"; import RetailerStatisticsReport from "./reports/retailerStatisticsReport"; import EquipmentTypeList from "./equipmentTypes/equipmentTypeList"; import EquipmentTypeEditor from "./equipmentTypes/equipmentTypeEditor"; +import ManufacturerList from "./manufacturers/manufacturerList"; +import ManufacturerEditor from "./manufacturers/manufacturerEditor"; /** * Component using the current context to select and render the current page @@ -129,6 +131,16 @@ const ComponentPicker = ({ context, navigate, logout }) => { logout={logout} /> ); + case pages.manufacturers: + return ; + case pages.manufacturerEditor: + return ( + + ); case pages.export: return ; case pages.artistStatisticsReport: diff --git a/src/music-catalogue-ui/components/equipmentTypes/equipmentTypeEditor.js b/src/music-catalogue-ui/components/equipmentTypes/equipmentTypeEditor.js index 8bef8e1..75e0429 100644 --- a/src/music-catalogue-ui/components/equipmentTypes/equipmentTypeEditor.js +++ b/src/music-catalogue-ui/components/equipmentTypes/equipmentTypeEditor.js @@ -43,7 +43,7 @@ const EquipmentTypeEditor = ({ equipmentType, navigate, logout }) => { ); } - // Go back to the artist list, which should reflect the updated details + // Go back to the equipment type list, which should reflect the updated details navigate({ page: pages.equipmentTypes, }); diff --git a/src/music-catalogue-ui/components/manufacturers/deleteManufacturerActionIcon.js b/src/music-catalogue-ui/components/manufacturers/deleteManufacturerActionIcon.js new file mode 100644 index 0000000..cb8524e --- /dev/null +++ b/src/music-catalogue-ui/components/manufacturers/deleteManufacturerActionIcon.js @@ -0,0 +1,59 @@ +import { useCallback } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; +import { + apiDeleteManufacturer, + apiFetchManufacturers, +} from "@/helpers/api/apiManufacturers"; + +/** + * Icon and associated action to delete a manufacturer + * @param {*} manufacturer + * @param {*} logout + * @param {*} setManufacturers + * @param {*} setError + * @returns + */ +const DeleteManufacturerActionIcon = ({ + manufacturer, + logout, + setManufacturers, + setError, +}) => { + /* Callback to prompt for confirmation and delete amanufacturer */ + const confirmDeleteManufacturer = useCallback( + async (e) => { + // Prevent the default action associated with the click event + e.preventDefault(); + + // Show a confirmation message and get the user response + const message = `This will delete the manufacturer "${manufacturer.name}" - click OK to confirm`; + const result = confirm(message); + + // If they've confirmed the deletion ... + if (result) { + // ... delete the manufacturer and confirm the API call was successful + try { + const result = await apiDeleteManufacturer(manufacturer.id, logout); + if (result) { + // Successful, so refresh the manufacturer list + const fetchedManufacturers = await apiFetchManufacturers(logout); + setManufacturers(fetchedManufacturers); + } + } catch (ex) { + setError(`Error deleting the equipment type: ${ex.message}`); + } + } + }, + [manufacturer, logout, setManufacturers, setError] + ); + + return ( + confirmDeleteManufacturer(e)} + /> + ); +}; + +export default DeleteManufacturerActionIcon; diff --git a/src/music-catalogue-ui/components/manufacturers/manufacturerEditor.js b/src/music-catalogue-ui/components/manufacturers/manufacturerEditor.js new file mode 100644 index 0000000..6543a55 --- /dev/null +++ b/src/music-catalogue-ui/components/manufacturers/manufacturerEditor.js @@ -0,0 +1,113 @@ +import styles from "./manufacturerEditor.module.css"; +import pages from "@/helpers/navigation"; +import FormInputField from "../common/formInputField"; +import { useState, useCallback } from "react"; +import { + apiCreateManufacturer, + apiUpdateManufacturer, +} from "@/helpers/api/apiManufacturers"; + +/** + * Component to render the manufacturer editor + * @param {*} manufacturer + * @param {*} navigate + * @param {*} logout + */ +const EquipmentTypeEditor = ({ manufacturer, navigate, logout }) => { + // Set up state + const initialName = manufacturer != null ? manufacturer.name : null; + const [name, setName] = useState(initialName); + const [error, setError] = useState(""); + + const saveManufacturer = useCallback( + async (e) => { + // Prevent the default action associated with the click event + e.preventDefault(); + + // Clear pre-existing errors + setError(""); + + try { + // Either add or update the manufacturer, depending on whether there's an + // existing manufacturer or not + let updatedEquipmentType = null; + if (manufacturer == null) { + // Create the manufacturer + updatedEquipmentType = await apiCreateManufacturer(name, logout); + } else { + // Update the existing manufacturer + updatedEquipmentType = await apiUpdateManufacturer( + manufacturer.id, + name, + logout + ); + } + + // Go back to the manufacturer list, which should reflect the updated details + navigate({ + page: pages.manufacturers, + }); + } catch (ex) { + setError( + `Error saving the updated manufacturer details: ${ex.message}` + ); + } + }, + [manufacturer, logout, name, navigate] + ); + + // Set the page title + const pageTitle = + manufacturer != null ? manufacturer.name : "New Manufacturer"; + + return ( + <> +
+
{pageTitle}
+
+
+
+
+ {error != "" ? ( +
{error}
+ ) : ( + <> + )} +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ + ); +}; + +export default EquipmentTypeEditor; diff --git a/src/music-catalogue-ui/components/manufacturers/manufacturerEditor.module.css b/src/music-catalogue-ui/components/manufacturers/manufacturerEditor.module.css new file mode 100644 index 0000000..b9aa728 --- /dev/null +++ b/src/music-catalogue-ui/components/manufacturers/manufacturerEditor.module.css @@ -0,0 +1,27 @@ +.manufacturerEditorFormContainer { + display: flex; + justify-content: center; + align-items: center; +} + +.manufacturerEditorForm { + width: 80%; + padding-top: 20px; + padding-bottom: 20px; +} + +.manufacturerEditorFormLabel { + font-size: 14px; + font-weight: 600; + color: rgb(34, 34, 34); +} + +.manufacturerEditorButton { + margin-left: 10px; + float: right; +} + +.manufacturerEditorError { + font-weight: bold; + color: red; +} diff --git a/src/music-catalogue-ui/components/manufacturers/manufacturerList.js b/src/music-catalogue-ui/components/manufacturers/manufacturerList.js new file mode 100644 index 0000000..f85a240 --- /dev/null +++ b/src/music-catalogue-ui/components/manufacturers/manufacturerList.js @@ -0,0 +1,66 @@ +import styles from "./manufacturerList.module.css"; +import pages from "@/helpers/navigation"; +import useManufacturers from "@/hooks/useManufacturers"; +import { useState } from "react"; +import ManufacturerRow from "./manufacturerRow"; + +/** + * Component to render a table listing all the manufacturers in the register + * @param {*} navigate + * @param {*} logout + * @returns + */ +const ManufacturerList = ({ navigate, logout }) => { + const { manufacturers, setManufacturers } = useManufacturers(logout); + const [error, setError] = useState(""); + + return ( + <> +
+
Manufacturers
+
+
+ {error != "" ? ( +
{error}
+ ) : ( + <> + )} +
+ + + + + + + {manufacturers != null && ( + + {manufacturers.map((m) => ( + + ))} + + )} +
Name
+
+ +
+ + ); +}; + +export default ManufacturerList; diff --git a/src/music-catalogue-ui/components/manufacturers/manufacturerList.module.css b/src/music-catalogue-ui/components/manufacturers/manufacturerList.module.css new file mode 100644 index 0000000..2f6a20c --- /dev/null +++ b/src/music-catalogue-ui/components/manufacturers/manufacturerList.module.css @@ -0,0 +1,9 @@ +.manufacturerListAddButton { + float: right; +} + +.manufacturerListError { + font-weight: bold; + color: red; + text-align: center; +} diff --git a/src/music-catalogue-ui/components/manufacturers/manufacturerRow.js b/src/music-catalogue-ui/components/manufacturers/manufacturerRow.js new file mode 100644 index 0000000..c869f4a --- /dev/null +++ b/src/music-catalogue-ui/components/manufacturers/manufacturerRow.js @@ -0,0 +1,48 @@ +import pages from "@/helpers/navigation"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPenToSquare } from "@fortawesome/free-solid-svg-icons"; +import DeleteManufacturerActionIcon from "./deleteManufacturerActionIcon"; + +/** + * Component to render a row containing the details for a single manufacturer + * @param {*} manufacturer + * @param {*} navigate + * @param {*} logout + * @param {*} setManufacturers + * @param {*} setError + * @returns + */ +const ManufacturerRow = ({ + manufacturer, + navigate, + logout, + setManufacturers, + setError, +}) => { + return ( + + {manufacturer.name} + + + + + + navigate({ + page: pages.manufacturerEditor, + manufacturer: manufacturer, + }) + } + /> + + + ); +}; + +export default ManufacturerRow; diff --git a/src/music-catalogue-ui/components/menuBar.js b/src/music-catalogue-ui/components/menuBar.js index 09894e2..4c52ba6 100644 --- a/src/music-catalogue-ui/components/menuBar.js +++ b/src/music-catalogue-ui/components/menuBar.js @@ -66,7 +66,9 @@ const MenuBar = ({ navigate, logout }) => { navigate({ page: pages.equipmentTypes })}> Equipment Types - Manufacturers + navigate({ page: pages.manufacturers })}> + Manufacturers + navigate({ page: pages.retailers })}>Retailers diff --git a/src/music-catalogue-ui/helpers/api/apiManufacturers.js b/src/music-catalogue-ui/helpers/api/apiManufacturers.js new file mode 100644 index 0000000..d6cac69 --- /dev/null +++ b/src/music-catalogue-ui/helpers/api/apiManufacturers.js @@ -0,0 +1,101 @@ +import config from "@/config.json"; +import { apiReadResponseData } from "./apiUtils"; +import { apiGetPostHeaders, apiGetHeaders } from "./apiHeaders"; + +/** + * Create an manufacturer + * @param {*} name + * @param {*} logout + * @returns + */ +const apiCreateManufacturer = async (name, logout) => { + // Create the request body + const body = JSON.stringify({ + name: name, + }); + + // Call the API to create the manufacturer. This will just return the current + // record if it already exists + const url = `${config.api.baseUrl}/manufacturers`; + const response = await fetch(url, { + method: "POST", + headers: apiGetPostHeaders(), + body: body, + }); + + const equipmentType = await apiReadResponseData(response, logout); + return equipmentType; +}; + +/** + * Update an manufacturer + * @param {*} id + * @param {*} name + * @param {*} logout + * @returns + */ +const apiUpdateManufacturer = async (id, name, logout) => { + // Construct the body + const body = JSON.stringify({ + id: id, + name: name, + }); + + // Call the API to set the wish list flag for a given album + const url = `${config.api.baseUrl}/manufacturers`; + const response = await fetch(url, { + method: "PUT", + headers: apiGetPostHeaders(), + body: body, + }); + + const equipmentType = await apiReadResponseData(response, logout); + return equipmentType; +}; + +/** + * Delete the manufacturer with the specified ID + * @param {*} equipmentTypeId + * @param {*} logout + * @returns + */ +const apiDeleteManufacturer = async (equipmentTypeId, logout) => { + // Call the API to delete the specified manufacturer + const url = `${config.api.baseUrl}/manufacturers/${equipmentTypeId}`; + const response = await fetch(url, { + method: "DELETE", + headers: apiGetHeaders(), + }); + + if (response.status == 401) { + // Unauthorized so the token's likely expired - force a login + logout(); + } else { + // Return the response status code + return response.ok; + } +}; + +/** + * Return a list of manufacturers + * @param {*} logout + * @returns + */ +const apiFetchManufacturers = async (logout) => { + // Call the API to retrieve the manufacturer list + const url = `${config.api.baseUrl}/manufacturers`; + const response = await fetch(url, { + method: "GET", + headers: apiGetHeaders(), + }); + + const manufacturers = await apiReadResponseData(response, logout); + return manufacturers; +}; + +export { + apiCreateManufacturer, + apiUpdateManufacturer, + apiDeleteManufacturer, + apiFetchManufacturers, +}; diff --git a/src/music-catalogue-ui/helpers/navigation.js b/src/music-catalogue-ui/helpers/navigation.js index 35e4a6b..6bf927f 100644 --- a/src/music-catalogue-ui/helpers/navigation.js +++ b/src/music-catalogue-ui/helpers/navigation.js @@ -13,6 +13,8 @@ const pages = { retailerEditor: "RetailerEditor", equipmentTypes: "EquipmentTypes", equipmentTypeEditor: "EquipmentTypeEditor", + manufacturers: "Manufacturers", + manufacturerEditor: "ManufacturerEditor", lookup: "Lookup", export: "Export", artistStatisticsReport: "ArtistStatisticsReport", diff --git a/src/music-catalogue-ui/hooks/useManufacturers.js b/src/music-catalogue-ui/hooks/useManufacturers.js new file mode 100644 index 0000000..9538c2b --- /dev/null +++ b/src/music-catalogue-ui/hooks/useManufacturers.js @@ -0,0 +1,29 @@ +import { useState, useEffect } from "react"; +import { apiFetchManufacturers } from "@/helpers/api/apiManufacturers"; + +/** + * Hook that uses the API helpers to retrieve a list of manufacturers + * from the Music Catalogue REST API + * @param {*} logout + * @returns + */ +const useManufacturers = (logout) => { + // Current list of manufacturers and the method to change it + const [manufacturers, setManufacturers] = useState([]); + + useEffect(() => { + const fetchEquipmentTypes = async () => { + try { + // Get a list of manufacturers via the service and store it in state + var fetchedManufacturers = await apiFetchManufacturers(logout); + setManufacturers(fetchedManufacturers); + } catch {} + }; + + fetchEquipmentTypes(); + }, [logout]); + + return { manufacturers, setManufacturers }; +}; + +export default useManufacturers;