diff --git a/.tool-versions b/.tool-versions index 066cde3ab..c4aae67d2 100644 --- a/.tool-versions +++ b/.tool-versions @@ -2,3 +2,5 @@ java adoptopenjdk-17.0.15+6 nodejs 20.19.1 +# Todo: This is the first version that supports macOS + arm. Aside from it obviously being a bit behind the times, we should update the pipelines to match. +terraform 1.1.0 diff --git a/Makefile b/Makefile index b75191f2c..a7d7b569b 100644 --- a/Makefile +++ b/Makefile @@ -5,30 +5,45 @@ # ### -# Run jobs in parallel so we can see log output MAKEFLAGS += -j .PHONY: setup setup: setup-root setup-backoffice setup-webapp + brew install asdf + asdf plugin add nodejs .PHONY: setup-root setup-root: - @echo "⏭ Installing root level dependencies and commit hooks..." - @cd ./ && npm install + @echo "\n===================================================" + @echo "Installing root level dependencies and commit hooks\n" + cd . && \ + asdf install && \ + node --version && \ + npm install .PHONY: setup-backoffice setup-backoffice: - @echo "⏭ Installing backoffice dependencies..." - @cd ./backoffice && npm install + @echo "\n==================================" + @echo "Installing backoffice dependencies\n" + cd ./backoffice && \ + asdf install && \ + node --version && \ + npm install .PHONY: setup-webapp setup-webapp: - @echo "⏭ Installing webapp dependencies..." - @cd ./webapp && npm install + @echo "\n==============================" + @echo "Installing webapp dependencies\n" + cd ./webapp && \ + asdf install && \ + node --version && \ + npm install ## # Applications ## + +# Parralel docker-compose .PHONY: serve serve: serve-backing-services serve-webapp serve-backoffice serve-backoffice-stubs diff --git a/backoffice/.tool-versions b/backoffice/.tool-versions new file mode 100644 index 000000000..9d7091125 --- /dev/null +++ b/backoffice/.tool-versions @@ -0,0 +1 @@ +nodejs 24.11.1 diff --git a/backoffice/package-lock.json b/backoffice/package-lock.json index 6c1c73423..f65ebfef5 100644 --- a/backoffice/package-lock.json +++ b/backoffice/package-lock.json @@ -242,6 +242,7 @@ "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.39.0.tgz", "integrity": "sha512-kks/n2AJzKUk+DBqZhiD+7zeQGBl+WpSOQYzWy6hff3bU0ZrYFqr4keFLlzB5VKuKZog0X59/FGHb1RPBDZLVg==", "license": "MIT", + "peer": true, "dependencies": { "@azure/msal-common": "13.3.3" }, @@ -301,6 +302,7 @@ "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2438,6 +2440,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2461,6 +2464,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2589,6 +2593,7 @@ "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.3.1.tgz", "integrity": "sha512-447aUEjPIm0MnE6QYIaFz9VQOHSXf4Iu6EWOIqq11EAPqinkSZmfymPTmlOE3QjLv846lH4JVZBUOtwGbuQoww==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.5.5", "@emotion/cache": "^10.0.27", @@ -2638,6 +2643,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2749,6 +2755,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -5206,6 +5213,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -5271,6 +5279,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.17.1.tgz", "integrity": "sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.17.1", @@ -5466,6 +5475,7 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.17.1.tgz", "integrity": "sha512-aJrmGfQpyF0U4D4xYwA6ueVtQcEMebET43CUmKMP7e7iFh3sMIF3sBR0l8Urb4pqx1CBjHAaWgB0ojpND4Q3Jg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.17.1", @@ -6785,7 +6795,6 @@ "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -6806,7 +6815,6 @@ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -6823,7 +6831,6 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7164,6 +7171,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -7185,6 +7193,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.85.tgz", "integrity": "sha512-5oBDUsRDsrYq4DdyHaL99gE1AJCfuDhyxqF6/55fvvOIRkp1PpKuwJ+aMiGJR+GJt7YqMNclPROTHF20vY2cXA==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", @@ -7196,6 +7205,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.26.tgz", "integrity": "sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^17.0.0" } @@ -7322,6 +7332,7 @@ "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", @@ -7526,6 +7537,7 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8532,6 +8544,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -10070,6 +10083,7 @@ "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -14202,6 +14216,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -15906,6 +15921,7 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -16072,6 +16088,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -16097,6 +16114,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -16249,6 +16267,7 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -16490,6 +16509,7 @@ "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -16694,6 +16714,7 @@ "integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -17535,6 +17556,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17823,6 +17845,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18081,6 +18104,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -18227,6 +18251,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/backoffice/src/gateways/notes/INotesGateway.ts b/backoffice/src/gateways/notes/INotesGateway.ts index 8629e8ca4..0f743755b 100644 --- a/backoffice/src/gateways/notes/INotesGateway.ts +++ b/backoffice/src/gateways/notes/INotesGateway.ts @@ -3,4 +3,6 @@ import { INote } from "../../entities/INote"; export interface INotesGateway { getNotes: (beaconId: string) => Promise; createNote: (note: Partial) => Promise; + updateNote(noteId: string, note: Partial): Promise; + deleteNote(noteId: string): Promise; } diff --git a/backoffice/src/gateways/notes/NotesGateway.test.ts b/backoffice/src/gateways/notes/NotesGateway.test.ts new file mode 100644 index 000000000..df3d0e429 --- /dev/null +++ b/backoffice/src/gateways/notes/NotesGateway.test.ts @@ -0,0 +1,296 @@ +import axios from "axios"; +import { NotesGateway } from "./NotesGateway"; // Adjust path as needed +import { applicationConfig } from "config"; +import { IAuthGateway } from "../auth/IAuthGateway"; +import { INote, NoteType } from "../../entities/INote"; +import { INotesGateway } from "./INotesGateway"; + +jest.mock("axios"); +const mockedAxios = axios as jest.Mocked; + +describe("NotesGateway", () => { + let notesGateway: INotesGateway; + let mockAuthGateway: IAuthGateway; + let accessToken: string; + let config: any; + let beaconId: string; + + beforeEach(() => { + jest.clearAllMocks(); + accessToken = "LET.ME.IN"; + mockAuthGateway = { + getAccessToken: jest.fn().mockResolvedValue(accessToken), + }; + + config = { + timeout: applicationConfig.apiTimeoutMs, + headers: { Authorization: `Bearer ${accessToken}` }, + }; + notesGateway = new NotesGateway(mockAuthGateway); + beaconId = "f48e8212-2e10-4154-95c7-bdfd061bcfd2"; + }); + + describe("getNotes", () => { + it("should make a GET request to the correct endpoint with auth headers", async () => { + const apiResponse = { + data: { + data: [], + }, + }; + mockedAxios.get.mockResolvedValue(apiResponse); + + await notesGateway.getNotes(beaconId); + + expect(mockAuthGateway.getAccessToken).toHaveBeenCalled(); + expect(mockedAxios.get).toHaveBeenCalledWith( + `${applicationConfig.apiUrl}/note?beaconId=${beaconId}`, + config, + ); + }); + + it("should correctly map the API response to INote objects", async () => { + const mockApiDate = "2023-01-01T12:00:00Z"; + const apiResponse = { + data: { + data: [ + { + id: "note-1", + attributes: { + beaconId: beaconId, + text: "Test Note Content", + type: "GENERAL", + createdDate: mockApiDate, + userId: "user-1", + fullName: "User Note", + email: "user@example.com", + }, + }, + ], + }, + }; + + mockedAxios.get.mockResolvedValue(apiResponse); + + const result = await notesGateway.getNotes(beaconId); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + id: "note-1", + beaconId: beaconId, + text: "Test Note Content", + type: NoteType.GENERAL, + createdDate: mockApiDate, + userId: "user-1", + fullName: "User Note", + email: "user@example.com", + }); + }); + + it("should return an empty array if the API returns no data", async () => { + mockedAxios.get.mockResolvedValue({ data: { data: [] } }); + + const result = await notesGateway.getNotes(beaconId); + + expect(result).toEqual([]); + }); + + it("should re-throw errors if the API call fails", async () => { + const error = new Error("Network Error"); + mockedAxios.get.mockRejectedValue(error); + + await expect(notesGateway.getNotes(beaconId)).rejects.toThrow( + "Network Error", + ); + }); + }); + + describe("createNote", () => { + const newNotePartial: Partial = { + beaconId: beaconId, + text: "New Note", + type: NoteType.INCIDENT, + }; + + it("should make a POST request with the correct body structure and headers", async () => { + mockedAxios.post.mockResolvedValue({ + data: { + data: { + id: "new-id", + attributes: { ...newNotePartial }, + }, + }, + }); + + await notesGateway.createNote(newNotePartial); + + expect(mockAuthGateway.getAccessToken).toHaveBeenCalled(); + + const expectedPayload = { + data: { + type: "note", + attributes: { + beaconId: "", + text: newNotePartial.text, + type: newNotePartial.type, + }, + }, + }; + + expect(mockedAxios.post).toHaveBeenCalledWith( + `${applicationConfig.apiUrl}/note`, + expectedPayload, + config, + ); + }); + + it("should map the creation response back to an INote", async () => { + const apiResponse = { + data: { + data: { + id: "note-id", + attributes: { + beaconId: beaconId, + text: "New Note", + type: "INCIDENT", + createdDate: "2023-01-01", + userId: "user-test", + fullName: "Test User", + email: "user@example.com", + }, + }, + }, + }; + mockedAxios.post.mockResolvedValue(apiResponse); + + const result = await notesGateway.createNote(newNotePartial); + + expect(result.id).toBe("note-id"); + expect(result.fullName).toBe("Test User"); + }); + + it("should handle default values for missing attributes in the request payload", async () => { + const emptyNote: Partial = {}; + mockedAxios.post.mockResolvedValue({ + data: { data: { attributes: {} } }, + }); + + try { + await notesGateway.createNote(emptyNote); + } catch (e) { + // Ignore response mapping errors for this specific assertion + } + + const expectedPayload = { + data: { + type: "note", + attributes: { + beaconId: "", + text: "", + type: "", + }, + }, + }; + + expect(mockedAxios.post).toHaveBeenCalledWith( + expect.any(String), + expectedPayload, + expect.any(Object), + ); + }); + + it("should re-throw errors if the POST request fails", async () => { + mockedAxios.post.mockRejectedValue(new Error("500 Server Error")); + + await expect(notesGateway.createNote(newNotePartial)).rejects.toThrow( + "500 Server Error", + ); + }); + }); + + describe("updateNote", () => { + const noteId = "note-123"; + const updateData: Partial = { + text: "Updated text content", + type: NoteType.GENERAL, + }; + + it("should make a PATCH request to the note endpoint with the specific ID", async () => { + mockedAxios.patch.mockResolvedValue({ + data: { + data: { + id: noteId, + attributes: { ...updateData, beaconId }, + }, + }, + }); + + await notesGateway.updateNote(noteId, updateData); + + expect(mockAuthGateway.getAccessToken).toHaveBeenCalled(); + + const expectedUrl = `${applicationConfig.apiUrl}/note/${noteId}`; + + const expectedPayload = { + data: { + type: "note", + attributes: { + beaconId: "", + text: "Updated text content", + type: "GENERAL", + }, + }, + }; + + expect(mockedAxios.patch).toHaveBeenCalledWith( + expectedUrl, + expectedPayload, + config, + ); + }); + + it("should return the updated note object", async () => { + mockedAxios.patch.mockResolvedValue({ + data: { + data: { + id: noteId, + attributes: { + beaconId: beaconId, + text: "Updated text content", + type: "GENERAL", + createdDate: "2023-01-01", + userId: "user-1", + fullName: "Test User", + email: "user@example.com", + }, + }, + }, + }); + + const result = await notesGateway.updateNote(noteId, updateData); + + expect(result.id).toEqual(noteId); + expect(result.text).toEqual("Updated text content"); + }); + }); + + describe("deleteNote", () => { + const noteId = "note-to-delete"; + + it("should make a DELETE request to the specific note endpoint", async () => { + mockedAxios.delete.mockResolvedValue({ status: 204 }); + + await notesGateway.deleteNote(noteId); + + const expectedUrl = `${applicationConfig.apiUrl}/note/${noteId}`; + + expect(mockedAxios.delete).toHaveBeenCalledWith(expectedUrl, config); + }); + + it("should throw an error if the delete fails", async () => { + mockedAxios.delete.mockRejectedValue(new Error("403 Forbidden")); + await expect(notesGateway.deleteNote(noteId)).rejects.toThrow( + "403 Forbidden", + ); + }); + }); +}); diff --git a/backoffice/src/gateways/notes/NotesGateway.ts b/backoffice/src/gateways/notes/NotesGateway.ts index 45a9802ab..f98e33a38 100644 --- a/backoffice/src/gateways/notes/NotesGateway.ts +++ b/backoffice/src/gateways/notes/NotesGateway.ts @@ -33,6 +33,51 @@ export class NotesGateway implements INotesGateway { } } + public async updateNote( + noteId: string, + note: Partial, + ): Promise { + try { + const response = await this._makePatchRequest(`/note/${noteId}`, note); + return this._mapNoteResponseToNote(response.data); + } catch (e) { + throw e; + } + } + + public async deleteNote(noteId: string): Promise { + try { + await this._makeDeleteRequest(`/note/${noteId}`); + } catch (e) { + throw e; + } + } + + private async _makePatchRequest( + path: string, + note: Partial, + ): Promise { + const accessToken = await this._authGateway.getAccessToken(); + + return await axios.patch( + `${applicationConfig.apiUrl}${path}`, + this._mapNoteToNoteRequest(note), // We reuse the existing mapper + { + timeout: applicationConfig.apiTimeoutMs, + headers: { Authorization: `Bearer ${accessToken}` }, + }, + ); + } + + private async _makeDeleteRequest(path: string): Promise { + const accessToken = await this._authGateway.getAccessToken(); + + return await axios.delete(`${applicationConfig.apiUrl}${path}`, { + timeout: applicationConfig.apiTimeoutMs, + headers: { Authorization: `Bearer ${accessToken}` }, + }); + } + private async _makeGetRequest(path: string): Promise { const accessToken = await this._authGateway.getAccessToken(); diff --git a/backoffice/src/panels/notesPanel/NotesViewing.tsx b/backoffice/src/panels/notesPanel/NotesViewing.tsx index b215bc931..698c1a828 100644 --- a/backoffice/src/panels/notesPanel/NotesViewing.tsx +++ b/backoffice/src/panels/notesPanel/NotesViewing.tsx @@ -1,4 +1,5 @@ import { + Button, CardHeader, Table, TableBody, @@ -36,6 +37,7 @@ export const NotesViewing: FunctionComponent = ({ Type of note Note Noted by + @@ -45,6 +47,12 @@ export const NotesViewing: FunctionComponent = ({ {titleCase(note.type)} {note.text} {note.fullName} + + + + ))}