From 5edb906037c42912fcb1a2938ae7db88328f25e0 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 19 Feb 2022 03:17:07 -0700 Subject: [PATCH 01/25] =?UTF-8?q?=E2=9B=B2=20feat=20First=20commit=20for?= =?UTF-8?q?=20the=20Documents=20feature=20-=20Added=20a=20Documents=20Tab?= =?UTF-8?q?=20to=20the=20LandingPage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Documents Tab added to the LandingPage - Worked out some basic file selection functionality - Added base64 encoding/decoding support - Added `base64ArrayBuffer()` to convert an ArrayBuffer to a base64 string See: #323 --- package.json | 1 + src/components/Pages/LandingPage.tsx | 17 +++ src/utility/common.ts | 220 +++++++++++++++++++++++++++ 3 files changed, 238 insertions(+) diff --git a/package.json b/package.json index 4ac4a87..6d214d6 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@types/react-dom": "^17.0.11", "@typescript-eslint/eslint-plugin": "^5.1.0", "@typescript-eslint/parser": "^5.1.0", + "@types/wicg-file-system-access": "latest", "eslint": "^7.11.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-jsdoc": "^36.1.1", diff --git a/src/components/Pages/LandingPage.tsx b/src/components/Pages/LandingPage.tsx index 8bbfbe0..dc90e2b 100644 --- a/src/components/Pages/LandingPage.tsx +++ b/src/components/Pages/LandingPage.tsx @@ -6,6 +6,7 @@ import Tabs from 'react-bootstrap/Tabs'; import ToggleButton from 'react-bootstrap/ToggleButton'; import React, {useEffect, useGlobal, useMemo} from 'reactn'; import {IPreferences} from 'reactn/default'; +import {Base64, base64ArrayBuffer} from 'utility/common'; import DiagnosticPage from './DiagnosticPage'; import LoginPage from './LoginPage'; import ManageDrugPage from './ManageDrugPage'; @@ -127,6 +128,22 @@ const LandingPage = (props: IProps) => { + Documents}> + + + + Diagnostics}> window.location.reload()} /> diff --git a/src/utility/common.ts b/src/utility/common.ts index 20a81e6..9385fe5 100644 --- a/src/utility/common.ts +++ b/src/utility/common.ts @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/prefer-spread,no-bitwise,space-before-function-paren,no-underscore-dangle */ // noinspection JSUnusedGlobalSymbols import {ClientRecord, DrugLogRecord, MedicineRecord} from 'types/RecordTypes'; @@ -451,3 +452,222 @@ export const setStickyState = (key: string, value: unknown) => { export const deleteStickyState = (key: string) => { return window.localStorage.removeItem(key); }; + +/** + * Given an ArrayBuffer return a base64 string + * @param {ArrayBuffer} arrayBuffer The arrayBuffer to convert + * @returns {string} base64 string + */ +export const base64ArrayBuffer = (arrayBuffer: ArrayBuffer) => { + let base64 = ''; + const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + const bytes = new Uint8Array(arrayBuffer); + const byteLength = bytes.byteLength; + const byteRemainder = byteLength % 3; + const mainLength = byteLength - byteRemainder; + + let a; + let b; + let c; + let d; + let chunk; + + // Main loop deals with bytes in chunks of 3 + for (let index = 0; index < mainLength; index = index + 3) { + // Combine the three bytes into a single integer + // eslint-disable-next-line no-bitwise + chunk = (bytes[index] << 16) | (bytes[index + 1] << 8) | bytes[index + 2]; + + // Use bitmasks to extract 6-bit segments from the triplet + // eslint-disable-next-line no-bitwise + a = (chunk & 16_515_072) >> 18; // 16515072 = (2^6 - 1) << 18 + // eslint-disable-next-line no-bitwise + b = (chunk & 258_048) >> 12; // 258048 = (2^6 - 1) << 12 + // eslint-disable-next-line no-bitwise + c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 + // eslint-disable-next-line no-bitwise + d = chunk & 63; // 63 = 2^6 - 1 + + // Convert the raw binary segments to the appropriate ASCII encoding + base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]; + } + + // Deal with the remaining bytes and padding + if (byteRemainder === 1) { + chunk = bytes[mainLength]; + + // eslint-disable-next-line no-bitwise + a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 + + // Set the 4 least significant bits to zero + // eslint-disable-next-line no-bitwise + b = (chunk & 3) << 4; // 3 = 2^2 - 1 + + base64 += encodings[a] + encodings[b] + '=='; + } else if (byteRemainder === 2) { + // eslint-disable-next-line no-bitwise + chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]; + + // eslint-disable-next-line no-bitwise + a = (chunk & 64_512) >> 10; // 64512 = (2^6 - 1) << 10 + // eslint-disable-next-line no-bitwise + b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 + + // Set the 2 least significant bits to zero + // eslint-disable-next-line no-bitwise + c = (chunk & 15) << 2; // 15 = 2^4 - 1 + + base64 += encodings[a] + encodings[b] + encodings[c] + '='; + } + + return base64; +}; + +interface IBase64 { + encode: (input: string) => string; + _utf8_encode: (input: string) => string; + decode: (input: string) => string; + _utf8_decode: (utfText: string) => string; + _keyStr: string; +} + +/** + * Pseudo class implementing base64 encoding and decoding -- avoiding atob() and btoa() and their problems + * @link https://stackoverflow.com/a/6740027/4323201 + * @type {IBase64} + */ +export const Base64 = { + // private property + _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', + + // public method for encoding + encode: (input: string) => { + let output = ''; + let chr1; + let chr2; + let chr3; + let enc1; + let enc2; + let enc3; + let enc4; + let index = 0; + + input = Base64._utf8_encode(input); + + while (index < input.length) { + chr1 = input.codePointAt(index++) as number; + chr2 = input.codePointAt(index++) as number; + chr3 = input.codePointAt(index++) as number; + + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + + if (Number.isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (Number.isNaN(chr3)) { + enc4 = 64; + } + + output = + output + + Base64._keyStr.charAt(enc1) + + Base64._keyStr.charAt(enc2) + + Base64._keyStr.charAt(enc3) + + Base64._keyStr.charAt(enc4); + } + + return output; + }, + + // public method for decoding + decode: (input: string) => { + let output = ''; + let chr1; + let chr2; + let chr3; + let enc1; + let enc2; + let enc3; + let enc4; + let index = 0; + + input = input.replace(/[^A-Za-z\d+/=]/g, ''); + + while (index < input.length) { + enc1 = Base64._keyStr.indexOf(input.charAt(index++)); + enc2 = Base64._keyStr.indexOf(input.charAt(index++)); + enc3 = Base64._keyStr.indexOf(input.charAt(index++)); + enc4 = Base64._keyStr.indexOf(input.charAt(index++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + output = output + String.fromCodePoint(chr1); + + if (enc3 !== 64) { + output = output + String.fromCodePoint(chr2); + } + if (enc4 !== 64) { + output = output + String.fromCodePoint(chr3); + } + } + + output = Base64._utf8_decode(output); + + return output; + }, + + // private method for UTF-8 encoding + _utf8_encode: (input: string) => { + input = input.replace(/\r\n/g, '\n'); + let utftext = ''; + + for (let n = 0; n < input.length; n++) { + const c = input.codePointAt(n) as number; + + if (c < 128) { + utftext += String.fromCodePoint(c); + } else if (c > 127 && c < 2048) { + utftext += String.fromCodePoint((c >> 6) | 192); + utftext += String.fromCodePoint((c & 63) | 128); + } else { + utftext += String.fromCodePoint((c >> 12) | 224); + utftext += String.fromCodePoint(((c >> 6) & 63) | 128); + utftext += String.fromCodePoint((c & 63) | 128); + } + } + return utftext; + }, + + // private method for UTF-8 decoding + _utf8_decode: (utfText: string) => { + let output = ''; + let index = 0; + let c; + let c2; + let c3; + + while (index < utfText.length) { + c = utfText.codePointAt(index) as number; + + if (c < 128) { + output += String.fromCodePoint(c); + index++; + } else if (c > 191 && c < 224) { + c2 = utfText.codePointAt(index + 1) as number; + output += String.fromCodePoint(((c & 31) << 6) | (c2 & 63)); + index += 2; + } else { + c2 = utfText.codePointAt(index + 1) as number; + c3 = utfText.codePointAt(index + 2) as number; + output += String.fromCodePoint(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); + index += 3; + } + } + return output; + } +}; From 1e7fd72ea4111124511787d8441a2cb28fc1dd28 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 19 Feb 2022 13:01:14 -0700 Subject: [PATCH 02/25] =?UTF-8?q?=E2=9B=B2=20feat=20PoC=20for=20saving=20a?= =?UTF-8?q?=20file=20as=20a=20base64=20encoded=20string=20then=20saving=20?= =?UTF-8?q?the=20encoded=20string=20as=20a=20file=20using=20Chrome's=20Fil?= =?UTF-8?q?e=20System=20Access=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created a Documents.tsx page - 🌀 refactored the base64 stuff into their own modules - Added `idx` as an acceptable variable name in .eslintrc.js - Found a faster way to encode and decode base64 ArrayBuffers See: #323 --- .eslintrc.js | 1 + src/components/Pages/Documents.tsx | 77 +++++++++ src/components/Pages/LandingPage.tsx | 15 +- src/utility/Base64.ts | 162 ++++++++++++++++++ src/utility/common.ts | 220 ------------------------- src/utility/decodeBase64ArrayBuffer.ts | 51 ++++++ src/utility/encodeBase64ArrayBuffer.ts | 33 ++++ 7 files changed, 326 insertions(+), 233 deletions(-) create mode 100644 src/components/Pages/Documents.tsx create mode 100644 src/utility/Base64.ts create mode 100644 src/utility/decodeBase64ArrayBuffer.ts create mode 100644 src/utility/encodeBase64ArrayBuffer.ts diff --git a/.eslintrc.js b/.eslintrc.js index d4a0e9f..1407035 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -139,6 +139,7 @@ module.exports = { props: true, CustomMenuProps: true, dropdownProps: true, + idx: true, IDropdownProps: true, IProps: true, IModalProps: true, diff --git a/src/components/Pages/Documents.tsx b/src/components/Pages/Documents.tsx new file mode 100644 index 0000000..505a49d --- /dev/null +++ b/src/components/Pages/Documents.tsx @@ -0,0 +1,77 @@ +import Button from 'react-bootstrap/Button'; +import ButtonGroup from 'react-bootstrap/ButtonGroup'; +import Row from 'react-bootstrap/Row'; +import React, {useGlobal, useState} from 'reactn'; +import decodeBase64ArrayBuffer from 'utility/decodeBase64ArrayBuffer'; +import encodeBase64ArrayBuffer from 'utility/encodeBase64ArrayBuffer'; + +export enum UploadFileErrorCode { + Ok, + file_selection_cancelled, + max_file_size_exceeded +} + +const Documents = () => { + const [, setErrorDetails] = useGlobal('__errorDetails'); + const [busy, setIsBusy] = useState(false); + const [encodedString, setEncodedString] = useState(''); + + const saveFile = async (content: ArrayBuffer, suggestedFileName?: string) => { + const options = suggestedFileName ? {suggestedName: suggestedFileName} : undefined; + const fileHandle = await window.showSaveFilePicker(options); + const fileStream = await fileHandle.createWritable(); + await fileStream.write(content); + await fileStream.close(); + }; + + const handleSaveEncodedStringToFile = () => { + // Now decode the string back to an ArrayBuffer + const decodedArrayBuffer = decodeBase64ArrayBuffer(encodedString); + // Save the decodedArrayBuffer as a new file + saveFile(decodedArrayBuffer); + }; + + const uploadFile = async () => { + try { + // Bring up the file selector dialog window + const [fileHandle] = await window.showOpenFilePicker(); + // Get the file object + const file = (await fileHandle.getFile()) as File; + if (file.size > 500_000_000) { + return UploadFileErrorCode.max_file_size_exceeded; + } + // Get the file contents as and ArrayBuffer + const fileArrayBuffer = await file.arrayBuffer(); + // Convert the ArrayBuffer into base64 + const arrayString = encodeBase64ArrayBuffer(fileArrayBuffer); + alert('base64 encoded: ' + JSON.stringify(arrayString)); + setEncodedString(arrayString); + } catch (error: unknown) { + if (error instanceof DOMException && error.message.toLowerCase().includes('the user aborted a request')) { + return UploadFileErrorCode.file_selection_cancelled; + } + + if (error instanceof Error) { + await setErrorDetails(error); + } + } + }; + + return ( + <> + + + + + +

Placeholder for DocumentsGrid

+
+ + ); +}; + +export default Documents; diff --git a/src/components/Pages/LandingPage.tsx b/src/components/Pages/LandingPage.tsx index dc90e2b..7f4df58 100644 --- a/src/components/Pages/LandingPage.tsx +++ b/src/components/Pages/LandingPage.tsx @@ -1,4 +1,5 @@ import ClientPage from 'components/Pages/ClientPage'; +import Documents from 'components/Pages/Documents'; import SettingsPage from 'components/Pages/SettingsPage'; import {ReactNode} from 'react'; import Tab from 'react-bootstrap/Tab'; @@ -6,7 +7,6 @@ import Tabs from 'react-bootstrap/Tabs'; import ToggleButton from 'react-bootstrap/ToggleButton'; import React, {useEffect, useGlobal, useMemo} from 'reactn'; import {IPreferences} from 'reactn/default'; -import {Base64, base64ArrayBuffer} from 'utility/common'; import DiagnosticPage from './DiagnosticPage'; import LoginPage from './LoginPage'; import ManageDrugPage from './ManageDrugPage'; @@ -130,18 +130,7 @@ const LandingPage = (props: IProps) => {
Documents}> - + Diagnostics}> diff --git a/src/utility/Base64.ts b/src/utility/Base64.ts new file mode 100644 index 0000000..4b6f1d8 --- /dev/null +++ b/src/utility/Base64.ts @@ -0,0 +1,162 @@ +/* eslint-disable no-underscore-dangle,no-bitwise */ + +export interface IBase64 { + encode: (input: string) => string; + decode: (input: string) => string; +} + +/** + * Pseudo class implementing base64 encoding and decoding -- avoiding atob() and btoa() and their problems + * @link https://stackoverflow.com/a/6740027/4323201 + * @type {IBase64} + */ +const Base64 = (): IBase64 => { + const _keyString = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + + /** + * UTF-8 encoding + * @param {string} input The string to encode + * @returns {string} The encoded string + */ + const _utf8_encode = (input: string) => { + input = input.replace(/\r\n/g, '\n'); + let utfText = ''; + + for (let n = 0; n < input.length; n++) { + const c = input.codePointAt(n) as number; + + if (c < 128) { + utfText += String.fromCodePoint(c); + } else if (c > 127 && c < 2048) { + utfText += String.fromCodePoint((c >> 6) | 192); + utfText += String.fromCodePoint((c & 63) | 128); + } else { + utfText += String.fromCodePoint((c >> 12) | 224); + utfText += String.fromCodePoint(((c >> 6) & 63) | 128); + utfText += String.fromCodePoint((c & 63) | 128); + } + } + return utfText; + }; + + /** + * UTF-8 decoding + * @param {string} utfText The encoded string + * @returns {string} The decoded string + */ + const _utf8_decode = (utfText: string) => { + let output = ''; + let index = 0; + let c; + let c2; + let c3; + + while (index < utfText.length) { + c = utfText.codePointAt(index) as number; + + if (c < 128) { + output += String.fromCodePoint(c); + index++; + } else if (c > 191 && c < 224) { + c2 = utfText.codePointAt(index + 1) as number; + output += String.fromCodePoint(((c & 31) << 6) | (c2 & 63)); + index += 2; + } else { + c2 = utfText.codePointAt(index + 1) as number; + c3 = utfText.codePointAt(index + 2) as number; + output += String.fromCodePoint(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); + index += 3; + } + } + return output; + }; + + return { + /** + * Base64 encoding + * @param {string} input The string to encode + * @returns {string} The encoded string + */ + encode: (input: string) => { + let output = ''; + let chr1; + let chr2; + let chr3; + let enc1; + let enc2; + let enc3; + let enc4; + let index = 0; + + input = _utf8_encode(input); + + while (index < input.length) { + chr1 = input.codePointAt(index++) as number; + chr2 = input.codePointAt(index++) as number; + chr3 = input.codePointAt(index++) as number; + + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + + if (Number.isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (Number.isNaN(chr3)) { + enc4 = 64; + } + + output = + output + + _keyString.charAt(enc1) + + _keyString.charAt(enc2) + + _keyString.charAt(enc3) + + _keyString.charAt(enc4); + } + return output; + }, + + /** + * Base64 decoding + * @param {string} input The encoded string + * @returns {string} The decoded string + */ + decode: (input: string) => { + let output = ''; + let chr1; + let chr2; + let chr3; + let enc1; + let enc2; + let enc3; + let enc4; + let index = 0; + + input = input.replace(/[^A-Za-z\d+/=]/g, ''); + + while (index < input.length) { + enc1 = _keyString.indexOf(input.charAt(index++)); + enc2 = _keyString.indexOf(input.charAt(index++)); + enc3 = _keyString.indexOf(input.charAt(index++)); + enc4 = _keyString.indexOf(input.charAt(index++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + output = output + String.fromCodePoint(chr1); + + if (enc3 !== 64) { + output = output + String.fromCodePoint(chr2); + } + if (enc4 !== 64) { + output = output + String.fromCodePoint(chr3); + } + } + output = _utf8_decode(output); + return output; + } + }; +}; + +export default Base64; diff --git a/src/utility/common.ts b/src/utility/common.ts index 9385fe5..20a81e6 100644 --- a/src/utility/common.ts +++ b/src/utility/common.ts @@ -1,4 +1,3 @@ -/* eslint-disable unicorn/prefer-spread,no-bitwise,space-before-function-paren,no-underscore-dangle */ // noinspection JSUnusedGlobalSymbols import {ClientRecord, DrugLogRecord, MedicineRecord} from 'types/RecordTypes'; @@ -452,222 +451,3 @@ export const setStickyState = (key: string, value: unknown) => { export const deleteStickyState = (key: string) => { return window.localStorage.removeItem(key); }; - -/** - * Given an ArrayBuffer return a base64 string - * @param {ArrayBuffer} arrayBuffer The arrayBuffer to convert - * @returns {string} base64 string - */ -export const base64ArrayBuffer = (arrayBuffer: ArrayBuffer) => { - let base64 = ''; - const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - - const bytes = new Uint8Array(arrayBuffer); - const byteLength = bytes.byteLength; - const byteRemainder = byteLength % 3; - const mainLength = byteLength - byteRemainder; - - let a; - let b; - let c; - let d; - let chunk; - - // Main loop deals with bytes in chunks of 3 - for (let index = 0; index < mainLength; index = index + 3) { - // Combine the three bytes into a single integer - // eslint-disable-next-line no-bitwise - chunk = (bytes[index] << 16) | (bytes[index + 1] << 8) | bytes[index + 2]; - - // Use bitmasks to extract 6-bit segments from the triplet - // eslint-disable-next-line no-bitwise - a = (chunk & 16_515_072) >> 18; // 16515072 = (2^6 - 1) << 18 - // eslint-disable-next-line no-bitwise - b = (chunk & 258_048) >> 12; // 258048 = (2^6 - 1) << 12 - // eslint-disable-next-line no-bitwise - c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 - // eslint-disable-next-line no-bitwise - d = chunk & 63; // 63 = 2^6 - 1 - - // Convert the raw binary segments to the appropriate ASCII encoding - base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]; - } - - // Deal with the remaining bytes and padding - if (byteRemainder === 1) { - chunk = bytes[mainLength]; - - // eslint-disable-next-line no-bitwise - a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 - - // Set the 4 least significant bits to zero - // eslint-disable-next-line no-bitwise - b = (chunk & 3) << 4; // 3 = 2^2 - 1 - - base64 += encodings[a] + encodings[b] + '=='; - } else if (byteRemainder === 2) { - // eslint-disable-next-line no-bitwise - chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]; - - // eslint-disable-next-line no-bitwise - a = (chunk & 64_512) >> 10; // 64512 = (2^6 - 1) << 10 - // eslint-disable-next-line no-bitwise - b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 - - // Set the 2 least significant bits to zero - // eslint-disable-next-line no-bitwise - c = (chunk & 15) << 2; // 15 = 2^4 - 1 - - base64 += encodings[a] + encodings[b] + encodings[c] + '='; - } - - return base64; -}; - -interface IBase64 { - encode: (input: string) => string; - _utf8_encode: (input: string) => string; - decode: (input: string) => string; - _utf8_decode: (utfText: string) => string; - _keyStr: string; -} - -/** - * Pseudo class implementing base64 encoding and decoding -- avoiding atob() and btoa() and their problems - * @link https://stackoverflow.com/a/6740027/4323201 - * @type {IBase64} - */ -export const Base64 = { - // private property - _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', - - // public method for encoding - encode: (input: string) => { - let output = ''; - let chr1; - let chr2; - let chr3; - let enc1; - let enc2; - let enc3; - let enc4; - let index = 0; - - input = Base64._utf8_encode(input); - - while (index < input.length) { - chr1 = input.codePointAt(index++) as number; - chr2 = input.codePointAt(index++) as number; - chr3 = input.codePointAt(index++) as number; - - enc1 = chr1 >> 2; - enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); - enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); - enc4 = chr3 & 63; - - if (Number.isNaN(chr2)) { - enc3 = enc4 = 64; - } else if (Number.isNaN(chr3)) { - enc4 = 64; - } - - output = - output + - Base64._keyStr.charAt(enc1) + - Base64._keyStr.charAt(enc2) + - Base64._keyStr.charAt(enc3) + - Base64._keyStr.charAt(enc4); - } - - return output; - }, - - // public method for decoding - decode: (input: string) => { - let output = ''; - let chr1; - let chr2; - let chr3; - let enc1; - let enc2; - let enc3; - let enc4; - let index = 0; - - input = input.replace(/[^A-Za-z\d+/=]/g, ''); - - while (index < input.length) { - enc1 = Base64._keyStr.indexOf(input.charAt(index++)); - enc2 = Base64._keyStr.indexOf(input.charAt(index++)); - enc3 = Base64._keyStr.indexOf(input.charAt(index++)); - enc4 = Base64._keyStr.indexOf(input.charAt(index++)); - - chr1 = (enc1 << 2) | (enc2 >> 4); - chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); - chr3 = ((enc3 & 3) << 6) | enc4; - - output = output + String.fromCodePoint(chr1); - - if (enc3 !== 64) { - output = output + String.fromCodePoint(chr2); - } - if (enc4 !== 64) { - output = output + String.fromCodePoint(chr3); - } - } - - output = Base64._utf8_decode(output); - - return output; - }, - - // private method for UTF-8 encoding - _utf8_encode: (input: string) => { - input = input.replace(/\r\n/g, '\n'); - let utftext = ''; - - for (let n = 0; n < input.length; n++) { - const c = input.codePointAt(n) as number; - - if (c < 128) { - utftext += String.fromCodePoint(c); - } else if (c > 127 && c < 2048) { - utftext += String.fromCodePoint((c >> 6) | 192); - utftext += String.fromCodePoint((c & 63) | 128); - } else { - utftext += String.fromCodePoint((c >> 12) | 224); - utftext += String.fromCodePoint(((c >> 6) & 63) | 128); - utftext += String.fromCodePoint((c & 63) | 128); - } - } - return utftext; - }, - - // private method for UTF-8 decoding - _utf8_decode: (utfText: string) => { - let output = ''; - let index = 0; - let c; - let c2; - let c3; - - while (index < utfText.length) { - c = utfText.codePointAt(index) as number; - - if (c < 128) { - output += String.fromCodePoint(c); - index++; - } else if (c > 191 && c < 224) { - c2 = utfText.codePointAt(index + 1) as number; - output += String.fromCodePoint(((c & 31) << 6) | (c2 & 63)); - index += 2; - } else { - c2 = utfText.codePointAt(index + 1) as number; - c3 = utfText.codePointAt(index + 2) as number; - output += String.fromCodePoint(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); - index += 3; - } - } - return output; - } -}; diff --git a/src/utility/decodeBase64ArrayBuffer.ts b/src/utility/decodeBase64ArrayBuffer.ts new file mode 100644 index 0000000..385cae9 --- /dev/null +++ b/src/utility/decodeBase64ArrayBuffer.ts @@ -0,0 +1,51 @@ +/* eslint-disable no-bitwise */ + +/** + * Converts a base64 string into an ArrayBuffer + * @link https://gist.github.com/jonleighton/958841?permalink_comment_id=2839519#gistcomment-2839519 + * @param {string} base64 The base64 encoded string + * @returns {ArrayBuffer} The decoded string as an ArrayBuffer + */ +const decodeBase64ArrayBuffer = (base64: string) => { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + // Use a lookup table to find the index. + const lookup = new Uint8Array(256); + for (let idx = 0; idx < chars.length; idx++) { + lookup[chars.codePointAt(idx) as number] = idx; + } + + let bufferLength = base64.length * 0.75; + const immutableLength = base64.length; + let index; + let p = 0; + let encoded1; + let encoded2; + let encoded3; + let encoded4; + + if (base64.at(-1) === '=') { + bufferLength--; + if (base64.at(-2) === '=') { + bufferLength--; + } + } + + const arraybuffer = new ArrayBuffer(bufferLength); + const bytes = new Uint8Array(arraybuffer); + + for (index = 0; index < immutableLength; index += 4) { + encoded1 = lookup[base64.codePointAt(index) as number]; + encoded2 = lookup[base64.codePointAt(index + 1) as number]; + encoded3 = lookup[base64.codePointAt(index + 2) as number]; + encoded4 = lookup[base64.codePointAt(index + 3) as number]; + + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); + } + + return arraybuffer; +}; + +export default decodeBase64ArrayBuffer; diff --git a/src/utility/encodeBase64ArrayBuffer.ts b/src/utility/encodeBase64ArrayBuffer.ts new file mode 100644 index 0000000..10bba41 --- /dev/null +++ b/src/utility/encodeBase64ArrayBuffer.ts @@ -0,0 +1,33 @@ +/* eslint-disable no-bitwise */ + +/** + * Given an ArrayBuffer return a base64 string + * @link https://gist.github.com/jonleighton/958841?permalink_comment_id=2839519#gistcomment-2839519 + * @param {ArrayBuffer} arrayBuffer The arrayBuffer to convert + * @returns {string} base64 string + */ +const encodeBase64ArrayBuffer = (arrayBuffer: ArrayBuffer) => { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + const bytes = new Uint8Array(arrayBuffer); + let idx; + const bytesLength = bytes.length; + let base64 = ''; + + for (idx = 0; idx < bytesLength; idx += 3) { + base64 += chars[bytes[idx] >> 2]; + base64 += chars[((bytes[idx] & 3) << 4) | (bytes[idx + 1] >> 4)]; + base64 += chars[((bytes[idx + 1] & 15) << 2) | (bytes[idx + 2] >> 6)]; + base64 += chars[bytes[idx + 2] & 63]; + } + + if (bytesLength % 3 === 2) { + base64 = base64.slice(0, Math.max(0, base64.length - 1)) + '='; + } else if (bytesLength % 3 === 1) { + base64 = base64.slice(0, Math.max(0, base64.length - 2)) + '=='; + } + + return base64; +}; + +export default encodeBase64ArrayBuffer; From dde51ea06757fb8cb811b3bcb8fb59f8165e4330 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 23 Feb 2022 04:19:14 -0700 Subject: [PATCH 03/25] =?UTF-8?q?=E2=9B=B2=20feat=20PoC=20Uploading=20a=20?= =?UTF-8?q?file=20via=20`FileData()`=20using=20the=20newly=20created=20`Do?= =?UTF-8?q?cumentProvider()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `DocumentProvider.uploadFile()` and `setApiKey()` methods established - Added some development environment shortcuts to speed development - `getInitalState()` updated with the new DocumentProvider See: #323 --- src/components/Pages/Documents.tsx | 32 +++++++++++++++++-- src/index.tsx | 5 +++ src/providers/DocumentProvider.ts | 49 ++++++++++++++++++++++++++++++ src/utility/getInitialState.ts | 12 +++++--- 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 src/providers/DocumentProvider.ts diff --git a/src/components/Pages/Documents.tsx b/src/components/Pages/Documents.tsx index 505a49d..304bc54 100644 --- a/src/components/Pages/Documents.tsx +++ b/src/components/Pages/Documents.tsx @@ -1,7 +1,9 @@ import Button from 'react-bootstrap/Button'; import ButtonGroup from 'react-bootstrap/ButtonGroup'; +import Form from 'react-bootstrap/Form'; import Row from 'react-bootstrap/Row'; import React, {useGlobal, useState} from 'reactn'; +import {ChangeEvent} from 'react'; import decodeBase64ArrayBuffer from 'utility/decodeBase64ArrayBuffer'; import encodeBase64ArrayBuffer from 'utility/encodeBase64ArrayBuffer'; @@ -13,8 +15,12 @@ export enum UploadFileErrorCode { const Documents = () => { const [, setErrorDetails] = useGlobal('__errorDetails'); + // todo: use when system is busy with stuff const [busy, setIsBusy] = useState(false); + const [uploadedFileName, setUploadedFileName] = useState('Select a File to Upload'); const [encodedString, setEncodedString] = useState(''); + const [providers] = useGlobal('providers'); + const documentProvider = providers.documentProvider; const saveFile = async (content: ArrayBuffer, suggestedFileName?: string) => { const options = suggestedFileName ? {suggestedName: suggestedFileName} : undefined; @@ -57,6 +63,23 @@ const Documents = () => { } }; + const handleFileInput = async (fileInputEvent: ChangeEvent) => { + if (fileInputEvent) { + const target = fileInputEvent.target as HTMLInputElement; + const files = target.files; + if (files && files.length > 0) { + const file = files[0]; + const formData = new FormData(); + formData.append('example1', file); + setUploadedFileName(file.name); + const fileUploadedSuccessfully = await documentProvider.uploadFile(formData); + if (!fileUploadedSuccessfully) { + setUploadedFileName('File upload failed'); + } + } + } + }; + return ( <> @@ -67,8 +90,13 @@ const Documents = () => { Save encoded string as new file - -

Placeholder for DocumentsGrid

+ + ) => handleFileInput(event)} + /> ); diff --git a/src/index.tsx b/src/index.tsx index 1a2ee84..4747a25 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -35,6 +35,11 @@ const logAppStarted = (initialState: State) => { const startApp = async () => { try { const initialState = await setGlobal(getInitialState()); + const isDevelopment = !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; + if (isDevelopment) { + const apiKey = process.env.REACT_APP_APIKEY || ''; + await initialState.providers.setApi(apiKey); + } ReactDOM.render(, document.getElementById('root'), () => logAppStarted(initialState)); } catch (error) { console.log('Something went wrong', error); // eslint-disable-line no-console diff --git a/src/providers/DocumentProvider.ts b/src/providers/DocumentProvider.ts new file mode 100644 index 0000000..ef927d5 --- /dev/null +++ b/src/providers/DocumentProvider.ts @@ -0,0 +1,49 @@ +export interface IDocumentProvider { + setApiKey: (apiKey: string) => void; + uploadFile: (formData: FormData) => Promise; // todo: Promise +} +type DocumentRecord = { + Id: number | null; + Size: number; + FileName: string; + Type: string | null; +}; + +type UploadResponse = { + status: number; + success: boolean; + data: null | DocumentRecord; +}; + +const DocumentProvider = (baseUrl: string): IDocumentProvider => { + const _baseUrl = baseUrl; + let _apiKey = null as string | null; + + return { + /** + * Set the apiKey + * @param {string} apiKey The API key to use + */ + setApiKey: (apiKey: string) => { + _apiKey = apiKey; + }, + + uploadFile: async (formData: FormData): Promise => { + const uri = _baseUrl + 'document/upload?api_key=' + _apiKey; + const response = await fetch(uri, { + method: 'POST', + body: formData + }); + + const responseJSON = (await response.json()) as UploadResponse; + if (responseJSON.success) { + alert('data: ' + JSON.stringify(responseJSON.data)); + return true; // TODO: Return a Document record object + } else { + throw response; + } + } + }; +}; + +export default DocumentProvider; diff --git a/src/utility/getInitialState.ts b/src/utility/getInitialState.ts index d66d028..8c969a3 100644 --- a/src/utility/getInitialState.ts +++ b/src/utility/getInitialState.ts @@ -1,5 +1,6 @@ import ClientManager from 'managers/ClientManager'; import ClientProvider, {IClientProvider} from 'providers/ClientProvider'; +import DocumentProvider, {IDocumentProvider} from 'providers/DocumentProvider'; import {State} from 'reactn/default'; import {ClientRecord, MedicineRecord} from 'types/RecordTypes'; import AuthManager from '../managers/AuthManager'; @@ -14,6 +15,7 @@ import PinProvider, {IPinProvider} from 'providers/PinProvider'; export interface IProviders { authenticationProvider: IAuthenticationProvider; clientProvider: IClientProvider; + documentProvider: IDocumentProvider; medicineProvider: IMedicineProvider; medHistoryProvider: IMedHistoryProvider; pillboxProvider: IPillboxProvider; @@ -35,11 +37,12 @@ const getInitialState = () => { const providers = { authenticationProvider: AuthenticationProvider(baseUrl), + clientProvider: ClientProvider(baseUrl), + documentProvider: DocumentProvider(baseUrl), medHistoryProvider: MedHistoryProvider(baseUrl), medicineProvider: MedicineProvider(baseUrl), - clientProvider: ClientProvider(baseUrl), - pillboxProvider: PillboxProvider(baseUrl), pillboxItemProvider: PillboxItemProvider(baseUrl), + pillboxProvider: PillboxProvider(baseUrl), pinProvider: PinProvider(baseUrl), /** @@ -47,11 +50,12 @@ const getInitialState = () => { * @param {string} apiKey The API key as returned from the web service */ setApi: async (apiKey: string): Promise => { + await providers.clientProvider.setApiKey(apiKey); + await providers.documentProvider.setApiKey(apiKey); await providers.medHistoryProvider.setApiKey(apiKey); await providers.medicineProvider.setApiKey(apiKey); - await providers.clientProvider.setApiKey(apiKey); - await providers.pillboxProvider.setApiKey(apiKey); await providers.pillboxItemProvider.setApiKey(apiKey); + await providers.pillboxProvider.setApiKey(apiKey); await providers.pinProvider.setApiKey(apiKey); } } as IProviders; From 1976a5470015bc66181b6a3ab8cade44654e7593 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 24 Feb 2022 06:00:26 -0700 Subject: [PATCH 04/25] =?UTF-8?q?=E2=9B=B2=20feat=20PoC=20Simplified=20the?= =?UTF-8?q?=20uploading=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `handleFileUpload()` passes mock client_id as a parameter - Uploading now saves to the Document table See: #323 --- src/components/Pages/Documents.tsx | 119 +++++++++++------------------ src/providers/DocumentProvider.ts | 20 +++-- 2 files changed, 57 insertions(+), 82 deletions(-) diff --git a/src/components/Pages/Documents.tsx b/src/components/Pages/Documents.tsx index 304bc54..af400e0 100644 --- a/src/components/Pages/Documents.tsx +++ b/src/components/Pages/Documents.tsx @@ -1,11 +1,7 @@ -import Button from 'react-bootstrap/Button'; -import ButtonGroup from 'react-bootstrap/ButtonGroup'; +import {ChangeEvent} from 'react'; import Form from 'react-bootstrap/Form'; import Row from 'react-bootstrap/Row'; import React, {useGlobal, useState} from 'reactn'; -import {ChangeEvent} from 'react'; -import decodeBase64ArrayBuffer from 'utility/decodeBase64ArrayBuffer'; -import encodeBase64ArrayBuffer from 'utility/encodeBase64ArrayBuffer'; export enum UploadFileErrorCode { Ok, @@ -15,90 +11,61 @@ export enum UploadFileErrorCode { const Documents = () => { const [, setErrorDetails] = useGlobal('__errorDetails'); - // todo: use when system is busy with stuff - const [busy, setIsBusy] = useState(false); - const [uploadedFileName, setUploadedFileName] = useState('Select a File to Upload'); - const [encodedString, setEncodedString] = useState(''); + const [isBusy, setIsIsBusy] = useState(false); + const defaultFileLabelText = 'Select a File to Upload'; + const [uploadedFileName, setUploadedFileName] = useState(defaultFileLabelText); + const [invalidMaxSize, setInvalidMaxSize] = useState(false); const [providers] = useGlobal('providers'); const documentProvider = providers.documentProvider; - const saveFile = async (content: ArrayBuffer, suggestedFileName?: string) => { - const options = suggestedFileName ? {suggestedName: suggestedFileName} : undefined; - const fileHandle = await window.showSaveFilePicker(options); - const fileStream = await fileHandle.createWritable(); - await fileStream.write(content); - await fileStream.close(); - }; - - const handleSaveEncodedStringToFile = () => { - // Now decode the string back to an ArrayBuffer - const decodedArrayBuffer = decodeBase64ArrayBuffer(encodedString); - // Save the decodedArrayBuffer as a new file - saveFile(decodedArrayBuffer); - }; - - const uploadFile = async () => { - try { - // Bring up the file selector dialog window - const [fileHandle] = await window.showOpenFilePicker(); - // Get the file object - const file = (await fileHandle.getFile()) as File; - if (file.size > 500_000_000) { - return UploadFileErrorCode.max_file_size_exceeded; - } - // Get the file contents as and ArrayBuffer - const fileArrayBuffer = await file.arrayBuffer(); - // Convert the ArrayBuffer into base64 - const arrayString = encodeBase64ArrayBuffer(fileArrayBuffer); - alert('base64 encoded: ' + JSON.stringify(arrayString)); - setEncodedString(arrayString); - } catch (error: unknown) { - if (error instanceof DOMException && error.message.toLowerCase().includes('the user aborted a request')) { - return UploadFileErrorCode.file_selection_cancelled; - } - - if (error instanceof Error) { - await setErrorDetails(error); - } - } - }; - - const handleFileInput = async (fileInputEvent: ChangeEvent) => { + /** + * Handle when the user clicked the Select a File to Upload component + * @param {React.ChangeEvent} fileInputEvent The file InputElement + * @returns {Promise} + */ + const handleFileUpload = async (fileInputEvent: ChangeEvent) => { if (fileInputEvent) { const target = fileInputEvent.target as HTMLInputElement; const files = target.files; - if (files && files.length > 0) { + // Must be only one file + if (files && files.length === 1) { const file = files[0]; - const formData = new FormData(); - formData.append('example1', file); - setUploadedFileName(file.name); - const fileUploadedSuccessfully = await documentProvider.uploadFile(formData); - if (!fileUploadedSuccessfully) { - setUploadedFileName('File upload failed'); + // Max file size is 100MB + if (file.size <= 104_857_600) { + setIsIsBusy(true); + try { + // See: https://www.slimframework.com/docs/v4/cookbook/uploading-files.html + const formData = new FormData(); + formData.append('single_file', file); + setUploadedFileName(file.name); + const documentRecord = await documentProvider.uploadFile(formData, 1092); + // TODO: Insert the record into the Document table + alert('documentRecord: ' + JSON.stringify(documentRecord)); + } catch (error) { + await setErrorDetails(error); + } + setIsIsBusy(false); + } else { + setInvalidMaxSize(true); } + } else { + setUploadedFileName(defaultFileLabelText); } } }; return ( - <> - - - - - - ) => handleFileInput(event)} - /> - - + + ) => handleFileUpload(event)} + /> +
File exceeds maximum size allowed
+
); }; diff --git a/src/providers/DocumentProvider.ts b/src/providers/DocumentProvider.ts index ef927d5..2fa2ee4 100644 --- a/src/providers/DocumentProvider.ts +++ b/src/providers/DocumentProvider.ts @@ -1,6 +1,6 @@ export interface IDocumentProvider { setApiKey: (apiKey: string) => void; - uploadFile: (formData: FormData) => Promise; // todo: Promise + uploadFile: (formData: FormData, clientId: number) => Promise; } type DocumentRecord = { Id: number | null; @@ -28,17 +28,25 @@ const DocumentProvider = (baseUrl: string): IDocumentProvider => { _apiKey = apiKey; }, - uploadFile: async (formData: FormData): Promise => { - const uri = _baseUrl + 'document/upload?api_key=' + _apiKey; + /** + * Upload a file as a FormData object. Note that Frak isn't used because the data is not JSON + * @param {FormData} formData The FormData object containing the name and file + * @param {number} clientId The Client PK + * @returns {Promise} A Document record as a promise + */ + uploadFile: async (formData: FormData, clientId): Promise => { + const uri = _baseUrl + 'document/upload/' + clientId + '?api_key=' + _apiKey; const response = await fetch(uri, { method: 'POST', - body: formData + body: formData, + headers: { + Accept: 'application/json' + } }); const responseJSON = (await response.json()) as UploadResponse; if (responseJSON.success) { - alert('data: ' + JSON.stringify(responseJSON.data)); - return true; // TODO: Return a Document record object + return responseJSON.data as DocumentRecord; } else { throw response; } From b00d6ee468736d0ffe0060c08478961f51da160b Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 27 Feb 2022 12:20:42 -0700 Subject: [PATCH 05/25] =?UTF-8?q?=E2=9B=B2=20feat=20Significantly=20changi?= =?UTF-8?q?ng=20the=20UI=20Moved=20ManageRx=20into=20the=20Rx=20Tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See: #326 --- src/components/Pages/ClientPage.tsx | 3 ++ .../Pages/{Documents.tsx => DocumentPage.tsx} | 42 +++++++++---------- src/components/Pages/LandingPage.tsx | 25 ++++------- .../Pages/ListGroups/OtcListGroup.tsx | 2 +- src/components/Pages/ManageDrugPage.tsx | 9 +--- src/components/Pages/RxTabs/RxOtc.tsx | 2 +- .../Pages/{MedicinePage.tsx => rxPage.tsx} | 35 +++++++++++++--- src/global.d.ts | 10 ++++- src/providers/DocumentProvider.ts | 42 ++++++++++++++++--- src/types/RecordTypes.ts | 15 +++++++ 10 files changed, 124 insertions(+), 61 deletions(-) rename src/components/Pages/{Documents.tsx => DocumentPage.tsx} (74%) rename src/components/Pages/{MedicinePage.tsx => rxPage.tsx} (88%) diff --git a/src/components/Pages/ClientPage.tsx b/src/components/Pages/ClientPage.tsx index de10746..dfbdafa 100644 --- a/src/components/Pages/ClientPage.tsx +++ b/src/components/Pages/ClientPage.tsx @@ -77,11 +77,14 @@ const ClientPage = (props: IProps): JSX.Element | null => { await setActiveClient({ ...activeClient, clientInfo: clientLoad.clientInfo, + documentList: clientLoad.documentList, drugLogList: clientLoad.drugLogList, medicineList: clientLoad.medicineList, pillboxList: clientLoad.pillboxList, pillboxItemList: clientLoad.pillboxItemList }); + // eslint-disable-next-line no-console + console.log('documentList', activeClient?.documentList); } catch (error) { await setErrorDetails(error); } diff --git a/src/components/Pages/Documents.tsx b/src/components/Pages/DocumentPage.tsx similarity index 74% rename from src/components/Pages/Documents.tsx rename to src/components/Pages/DocumentPage.tsx index af400e0..6403962 100644 --- a/src/components/Pages/Documents.tsx +++ b/src/components/Pages/DocumentPage.tsx @@ -1,15 +1,8 @@ import {ChangeEvent} from 'react'; import Form from 'react-bootstrap/Form'; -import Row from 'react-bootstrap/Row'; import React, {useGlobal, useState} from 'reactn'; -export enum UploadFileErrorCode { - Ok, - file_selection_cancelled, - max_file_size_exceeded -} - -const Documents = () => { +const DocumentPage = () => { const [, setErrorDetails] = useGlobal('__errorDetails'); const [isBusy, setIsIsBusy] = useState(false); const defaultFileLabelText = 'Select a File to Upload'; @@ -39,7 +32,7 @@ const Documents = () => { formData.append('single_file', file); setUploadedFileName(file.name); const documentRecord = await documentProvider.uploadFile(formData, 1092); - // TODO: Insert the record into the Document table + alert('documentRecord: ' + JSON.stringify(documentRecord)); } catch (error) { await setErrorDetails(error); @@ -55,18 +48,25 @@ const Documents = () => { }; return ( - - ) => handleFileUpload(event)} - /> -
File exceeds maximum size allowed
-
+
+ + ) => handleFileUpload(event)} + /> +
File exceeds maximum size allowed
+
+ + +

Place holder

+
+
); }; -export default Documents; +export default DocumentPage; diff --git a/src/components/Pages/LandingPage.tsx b/src/components/Pages/LandingPage.tsx index 7f4df58..6ce071c 100644 --- a/src/components/Pages/LandingPage.tsx +++ b/src/components/Pages/LandingPage.tsx @@ -1,5 +1,6 @@ import ClientPage from 'components/Pages/ClientPage'; -import Documents from 'components/Pages/Documents'; +import DocumentPage from 'components/Pages/DocumentPage'; +import RxPage from 'components/Pages/rxPage'; import SettingsPage from 'components/Pages/SettingsPage'; import {ReactNode} from 'react'; import Tab from 'react-bootstrap/Tab'; @@ -9,9 +10,7 @@ import React, {useEffect, useGlobal, useMemo} from 'reactn'; import {IPreferences} from 'reactn/default'; import DiagnosticPage from './DiagnosticPage'; import LoginPage from './LoginPage'; -import ManageDrugPage from './ManageDrugPage'; import ManageOtcPage from './ManageOtcPage'; -import MedicinePage from './MedicinePage'; interface ITitleProps { activeKey: string; @@ -63,13 +62,9 @@ const LandingPage = (props: IProps) => { * Memoized pages to reduce number of re-renders */ const medicinePage = useMemo(() => { - return ; + return ; }, [activeTabKey, preferences]); - const manageDrugPage = useMemo(() => { - return ; - }, [activeTabKey]); - const clientPage = useMemo(() => { return setActiveTabKey('medicine')} />; }, [activeTabKey, setActiveTabKey]); @@ -116,21 +111,15 @@ const LandingPage = (props: IProps) => { {medicinePage} )}
- Manage Rx} - > - {manageDrugPage} - + Manage OTC}> - Documents}> + Documents}> - + Diagnostics}> @@ -138,7 +127,7 @@ const LandingPage = (props: IProps) => { window.location.reload()} /> - Preferences}> + Management}> diff --git a/src/components/Pages/ListGroups/OtcListGroup.tsx b/src/components/Pages/ListGroups/OtcListGroup.tsx index 913908a..8cd5208 100644 --- a/src/components/Pages/ListGroups/OtcListGroup.tsx +++ b/src/components/Pages/ListGroups/OtcListGroup.tsx @@ -1,6 +1,6 @@ import OtcListGroupGrid from 'components/Pages/Grids/OtcListGroupGrid'; import DisabledSpinner from 'components/Pages/ListGroups/DisabledSpinner'; -import {TAB_KEY} from 'components/Pages/MedicinePage'; +import {TAB_KEY} from 'components/Pages/rxPage'; import Button from 'react-bootstrap/Button'; import Form from 'react-bootstrap/Form'; import FormGroup from 'react-bootstrap/FormGroup'; diff --git a/src/components/Pages/ManageDrugPage.tsx b/src/components/Pages/ManageDrugPage.tsx index 348db45..d774346 100644 --- a/src/components/Pages/ManageDrugPage.tsx +++ b/src/components/Pages/ManageDrugPage.tsx @@ -24,15 +24,10 @@ import TabContent from '../../styles/common.css'; import DrugLogEdit from './Modals/DrugLogEdit'; import MedicineEdit from './Modals/MedicineEdit'; -interface IProps { - activeTabKey: string; -} - /** * ManageDrugPage - UI for Displaying, editing and adding Medicine - * @param {IProps} props The props for the component */ -const ManageDrugPage = (props: IProps): JSX.Element | null => { +const ManageDrugPage = (): JSX.Element | null => { const [, setPillboxItemList] = useState([]); const [activeClient, setActiveClient] = useGlobal('activeClient'); const [checkoutList, setCheckoutList] = useState([]); @@ -48,7 +43,6 @@ const ManageDrugPage = (props: IProps): JSX.Element | null => { const [showDeleteMedicine, setShowDeleteMedicine] = useState(0); const [showMedicineEdit, setShowMedicineEdit] = useState(false); const [toast, setToast] = useState(null); - const activeTabKey = props.activeTabKey; // When the activeClient is "active" then deconstruct the activeClient into the lists and clientInfo constants useEffect(() => { @@ -69,7 +63,6 @@ const ManageDrugPage = (props: IProps): JSX.Element | null => { // No need to render if there's not an activeClient or if the activeTabKey isn't 'manage' if (!activeClient || !clientInfo) return null; - if (activeTabKey !== 'manage') return null; /** * Given a DrugLogRecord Update or Insert the record and rehydrate the drugLogList diff --git a/src/components/Pages/RxTabs/RxOtc.tsx b/src/components/Pages/RxTabs/RxOtc.tsx index 89ae905..14ac3df 100644 --- a/src/components/Pages/RxTabs/RxOtc.tsx +++ b/src/components/Pages/RxTabs/RxOtc.tsx @@ -1,6 +1,6 @@ import DrugLogGrid from 'components/Pages/Grids/DrugLogGrid'; import OtcListGroup from 'components/Pages/ListGroups/OtcListGroup'; -import {TAB_KEY} from 'components/Pages/MedicinePage'; +import {TAB_KEY} from 'components/Pages/rxPage'; import DeleteDrugLogModal from 'components/Pages/Modals/DeleteDrugLogModal'; import DrugLogEdit from 'components/Pages/Modals/DrugLogEdit'; import MedicineEdit from 'components/Pages/Modals/MedicineEdit'; diff --git a/src/components/Pages/MedicinePage.tsx b/src/components/Pages/rxPage.tsx similarity index 88% rename from src/components/Pages/MedicinePage.tsx rename to src/components/Pages/rxPage.tsx index cace861..921e552 100644 --- a/src/components/Pages/MedicinePage.tsx +++ b/src/components/Pages/rxPage.tsx @@ -1,3 +1,4 @@ +import ManageDrugPage from 'components/Pages/ManageDrugPage'; import RxHistory from 'components/Pages/RxTabs/RxHistory'; import RxMedicine from 'components/Pages/RxTabs/RxMedicine'; import RxOtc from 'components/Pages/RxTabs/RxOtc'; @@ -18,7 +19,8 @@ export enum TAB_KEY { Medicine = 'med', OTC = 'otc', Pillbox = 'pillbox', - Print = 'print' + Print = 'print', + Manage = 'manage' } interface IProps { @@ -27,10 +29,10 @@ interface IProps { } /** - * MedicinePage - UI for logging prescription & OTC medications as well as pillboxes and medication checkout + * RxPage - UI for logging prescription & OTC medications as well as pillboxes and medication checkout * @param {IProps} props The props for this component */ -const MedicinePage = (props: IProps): JSX.Element | null => { +const RxPage = (props: IProps): JSX.Element | null => { const [activeClient] = useGlobal('activeClient'); const [activePillbox, setActivePillbox] = useState(null); const [activeRxTab, setActiveRxTab] = useState(TAB_KEY.Medicine); @@ -85,7 +87,7 @@ const MedicinePage = (props: IProps): JSX.Element | null => { tabContent[0].style.marginTop = '-15px'; } - // Move MedicinePage rxTabs up for more screen real estate + // Move RxPage rxTabs up for more screen real estate const navElement = document.querySelectorAll('div.medicine-page-tablet > nav.nav'); if (navElement && navElement.length > 0) { navElement[0].style.marginBottom = '-15px'; @@ -253,9 +255,32 @@ const MedicinePage = (props: IProps): JSX.Element | null => { > + setActiveRxTab(TAB_KEY.Manage)} + size={preferences.rxTabSize} + type="radio" + value={TAB_KEY.Manage} + variant="outline-success" + > + Manage Rx + + } + > + + ); }; -export default MedicinePage; +export default RxPage; diff --git a/src/global.d.ts b/src/global.d.ts index 0acd549..8426872 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -4,7 +4,14 @@ import {IMedicineManager} from 'managers/MedicineManager'; import {Authenticated} from 'providers/AuthenticationProvider'; import 'reactn'; import {State} from 'reactn/default'; -import {ClientRecord, DrugLogRecord, MedicineRecord, PillboxItemRecord, PillboxRecord} from 'types/RecordTypes'; +import { + ClientRecord, + DocumentRecord, + DrugLogRecord, + MedicineRecord, + PillboxItemRecord, + PillboxRecord +} from 'types/RecordTypes'; import {IProviders} from 'utility/getInitialState'; /* eslint @typescript-eslint/no-explicit-any: off */ @@ -12,6 +19,7 @@ declare module 'reactn/default' { // Client Type for the activeClient export type TClient = { clientInfo: ClientRecord; + documentList: DocumentRecord[]; drugLogList: DrugLogRecord[]; medicineList: MedicineRecord[]; pillboxList: PillboxRecord[]; diff --git a/src/providers/DocumentProvider.ts b/src/providers/DocumentProvider.ts index 2fa2ee4..e7a43c5 100644 --- a/src/providers/DocumentProvider.ts +++ b/src/providers/DocumentProvider.ts @@ -1,8 +1,13 @@ +import Frak from 'frak/lib/components/Frak'; +import {DocumentRecord} from 'types/RecordTypes'; + export interface IDocumentProvider { setApiKey: (apiKey: string) => void; - uploadFile: (formData: FormData, clientId: number) => Promise; + uploadFile: (formData: FormData, clientId: number) => Promise; + load: (clientId: number) => Promise; } -type DocumentRecord = { + +type DocumentUploadRecord = { Id: number | null; Size: number; FileName: string; @@ -12,11 +17,18 @@ type DocumentRecord = { type UploadResponse = { status: number; success: boolean; - data: null | DocumentRecord; + data: null | DocumentUploadRecord; +}; + +type LoadResponse = { + status: number; + success: boolean; + data: DocumentRecord[]; }; const DocumentProvider = (baseUrl: string): IDocumentProvider => { const _baseUrl = baseUrl; + const _frak = Frak(); let _apiKey = null as string | null; return { @@ -32,9 +44,9 @@ const DocumentProvider = (baseUrl: string): IDocumentProvider => { * Upload a file as a FormData object. Note that Frak isn't used because the data is not JSON * @param {FormData} formData The FormData object containing the name and file * @param {number} clientId The Client PK - * @returns {Promise} A Document record as a promise + * @returns {Promise} A Document record as a promise */ - uploadFile: async (formData: FormData, clientId): Promise => { + uploadFile: async (formData: FormData, clientId): Promise => { const uri = _baseUrl + 'document/upload/' + clientId + '?api_key=' + _apiKey; const response = await fetch(uri, { method: 'POST', @@ -46,8 +58,26 @@ const DocumentProvider = (baseUrl: string): IDocumentProvider => { const responseJSON = (await response.json()) as UploadResponse; if (responseJSON.success) { - return responseJSON.data as DocumentRecord; + return responseJSON.data as DocumentUploadRecord; + } else { + throw response; + } + }, + + /** + * Given a clientId (Resident PK) return all the Document records for the client + * @param {number} clientId Client (Resident) PK + * @returns {Promise} An array of DocumentRecords + */ + load: async (clientId: number): Promise => { + const uri = `${_baseUrl}document/load/${clientId}?api_key=${_apiKey}`; + const response = await _frak.get(uri); + if (response.success) { + return response.data as DocumentRecord[]; } else { + if (response.status === 404) { + return [] as DocumentRecord[]; + } throw response; } } diff --git a/src/types/RecordTypes.ts b/src/types/RecordTypes.ts index fbcab8b..51c3112 100644 --- a/src/types/RecordTypes.ts +++ b/src/types/RecordTypes.ts @@ -15,6 +15,21 @@ export type ClientRecord = { [key: string]: unknown; }; +// ORM record of the Document table +export type DocumentRecord = { + Created?: null | Date; + Description: null | string; + FileName: string; + Id: null | number; + Image: null | string; + MediaType: null | string; + ResidentId: number; + Size: null | number; + Updated?: null | Date; + [key: string]: unknown; + deleted_at?: null | Date; +}; + // ORM record of the MedHistory table export type DrugLogRecord = { Created?: string | null; From 6165c62bce8a0215c6e3dab4856f542d9b06cc43 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 27 Feb 2022 12:27:44 -0700 Subject: [PATCH 06/25] =?UTF-8?q?=E2=9B=B2=20feat/=20=F0=9F=8C=80=20refact?= =?UTF-8?q?or=20Changed=20the=20UI=20and=20changed=20the=20name=20and=20lo?= =?UTF-8?q?cation=20of=20various=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See: #326 --- src/components/Pages/LandingPage.tsx | 2 +- src/components/Pages/ListGroups/OtcListGroup.tsx | 2 +- src/components/Pages/Modals/CheckoutAllModal.tsx | 2 +- src/components/Pages/{rxPage.tsx => RxPage.tsx} | 4 ++-- .../{ManageDrugPage.tsx => RxTabs/ManageRx.tsx} | 12 ++++++------ src/components/Pages/RxTabs/RxOtc.tsx | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) rename src/components/Pages/{rxPage.tsx => RxPage.tsx} (99%) rename src/components/Pages/{ManageDrugPage.tsx => RxTabs/ManageRx.tsx} (97%) diff --git a/src/components/Pages/LandingPage.tsx b/src/components/Pages/LandingPage.tsx index 6ce071c..614bfcf 100644 --- a/src/components/Pages/LandingPage.tsx +++ b/src/components/Pages/LandingPage.tsx @@ -1,6 +1,6 @@ import ClientPage from 'components/Pages/ClientPage'; import DocumentPage from 'components/Pages/DocumentPage'; -import RxPage from 'components/Pages/rxPage'; +import RxPage from 'components/Pages/RxPage'; import SettingsPage from 'components/Pages/SettingsPage'; import {ReactNode} from 'react'; import Tab from 'react-bootstrap/Tab'; diff --git a/src/components/Pages/ListGroups/OtcListGroup.tsx b/src/components/Pages/ListGroups/OtcListGroup.tsx index 8cd5208..06cd0c7 100644 --- a/src/components/Pages/ListGroups/OtcListGroup.tsx +++ b/src/components/Pages/ListGroups/OtcListGroup.tsx @@ -1,6 +1,6 @@ import OtcListGroupGrid from 'components/Pages/Grids/OtcListGroupGrid'; import DisabledSpinner from 'components/Pages/ListGroups/DisabledSpinner'; -import {TAB_KEY} from 'components/Pages/rxPage'; +import {TAB_KEY} from 'components/Pages/RxPage'; import Button from 'react-bootstrap/Button'; import Form from 'react-bootstrap/Form'; import FormGroup from 'react-bootstrap/FormGroup'; diff --git a/src/components/Pages/Modals/CheckoutAllModal.tsx b/src/components/Pages/Modals/CheckoutAllModal.tsx index 4fc9038..bf8a989 100644 --- a/src/components/Pages/Modals/CheckoutAllModal.tsx +++ b/src/components/Pages/Modals/CheckoutAllModal.tsx @@ -15,7 +15,7 @@ interface IProps { /** * Confirmation modal to Check out All Medications and Print - * This component exists to make the code in ManageDrugPage more readable + * This component exists to make the code in ManageRx more readable * @param {IProps} props The props for this component */ const CheckoutAllModal = (props: IProps) => { diff --git a/src/components/Pages/rxPage.tsx b/src/components/Pages/RxPage.tsx similarity index 99% rename from src/components/Pages/rxPage.tsx rename to src/components/Pages/RxPage.tsx index 921e552..1168bd8 100644 --- a/src/components/Pages/rxPage.tsx +++ b/src/components/Pages/RxPage.tsx @@ -1,4 +1,4 @@ -import ManageDrugPage from 'components/Pages/ManageDrugPage'; +import ManageRx from 'components/Pages/RxTabs/ManageRx'; import RxHistory from 'components/Pages/RxTabs/RxHistory'; import RxMedicine from 'components/Pages/RxTabs/RxMedicine'; import RxOtc from 'components/Pages/RxTabs/RxOtc'; @@ -276,7 +276,7 @@ const RxPage = (props: IProps): JSX.Element | null => { } > - + diff --git a/src/components/Pages/ManageDrugPage.tsx b/src/components/Pages/RxTabs/ManageRx.tsx similarity index 97% rename from src/components/Pages/ManageDrugPage.tsx rename to src/components/Pages/RxTabs/ManageRx.tsx index d774346..44106b3 100644 --- a/src/components/Pages/ManageDrugPage.tsx +++ b/src/components/Pages/RxTabs/ManageRx.tsx @@ -20,14 +20,14 @@ import { PillboxItemRecord } from 'types/RecordTypes'; import {clientFullName, getCheckoutList, getDrugName} from 'utility/common'; -import TabContent from '../../styles/common.css'; -import DrugLogEdit from './Modals/DrugLogEdit'; -import MedicineEdit from './Modals/MedicineEdit'; +import TabContent from 'styles/common.css'; +import DrugLogEdit from 'components/Pages/Modals/DrugLogEdit'; +import MedicineEdit from 'components/Pages/Modals/MedicineEdit'; /** - * ManageDrugPage - UI for Displaying, editing and adding Medicine + * ManageRx - UI for Displaying, editing and adding Medicine */ -const ManageDrugPage = (): JSX.Element | null => { +const ManageRx = (): JSX.Element | null => { const [, setPillboxItemList] = useState([]); const [activeClient, setActiveClient] = useGlobal('activeClient'); const [checkoutList, setCheckoutList] = useState([]); @@ -307,4 +307,4 @@ const ManageDrugPage = (): JSX.Element | null => { ); }; -export default ManageDrugPage; +export default ManageRx; diff --git a/src/components/Pages/RxTabs/RxOtc.tsx b/src/components/Pages/RxTabs/RxOtc.tsx index 14ac3df..6b6f6a4 100644 --- a/src/components/Pages/RxTabs/RxOtc.tsx +++ b/src/components/Pages/RxTabs/RxOtc.tsx @@ -1,6 +1,6 @@ import DrugLogGrid from 'components/Pages/Grids/DrugLogGrid'; import OtcListGroup from 'components/Pages/ListGroups/OtcListGroup'; -import {TAB_KEY} from 'components/Pages/rxPage'; +import {TAB_KEY} from 'components/Pages/RxPage'; import DeleteDrugLogModal from 'components/Pages/Modals/DeleteDrugLogModal'; import DrugLogEdit from 'components/Pages/Modals/DrugLogEdit'; import MedicineEdit from 'components/Pages/Modals/MedicineEdit'; From 56cd566cbabfe73e2cfd8d227de159b0863890c6 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 28 Feb 2022 10:57:02 -0700 Subject: [PATCH 07/25] =?UTF-8?q?=E2=9B=B2=20feat/=20=F0=9F=8C=80=20refact?= =?UTF-8?q?or=20Changed=20more=20of=20the=20UI=20and=20changed=20the=20nam?= =?UTF-8?q?e=20and=20location=20of=20various=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added Management Tab - Moved Settings and Manage OTC pages into the Management Tab See: #326 --- src/components/Pages/LandingPage.tsx | 22 +++------ src/components/Pages/ManagementPage.tsx | 64 +++++++++++++++++++++++++ src/components/Pages/RxPage.tsx | 28 ++++++++++- 3 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 src/components/Pages/ManagementPage.tsx diff --git a/src/components/Pages/LandingPage.tsx b/src/components/Pages/LandingPage.tsx index 614bfcf..132a02b 100644 --- a/src/components/Pages/LandingPage.tsx +++ b/src/components/Pages/LandingPage.tsx @@ -1,7 +1,6 @@ import ClientPage from 'components/Pages/ClientPage'; -import DocumentPage from 'components/Pages/DocumentPage'; +import ManagementPage from 'components/Pages/ManagementPage'; import RxPage from 'components/Pages/RxPage'; -import SettingsPage from 'components/Pages/SettingsPage'; import {ReactNode} from 'react'; import Tab from 'react-bootstrap/Tab'; import Tabs from 'react-bootstrap/Tabs'; @@ -10,7 +9,6 @@ import React, {useEffect, useGlobal, useMemo} from 'reactn'; import {IPreferences} from 'reactn/default'; import DiagnosticPage from './DiagnosticPage'; import LoginPage from './LoginPage'; -import ManageOtcPage from './ManageOtcPage'; interface ITitleProps { activeKey: string; @@ -49,7 +47,7 @@ const LandingPage = (props: IProps) => { // Observer to show / hide tabs based on if logged in and if a client has been selected useEffect(() => { - ['resident', 'medicine', 'manage', 'manage-otc'].map((tab) => { + ['resident', 'medicine', 'management'].map((tab) => { const element = document.getElementById('landing-page-tabs-tab-' + tab); if (element) { if (tab === 'resident' || tab === 'manage-otc') element.style.display = apiKey ? 'block' : 'none'; @@ -106,32 +104,24 @@ const LandingPage = (props: IProps) => { Clients}> {clientPage} + Rx}> {activeClient && activeTabKey === 'medicine' && ( {medicinePage} )} - Manage OTC}> - - - - - Documents}> + Management}> - + + Diagnostics}> window.location.reload()} /> - Management}> - - - - ); }; diff --git a/src/components/Pages/ManagementPage.tsx b/src/components/Pages/ManagementPage.tsx new file mode 100644 index 0000000..41ecafd --- /dev/null +++ b/src/components/Pages/ManagementPage.tsx @@ -0,0 +1,64 @@ +import ManageOtcPage from 'components/Pages/ManageOtcPage'; +import SettingsPage from 'components/Pages/SettingsPage'; +import Tab from 'react-bootstrap/Tab'; +import Tabs from 'react-bootstrap/Tabs'; +import ToggleButton from 'react-bootstrap/ToggleButton'; +import React, {useState} from 'reactn'; + +const ManagementPage = () => { + const [activeManagementKey, setActiveManagementKey] = useState('otc'); + return ( + setActiveManagementKey(s || 'otc')} + > + setActiveManagementKey('otc')} + size="sm" + type="radio" + value="otc" + variant="outline-success" + > + Manage OTC + + } + > + + + + + setActiveManagementKey('settings')} + size="sm" + type="radio" + value="settings" + variant="outline-success" + > + Settings + + } + > + + + + + + ); +}; + +export default ManagementPage; diff --git a/src/components/Pages/RxPage.tsx b/src/components/Pages/RxPage.tsx index 1168bd8..73fb0c6 100644 --- a/src/components/Pages/RxPage.tsx +++ b/src/components/Pages/RxPage.tsx @@ -1,3 +1,4 @@ +import DocumentPage from 'components/Pages/DocumentPage'; import ManageRx from 'components/Pages/RxTabs/ManageRx'; import RxHistory from 'components/Pages/RxTabs/RxHistory'; import RxMedicine from 'components/Pages/RxTabs/RxMedicine'; @@ -20,7 +21,8 @@ export enum TAB_KEY { OTC = 'otc', Pillbox = 'pillbox', Print = 'print', - Manage = 'manage' + Manage = 'manage', + Document = 'document' } interface IProps { @@ -278,6 +280,30 @@ const RxPage = (props: IProps): JSX.Element | null => { > + + setActiveRxTab(TAB_KEY.Document)} + size={preferences.rxTabSize} + type="radio" + value={TAB_KEY.Document} + variant="outline-success" + > + Documents + + } + > + + ); From ef85021065f2dcededafe6318f0c90c2d1d9a524 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 28 Feb 2022 11:39:06 -0700 Subject: [PATCH 08/25] =?UTF-8?q?=F0=9F=8C=80=20refactor=20Created=20a=20M?= =?UTF-8?q?anagementTabs=20directory=20and=20moved=20and=20renamed=20the?= =?UTF-8?q?=20ManageOtc=20and=20Settings=20pages=20into=20this=20directory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Pages/ManagementPage.tsx | 8 ++++---- .../{ManageOtcPage.tsx => ManagementTabs/ManageOtc.tsx} | 8 ++++---- .../{SettingsPage.tsx => ManagementTabs/Settings.tsx} | 4 ++-- src/components/Pages/RxPage.tsx | 4 ++-- .../Pages/{DocumentPage.tsx => RxTabs/Documents.tsx} | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) rename src/components/Pages/{ManageOtcPage.tsx => ManagementTabs/ManageOtc.tsx} (96%) rename src/components/Pages/{SettingsPage.tsx => ManagementTabs/Settings.tsx} (97%) rename src/components/Pages/{DocumentPage.tsx => RxTabs/Documents.tsx} (97%) diff --git a/src/components/Pages/ManagementPage.tsx b/src/components/Pages/ManagementPage.tsx index 41ecafd..d33ba5b 100644 --- a/src/components/Pages/ManagementPage.tsx +++ b/src/components/Pages/ManagementPage.tsx @@ -1,5 +1,5 @@ -import ManageOtcPage from 'components/Pages/ManageOtcPage'; -import SettingsPage from 'components/Pages/SettingsPage'; +import ManageOtc from 'components/Pages/ManagementTabs/ManageOtc'; +import Settings from 'components/Pages/ManagementTabs/Settings'; import Tab from 'react-bootstrap/Tab'; import Tabs from 'react-bootstrap/Tabs'; import ToggleButton from 'react-bootstrap/ToggleButton'; @@ -32,7 +32,7 @@ const ManagementPage = () => { } > - + { } > - + diff --git a/src/components/Pages/ManageOtcPage.tsx b/src/components/Pages/ManagementTabs/ManageOtc.tsx similarity index 96% rename from src/components/Pages/ManageOtcPage.tsx rename to src/components/Pages/ManagementTabs/ManageOtc.tsx index 147a91a..67393b4 100644 --- a/src/components/Pages/ManageOtcPage.tsx +++ b/src/components/Pages/ManagementTabs/ManageOtc.tsx @@ -7,17 +7,17 @@ import Form from 'react-bootstrap/Form'; import Row from 'react-bootstrap/Row'; import React, {useEffect, useGlobal, useRef, useState} from 'reactn'; import {DrugLogRecord, MedicineRecord, newMedicineRecord} from 'types/RecordTypes'; -import MedicineEdit from './Modals/MedicineEdit'; +import MedicineEdit from 'components/Pages/Modals/MedicineEdit'; interface IProps { activeTabKey: string; } /** - * ManageOtcPage - UI for Displaying, editing and adding OTC drugs + * ManageOtc - UI for Displaying, editing and adding OTC drugs * @param {IProps} props The props for the component */ -const ManageOtcPage = (props: IProps): JSX.Element | null => { +const ManageOtc = (props: IProps): JSX.Element | null => { const [allowDelete, setAllowDelete] = useState(false); const [medicineInfo, setMedicineInfo] = useState(null); const [mm] = useGlobal('medicineManager'); @@ -167,4 +167,4 @@ const ManageOtcPage = (props: IProps): JSX.Element | null => { ); }; -export default ManageOtcPage; +export default ManageOtc; diff --git a/src/components/Pages/SettingsPage.tsx b/src/components/Pages/ManagementTabs/Settings.tsx similarity index 97% rename from src/components/Pages/SettingsPage.tsx rename to src/components/Pages/ManagementTabs/Settings.tsx index a57bd97..bde1023 100644 --- a/src/components/Pages/SettingsPage.tsx +++ b/src/components/Pages/ManagementTabs/Settings.tsx @@ -5,7 +5,7 @@ import React, {useEffect, useGlobal} from 'reactn'; import 'styles/neumorphism/settings.css'; import {setStickyState} from 'utility/common'; -const SettingsPage = () => { +const Settings = () => { const [preferences, setPreferences] = useGlobal('preferences'); useEffect(() => { if (preferences) { @@ -50,4 +50,4 @@ const SettingsPage = () => { ); }; -export default SettingsPage; +export default Settings; diff --git a/src/components/Pages/RxPage.tsx b/src/components/Pages/RxPage.tsx index 73fb0c6..723f4ec 100644 --- a/src/components/Pages/RxPage.tsx +++ b/src/components/Pages/RxPage.tsx @@ -1,4 +1,4 @@ -import DocumentPage from 'components/Pages/DocumentPage'; +import Documents from 'components/Pages/RxTabs/Documents'; import ManageRx from 'components/Pages/RxTabs/ManageRx'; import RxHistory from 'components/Pages/RxTabs/RxHistory'; import RxMedicine from 'components/Pages/RxTabs/RxMedicine'; @@ -302,7 +302,7 @@ const RxPage = (props: IProps): JSX.Element | null => { } > - + diff --git a/src/components/Pages/DocumentPage.tsx b/src/components/Pages/RxTabs/Documents.tsx similarity index 97% rename from src/components/Pages/DocumentPage.tsx rename to src/components/Pages/RxTabs/Documents.tsx index 6403962..fe0da82 100644 --- a/src/components/Pages/DocumentPage.tsx +++ b/src/components/Pages/RxTabs/Documents.tsx @@ -2,7 +2,7 @@ import {ChangeEvent} from 'react'; import Form from 'react-bootstrap/Form'; import React, {useGlobal, useState} from 'reactn'; -const DocumentPage = () => { +const Documents = () => { const [, setErrorDetails] = useGlobal('__errorDetails'); const [isBusy, setIsIsBusy] = useState(false); const defaultFileLabelText = 'Select a File to Upload'; @@ -69,4 +69,4 @@ const DocumentPage = () => { ); }; -export default DocumentPage; +export default Documents; From c8057b977be302e50f0863a3d9a0413609bc1db7 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 2 Mar 2022 01:28:53 -0700 Subject: [PATCH 09/25] =?UTF-8?q?=F0=9F=8C=80=20refactor=20=F0=9F=90=9B=20?= =?UTF-8?q?bug=20Changed=20the=20name=20of=20an=20enum=20from=20`TAB=5FKEY?= =?UTF-8?q?`=20TO=20`RX=5FTAB=5FKEY`=20and=20fixed=20a=20tooltip=20bleedin?= =?UTF-8?q?g=20through=20when=20the=20tab=20it=20was=20contained=20in=20wa?= =?UTF-8?q?sn't=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pages/ListGroups/OtcListGroup.tsx | 8 +- src/components/Pages/ManagementPage.tsx | 2 +- .../Pages/ManagementTabs/ManageOtc.tsx | 4 +- src/components/Pages/RxPage.tsx | 88 +++++++++---------- src/components/Pages/RxTabs/ManageRx.tsx | 23 ++++- src/components/Pages/RxTabs/RxOtc.tsx | 6 +- 6 files changed, 75 insertions(+), 56 deletions(-) diff --git a/src/components/Pages/ListGroups/OtcListGroup.tsx b/src/components/Pages/ListGroups/OtcListGroup.tsx index 06cd0c7..36d7b5f 100644 --- a/src/components/Pages/ListGroups/OtcListGroup.tsx +++ b/src/components/Pages/ListGroups/OtcListGroup.tsx @@ -1,6 +1,6 @@ import OtcListGroupGrid from 'components/Pages/Grids/OtcListGroupGrid'; import DisabledSpinner from 'components/Pages/ListGroups/DisabledSpinner'; -import {TAB_KEY} from 'components/Pages/RxPage'; +import {RX_TAB_KEY} from 'components/Pages/RxPage'; import Button from 'react-bootstrap/Button'; import Form from 'react-bootstrap/Form'; import FormGroup from 'react-bootstrap/FormGroup'; @@ -14,7 +14,7 @@ import ShadowBox from '../Buttons/ShadowBox'; interface IProps { activeOtc: MedicineRecord | null; - activeRxTab: TAB_KEY; + activeRxTab: RX_TAB_KEY; disabled?: boolean; drugLogList: DrugLogRecord[]; editOtcMedicine: (m: MedicineRecord) => void; @@ -47,10 +47,10 @@ const OtcListGroup = (props: IProps): JSX.Element | null => { const lastTakenVariant = lastTaken && lastTaken >= 8 ? 'primary' : getLastTakenVariant(lastTaken); const searchReference = useRef(null); - const [activeRxTab, setActiveRxTab] = useState(props.activeRxTab); + const [activeRxTab, setActiveRxTab] = useState(props.activeRxTab); useEffect(() => { setActiveRxTab(props.activeRxTab); - if (props.activeRxTab === TAB_KEY.OTC) searchReference?.current?.focus(); + if (props.activeRxTab === RX_TAB_KEY.OTC) searchReference?.current?.focus(); }, [activeRxTab, props.activeRxTab]); // Filter the otcList by the search textbox value diff --git a/src/components/Pages/ManagementPage.tsx b/src/components/Pages/ManagementPage.tsx index d33ba5b..05be91a 100644 --- a/src/components/Pages/ManagementPage.tsx +++ b/src/components/Pages/ManagementPage.tsx @@ -32,7 +32,7 @@ const ManagementPage = () => { } > - + { const [searchText, setSearchText] = useState(''); const [showDeleteMedicine, setShowDeleteMedicine] = useState(0); const [showMedicineEdit, setShowMedicineEdit] = useState(false); - const activeTabKey = props.activeTabKey; + const activeTabKey = props.activeManagementKey; const focusReference = useRef(null); useEffect(() => { diff --git a/src/components/Pages/RxPage.tsx b/src/components/Pages/RxPage.tsx index 723f4ec..091eb71 100644 --- a/src/components/Pages/RxPage.tsx +++ b/src/components/Pages/RxPage.tsx @@ -15,7 +15,7 @@ import {DrugLogRecord, PillboxRecord} from 'types/RecordTypes'; import {getCheckoutList} from 'utility/common'; // Active Rx tab states -export enum TAB_KEY { +export enum RX_TAB_KEY { History = 'history', Medicine = 'med', OTC = 'otc', @@ -37,7 +37,7 @@ interface IProps { const RxPage = (props: IProps): JSX.Element | null => { const [activeClient] = useGlobal('activeClient'); const [activePillbox, setActivePillbox] = useState(null); - const [activeRxTab, setActiveRxTab] = useState(TAB_KEY.Medicine); + const [activeRxTab, setActiveRxTab] = useState(RX_TAB_KEY.Medicine); const [checkoutList, setCheckoutList] = useState([]); const [mm] = useGlobal('medicineManager'); const [otcList] = useGlobal('otcList'); @@ -58,27 +58,27 @@ const RxPage = (props: IProps): JSX.Element | null => { // Observer to show / hide RxTabs useEffect(() => { if (activeClient) { - const historyElement = document.getElementById('medicine-page-tabs-tab-' + TAB_KEY.History); + const historyElement = document.getElementById('medicine-page-tabs-tab-' + RX_TAB_KEY.History); if (historyElement) { historyElement.style.display = activeClient.drugLogList.length === 0 ? 'none' : 'block'; - if (activeRxTab === TAB_KEY.History && activeClient.drugLogList.length === 0) { - setActiveRxTab(TAB_KEY.Medicine); + if (activeRxTab === RX_TAB_KEY.History && activeClient.drugLogList.length === 0) { + setActiveRxTab(RX_TAB_KEY.Medicine); } } - const pillboxElement = document.getElementById('medicine-page-tabs-tab-' + TAB_KEY.Pillbox); + const pillboxElement = document.getElementById('medicine-page-tabs-tab-' + RX_TAB_KEY.Pillbox); if (pillboxElement) { pillboxElement.style.display = activeClient.medicineList.length < 5 ? 'none' : 'block'; - if (activeRxTab === TAB_KEY.Pillbox && activeClient.medicineList.length < 5) { - setActiveRxTab(TAB_KEY.Medicine); + if (activeRxTab === RX_TAB_KEY.Pillbox && activeClient.medicineList.length < 5) { + setActiveRxTab(RX_TAB_KEY.Medicine); } } - const printElement = document.getElementById('medicine-page-tabs-tab-' + TAB_KEY.Print); + const printElement = document.getElementById('medicine-page-tabs-tab-' + RX_TAB_KEY.Print); if (printElement) { printElement.style.display = checkoutList.length === 0 ? 'none' : 'block'; - if (activeRxTab === TAB_KEY.Print && checkoutList.length === 0) { - setActiveRxTab(TAB_KEY.Medicine); + if (activeRxTab === RX_TAB_KEY.Print && checkoutList.length === 0) { + setActiveRxTab(RX_TAB_KEY.Medicine); } } } @@ -113,24 +113,24 @@ const RxPage = (props: IProps): JSX.Element | null => { setActiveRxTab((key as TAB_KEY) || TAB_KEY.Medicine)} + defaultActiveKey={RX_TAB_KEY.Medicine} + onSelect={(key) => setActiveRxTab((key as RX_TAB_KEY) || RX_TAB_KEY.Medicine)} > setActiveRxTab(TAB_KEY.Medicine)} + onChange={() => setActiveRxTab(RX_TAB_KEY.Medicine)} size={preferences.rxTabSize} type="radio" - value={TAB_KEY.Medicine} + value={RX_TAB_KEY.Medicine} variant="outline-success" > Medicine @@ -141,26 +141,26 @@ const RxPage = (props: IProps): JSX.Element | null => { mm={mm} pillboxSelected={(id) => { setActivePillbox(pillboxList.find((p) => p.Id === id) || null); - setActiveRxTab(TAB_KEY.Pillbox); + setActiveRxTab(RX_TAB_KEY.Pillbox); }} /> setActiveRxTab(TAB_KEY.OTC)} + onChange={() => setActiveRxTab(RX_TAB_KEY.OTC)} size={preferences.rxTabSize} type="radio" - value={TAB_KEY.OTC} + value={RX_TAB_KEY.OTC} variant="outline-success" > OTC @@ -171,20 +171,20 @@ const RxPage = (props: IProps): JSX.Element | null => { setActiveRxTab(TAB_KEY.History)} + onChange={() => setActiveRxTab(RX_TAB_KEY.History)} size={preferences.rxTabSize} type="radio" - value={TAB_KEY.History} + value={RX_TAB_KEY.History} variant="outline-success" > History @@ -196,27 +196,27 @@ const RxPage = (props: IProps): JSX.Element | null => { otcList={otcList} onPillboxSelected={(id) => { setActivePillbox(pillboxList.find((p) => p.Id === id) || null); - setActiveRxTab(TAB_KEY.Pillbox); + setActiveRxTab(RX_TAB_KEY.Pillbox); }} /> setActiveRxTab(TAB_KEY.Pillbox)} + onChange={() => setActiveRxTab(RX_TAB_KEY.Pillbox)} size={preferences.rxTabSize} type="radio" - value={TAB_KEY.Pillbox} + value={RX_TAB_KEY.Pillbox} variant="outline-success" > Pillbox @@ -233,19 +233,19 @@ const RxPage = (props: IProps): JSX.Element | null => { setActiveRxTab(TAB_KEY.Print)} + onChange={() => setActiveRxTab(RX_TAB_KEY.Print)} size={preferences.rxTabSize} type="radio" - value={TAB_KEY.Print} + value={RX_TAB_KEY.Print} variant="outline-success" > @@ -258,44 +258,44 @@ const RxPage = (props: IProps): JSX.Element | null => { setActiveRxTab(TAB_KEY.Manage)} + onChange={() => setActiveRxTab(RX_TAB_KEY.Manage)} size={preferences.rxTabSize} type="radio" - value={TAB_KEY.Manage} + value={RX_TAB_KEY.Manage} variant="outline-success" > Manage Rx } > - + setActiveRxTab(TAB_KEY.Document)} + onChange={() => setActiveRxTab(RX_TAB_KEY.Document)} size={preferences.rxTabSize} type="radio" - value={TAB_KEY.Document} + value={RX_TAB_KEY.Document} variant="outline-success" > Documents diff --git a/src/components/Pages/RxTabs/ManageRx.tsx b/src/components/Pages/RxTabs/ManageRx.tsx index 44106b3..db50f81 100644 --- a/src/components/Pages/RxTabs/ManageRx.tsx +++ b/src/components/Pages/RxTabs/ManageRx.tsx @@ -4,6 +4,7 @@ import ManageDrugGrid from 'components/Pages/Grids/ManageDrugGrid'; import CheckoutListGroup from 'components/Pages/ListGroups/CheckoutListGroup'; import CheckoutAllModal from 'components/Pages/Modals/CheckoutAllModal'; import DeleteMedicineModal from 'components/Pages/Modals/DeleteMedicineModal'; +import {RX_TAB_KEY} from 'components/Pages/RxPage'; import DrugLogToast from 'components/Pages/Toasts/DrugLogToast'; import Badge from 'react-bootstrap/Badge'; import Button from 'react-bootstrap/Button'; @@ -24,10 +25,15 @@ import TabContent from 'styles/common.css'; import DrugLogEdit from 'components/Pages/Modals/DrugLogEdit'; import MedicineEdit from 'components/Pages/Modals/MedicineEdit'; +interface IProps { + rxTabKey: string; +} + /** * ManageRx - UI for Displaying, editing and adding Medicine + * @param {IProps} props The props for this component */ -const ManageRx = (): JSX.Element | null => { +const ManageRx = (props: IProps): JSX.Element | null => { const [, setPillboxItemList] = useState([]); const [activeClient, setActiveClient] = useGlobal('activeClient'); const [checkoutList, setCheckoutList] = useState([]); @@ -44,6 +50,11 @@ const ManageRx = (): JSX.Element | null => { const [showMedicineEdit, setShowMedicineEdit] = useState(false); const [toast, setToast] = useState(null); + const [rxTabKey, setRxTabKey] = useState(props.rxTabKey); + useEffect(() => { + setRxTabKey(props.rxTabKey); + }, [props.rxTabKey]); + // When the activeClient is "active" then deconstruct the activeClient into the lists and clientInfo constants useEffect(() => { if (activeClient) { @@ -187,6 +198,8 @@ const ManageRx = (): JSX.Element | null => { return checkoutList.find((c) => c.MedicineId === m.Id); }); + if (rxTabKey !== RX_TAB_KEY.Manage) return null; + return (
@@ -210,7 +223,13 @@ const ManageRx = (): JSX.Element | null => { 0 && !showCheckoutAllMeds && !showCheckoutPrint && !showMedicineEdit} + show={ + checkoutList.length > 0 && + !showCheckoutAllMeds && + !showCheckoutPrint && + !showMedicineEdit && + rxTabKey === RX_TAB_KEY.Manage + } tooltip={'At least one drug is already checked out'} > + + + + + ); + }; + + const tableProps = {...props} as ITableProps; + delete tableProps.onDelete; + delete tableProps.onDownload; + delete tableProps.fileList; + + return ( + + + + + + + + + + + + {fileList.map((p) => FileRow(p))} +
FilenameDescriptionTypeSize
+ ); +}; + +export default FileGrid; diff --git a/src/components/Pages/RxPage.tsx b/src/components/Pages/RxPage.tsx index 091eb71..39f3b24 100644 --- a/src/components/Pages/RxPage.tsx +++ b/src/components/Pages/RxPage.tsx @@ -1,4 +1,4 @@ -import Documents from 'components/Pages/RxTabs/Documents'; +import Files from 'components/Pages/RxTabs/Files'; import ManageRx from 'components/Pages/RxTabs/ManageRx'; import RxHistory from 'components/Pages/RxTabs/RxHistory'; import RxMedicine from 'components/Pages/RxTabs/RxMedicine'; @@ -302,7 +302,7 @@ const RxPage = (props: IProps): JSX.Element | null => { } > - + diff --git a/src/components/Pages/RxTabs/Documents.tsx b/src/components/Pages/RxTabs/Files.tsx similarity index 74% rename from src/components/Pages/RxTabs/Documents.tsx rename to src/components/Pages/RxTabs/Files.tsx index fe0da82..44da370 100644 --- a/src/components/Pages/RxTabs/Documents.tsx +++ b/src/components/Pages/RxTabs/Files.tsx @@ -1,15 +1,27 @@ +import FileGrid from 'components/Pages/Grids/FileGrid'; +import {RX_TAB_KEY} from 'components/Pages/RxPage'; import {ChangeEvent} from 'react'; import Form from 'react-bootstrap/Form'; import React, {useGlobal, useState} from 'reactn'; +import {TClient} from 'reactn/default'; -const Documents = () => { +interface IProps { + activeClient: TClient; + rxTabKey: string; +} + +const Files = (props: IProps) => { const [, setErrorDetails] = useGlobal('__errorDetails'); const [isBusy, setIsIsBusy] = useState(false); const defaultFileLabelText = 'Select a File to Upload'; const [uploadedFileName, setUploadedFileName] = useState(defaultFileLabelText); const [invalidMaxSize, setInvalidMaxSize] = useState(false); const [providers] = useGlobal('providers'); - const documentProvider = providers.documentProvider; + const documentProvider = providers.fileProvider; + const fileList = props.activeClient.fileList; + + const clientInfo = props.activeClient.clientInfo; + const activeRxTab = props.rxTabKey; /** * Handle when the user clicked the Select a File to Upload component @@ -31,7 +43,7 @@ const Documents = () => { const formData = new FormData(); formData.append('single_file', file); setUploadedFileName(file.name); - const documentRecord = await documentProvider.uploadFile(formData, 1092); + const documentRecord = await documentProvider.uploadFile(formData, clientInfo.Id as number); alert('documentRecord: ' + JSON.stringify(documentRecord)); } catch (error) { @@ -47,6 +59,8 @@ const Documents = () => { } }; + if (activeRxTab !== RX_TAB_KEY.Document) return null; + return ( @@ -62,11 +76,17 @@ const Documents = () => {
File exceeds maximum size allowed
- -

Place holder

-
+ { + + alert('delete: ' + d)} + onDownload={(d) => alert('download: ' + d)} + /> + + } ); }; -export default Documents; +export default Files; diff --git a/src/global.d.ts b/src/global.d.ts index 8426872..bf14bbf 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -6,7 +6,7 @@ import 'reactn'; import {State} from 'reactn/default'; import { ClientRecord, - DocumentRecord, + FileRecord, DrugLogRecord, MedicineRecord, PillboxItemRecord, @@ -19,7 +19,7 @@ declare module 'reactn/default' { // Client Type for the activeClient export type TClient = { clientInfo: ClientRecord; - documentList: DocumentRecord[]; + fileList: FileRecord[]; drugLogList: DrugLogRecord[]; medicineList: MedicineRecord[]; pillboxList: PillboxRecord[]; diff --git a/src/providers/DocumentProvider.ts b/src/providers/FileProvider.ts similarity index 62% rename from src/providers/DocumentProvider.ts rename to src/providers/FileProvider.ts index e7a43c5..d2d778e 100644 --- a/src/providers/DocumentProvider.ts +++ b/src/providers/FileProvider.ts @@ -1,13 +1,13 @@ import Frak from 'frak/lib/components/Frak'; -import {DocumentRecord} from 'types/RecordTypes'; +import {FileRecord} from 'types/RecordTypes'; -export interface IDocumentProvider { +export interface IFileProvider { setApiKey: (apiKey: string) => void; - uploadFile: (formData: FormData, clientId: number) => Promise; - load: (clientId: number) => Promise; + uploadFile: (formData: FormData, clientId: number) => Promise; + load: (clientId: number) => Promise; } -type DocumentUploadRecord = { +type FileUploadRecord = { Id: number | null; Size: number; FileName: string; @@ -17,16 +17,16 @@ type DocumentUploadRecord = { type UploadResponse = { status: number; success: boolean; - data: null | DocumentUploadRecord; + data: null | FileUploadRecord; }; type LoadResponse = { status: number; success: boolean; - data: DocumentRecord[]; + data: FileRecord[]; }; -const DocumentProvider = (baseUrl: string): IDocumentProvider => { +const FileProvider = (baseUrl: string): IFileProvider => { const _baseUrl = baseUrl; const _frak = Frak(); let _apiKey = null as string | null; @@ -44,10 +44,10 @@ const DocumentProvider = (baseUrl: string): IDocumentProvider => { * Upload a file as a FormData object. Note that Frak isn't used because the data is not JSON * @param {FormData} formData The FormData object containing the name and file * @param {number} clientId The Client PK - * @returns {Promise} A Document record as a promise + * @returns {Promise} A FileUploadRecord object as a promise */ - uploadFile: async (formData: FormData, clientId): Promise => { - const uri = _baseUrl + 'document/upload/' + clientId + '?api_key=' + _apiKey; + uploadFile: async (formData: FormData, clientId): Promise => { + const uri = _baseUrl + 'file/upload/' + clientId + '?api_key=' + _apiKey; const response = await fetch(uri, { method: 'POST', body: formData, @@ -58,25 +58,25 @@ const DocumentProvider = (baseUrl: string): IDocumentProvider => { const responseJSON = (await response.json()) as UploadResponse; if (responseJSON.success) { - return responseJSON.data as DocumentUploadRecord; + return responseJSON.data as FileUploadRecord; } else { throw response; } }, /** - * Given a clientId (Resident PK) return all the Document records for the client + * Given a clientId (Resident PK) return all the File records for the client * @param {number} clientId Client (Resident) PK - * @returns {Promise} An array of DocumentRecords + * @returns {Promise} An array of FileRecords */ - load: async (clientId: number): Promise => { - const uri = `${_baseUrl}document/load/${clientId}?api_key=${_apiKey}`; + load: async (clientId: number): Promise => { + const uri = `${_baseUrl}file/load/${clientId}?api_key=${_apiKey}`; const response = await _frak.get(uri); if (response.success) { - return response.data as DocumentRecord[]; + return response.data as FileRecord[]; } else { if (response.status === 404) { - return [] as DocumentRecord[]; + return [] as FileRecord[]; } throw response; } @@ -84,4 +84,4 @@ const DocumentProvider = (baseUrl: string): IDocumentProvider => { }; }; -export default DocumentProvider; +export default FileProvider; diff --git a/src/types/RecordTypes.ts b/src/types/RecordTypes.ts index 51c3112..3dcf361 100644 --- a/src/types/RecordTypes.ts +++ b/src/types/RecordTypes.ts @@ -16,7 +16,7 @@ export type ClientRecord = { }; // ORM record of the Document table -export type DocumentRecord = { +export type FileRecord = { Created?: null | Date; Description: null | string; FileName: string; diff --git a/src/utility/getInitialState.ts b/src/utility/getInitialState.ts index 8c969a3..9fafcf2 100644 --- a/src/utility/getInitialState.ts +++ b/src/utility/getInitialState.ts @@ -1,6 +1,6 @@ import ClientManager from 'managers/ClientManager'; import ClientProvider, {IClientProvider} from 'providers/ClientProvider'; -import DocumentProvider, {IDocumentProvider} from 'providers/DocumentProvider'; +import FileProvider, {IFileProvider} from 'providers/FileProvider'; import {State} from 'reactn/default'; import {ClientRecord, MedicineRecord} from 'types/RecordTypes'; import AuthManager from '../managers/AuthManager'; @@ -15,7 +15,7 @@ import PinProvider, {IPinProvider} from 'providers/PinProvider'; export interface IProviders { authenticationProvider: IAuthenticationProvider; clientProvider: IClientProvider; - documentProvider: IDocumentProvider; + fileProvider: IFileProvider; medicineProvider: IMedicineProvider; medHistoryProvider: IMedHistoryProvider; pillboxProvider: IPillboxProvider; @@ -38,7 +38,7 @@ const getInitialState = () => { const providers = { authenticationProvider: AuthenticationProvider(baseUrl), clientProvider: ClientProvider(baseUrl), - documentProvider: DocumentProvider(baseUrl), + fileProvider: FileProvider(baseUrl), medHistoryProvider: MedHistoryProvider(baseUrl), medicineProvider: MedicineProvider(baseUrl), pillboxItemProvider: PillboxItemProvider(baseUrl), @@ -51,7 +51,7 @@ const getInitialState = () => { */ setApi: async (apiKey: string): Promise => { await providers.clientProvider.setApiKey(apiKey); - await providers.documentProvider.setApiKey(apiKey); + await providers.fileProvider.setApiKey(apiKey); await providers.medHistoryProvider.setApiKey(apiKey); await providers.medicineProvider.setApiKey(apiKey); await providers.pillboxItemProvider.setApiKey(apiKey); From e4a783e747ad2fb8c34f4dbdd6ddd6bdd20276eb Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 2 Mar 2022 06:07:06 -0700 Subject: [PATCH 12/25] =?UTF-8?q?=F0=9F=90=9B=20bug=20Fixed=20a=20minor=20?= =?UTF-8?q?bug=20that=20was=20preventing=20the=20Manage=20OTC=20from=20ren?= =?UTF-8?q?dering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Pages/ManagementTabs/ManageOtc.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Pages/ManagementTabs/ManageOtc.tsx b/src/components/Pages/ManagementTabs/ManageOtc.tsx index acfbc2a..8c122ee 100644 --- a/src/components/Pages/ManagementTabs/ManageOtc.tsx +++ b/src/components/Pages/ManagementTabs/ManageOtc.tsx @@ -52,7 +52,7 @@ const ManageOtc = (props: IProps): JSX.Element | null => { }, [otcList, searchText]); // If this tab isn't active then don't render - if (activeTabKey !== 'manage-otc') return null; + if (activeTabKey !== 'otc') return null; /** * Given a MedicineRecord object Update or Insert the record and rehydrate the global otcList From 407fdca64e113f90085c3c7dd07b215e7107ac5f Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 2 Mar 2022 06:11:33 -0700 Subject: [PATCH 13/25] =?UTF-8?q?=F0=9F=90=9B=20bug=20Fixed=20a=20minor=20?= =?UTF-8?q?bug=20that=20was=20preventing=20the=20Management=20tab=20from?= =?UTF-8?q?=20being=20displayed=20when=20a=20user=20logs=20in=20successful?= =?UTF-8?q?ly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Pages/LandingPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Pages/LandingPage.tsx b/src/components/Pages/LandingPage.tsx index 132a02b..862e144 100644 --- a/src/components/Pages/LandingPage.tsx +++ b/src/components/Pages/LandingPage.tsx @@ -50,7 +50,7 @@ const LandingPage = (props: IProps) => { ['resident', 'medicine', 'management'].map((tab) => { const element = document.getElementById('landing-page-tabs-tab-' + tab); if (element) { - if (tab === 'resident' || tab === 'manage-otc') element.style.display = apiKey ? 'block' : 'none'; + if (tab === 'resident' || tab === 'management') element.style.display = apiKey ? 'block' : 'none'; else element.style.display = apiKey && activeClient ? 'block' : 'none'; } }); From 299e616c5904c417a266bf677a4a3abed511bf7e Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 2 Mar 2022 06:47:49 -0700 Subject: [PATCH 14/25] =?UTF-8?q?=F0=9F=8C=80=20refactor=20Minor=20change?= =?UTF-8?q?=20to=20the=20FileGrid=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Pages/Grids/FileGrid.tsx | 33 +++++++++++++++++-------- src/components/Pages/RxTabs/Files.tsx | 1 + 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/components/Pages/Grids/FileGrid.tsx b/src/components/Pages/Grids/FileGrid.tsx index dc8cf66..ad54a885 100644 --- a/src/components/Pages/Grids/FileGrid.tsx +++ b/src/components/Pages/Grids/FileGrid.tsx @@ -8,6 +8,7 @@ interface IProps extends TableProps { [key: string]: unknown; onDownload: (docId: number) => void; onDelete: (docId: number) => void; + onEdit: (docId: number) => void; fileList: FileRecord[]; } @@ -15,6 +16,7 @@ interface ITableProps extends TableProps { onDownload: unknown; onDelete: unknown; fileList: unknown; + onEdit: unknown; } /** @@ -22,7 +24,7 @@ interface ITableProps extends TableProps { * @param {IProps} props The props for this component */ const FileGrid = (props: IProps): JSX.Element | null => { - const {fileList, onDownload, onDelete} = props; + const {fileList, onDownload, onDelete, onEdit} = props; // No render if there isn't anything to render if (!fileList || fileList.length === 0) return null; @@ -40,6 +42,16 @@ const FileGrid = (props: IProps): JSX.Element | null => { {file.Description} {file.MediaType} {file.Size} + + + - + + @@ -70,6 +81,7 @@ const FileGrid = (props: IProps): JSX.Element | null => { const tableProps = {...props} as ITableProps; delete tableProps.onDelete; delete tableProps.onDownload; + delete tableProps.onEdit; delete tableProps.fileList; return ( @@ -82,6 +94,7 @@ const FileGrid = (props: IProps): JSX.Element | null => { Size + {fileList.map((p) => FileRow(p))} diff --git a/src/components/Pages/RxTabs/Files.tsx b/src/components/Pages/RxTabs/Files.tsx index 44da370..fabd07f 100644 --- a/src/components/Pages/RxTabs/Files.tsx +++ b/src/components/Pages/RxTabs/Files.tsx @@ -82,6 +82,7 @@ const Files = (props: IProps) => { fileList={fileList} onDelete={(d) => alert('delete: ' + d)} onDownload={(d) => alert('download: ' + d)} + onEdit={(f) => alert('Edit: ' + f)} /> } From eeb85c2f8da0aa70586a4695553afa1e93aff7e8 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 3 Mar 2022 05:25:53 -0700 Subject: [PATCH 15/25] =?UTF-8?q?=E2=9B=B2=20feat=20Added=20FileEdit=20mod?= =?UTF-8?q?al=20and=20save=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Pages/Grids/FileGrid.tsx | 9 +- src/components/Pages/Modals/FileEdit.tsx | 112 +++++++++++++++++++++++ src/components/Pages/RxPage.tsx | 2 +- src/components/Pages/RxTabs/Files.tsx | 59 +++++++++--- src/providers/FileProvider.ts | 24 ++++- 5 files changed, 182 insertions(+), 24 deletions(-) create mode 100644 src/components/Pages/Modals/FileEdit.tsx diff --git a/src/components/Pages/Grids/FileGrid.tsx b/src/components/Pages/Grids/FileGrid.tsx index ad54a885..00d1976 100644 --- a/src/components/Pages/Grids/FileGrid.tsx +++ b/src/components/Pages/Grids/FileGrid.tsx @@ -8,7 +8,7 @@ interface IProps extends TableProps { [key: string]: unknown; onDownload: (docId: number) => void; onDelete: (docId: number) => void; - onEdit: (docId: number) => void; + onEdit: (fileRecord: FileRecord) => void; fileList: FileRecord[]; } @@ -43,12 +43,7 @@ const FileGrid = (props: IProps): JSX.Element | null => { {file.MediaType} {file.Size} - diff --git a/src/components/Pages/Modals/FileEdit.tsx b/src/components/Pages/Modals/FileEdit.tsx new file mode 100644 index 0000000..b802e4b --- /dev/null +++ b/src/components/Pages/Modals/FileEdit.tsx @@ -0,0 +1,112 @@ +import Button from 'react-bootstrap/Button'; +import Col from 'react-bootstrap/Col'; +import Form from 'react-bootstrap/Form'; +import Modal from 'react-bootstrap/Modal'; +import Row from 'react-bootstrap/Row'; +import React, {useEffect, useRef, useState} from 'reactn'; +import {FileRecord} from 'types/RecordTypes'; + +interface IProps { + fileInfo: FileRecord; + show: boolean; + onHide: () => void; + onClose: (r: FileRecord | null) => void; +} + +const FileEdit = (props: IProps) => { + const textInput = useRef(null); + const {onHide, onClose} = props; + + const [fileInfo, setFileInfo] = useState(props.fileInfo); + useEffect(() => { + const fileRecord = {...props.fileInfo}; + if (!fileRecord.Description) { + fileRecord.Description = ''; + } + setFileInfo(fileRecord); + }, [props.fileInfo]); + + const [show, setShow] = useState(props.show); + useEffect(() => { + setShow(props.show); + }, [props.show]); + + const handleOnChange = (changeEvent: React.ChangeEvent) => { + const target = changeEvent.target as HTMLInputElement; + const value = target.type === 'checkbox' ? target.checked : target.value; + const name = target.name; + fileInfo[name] = value; + setFileInfo({...fileInfo}); + }; + + /** + * Fires when the user clicks on Save or Cancel + * @param {boolean} shouldSave True if the user clicked save, otherwise false + */ + const handleHide = (shouldSave: boolean) => { + if (shouldSave) onClose({...fileInfo}); + else onClose(null); + setShow(false); + }; + + if (!fileInfo) return null; + + return ( + textInput?.current?.focus()} + onHide={() => onHide()} + show={show} + size="lg" + > + Edit File Info + + +
+ + + File Name + + + handleOnChange(changeEvent)} + value={fileInfo.FileName} + /> + + + + + + Description + + + handleOnChange(changeEvent)} + value={fileInfo.Description as string} + /> + + +
+
+ + + + + +
+ ); +}; + +export default FileEdit; diff --git a/src/components/Pages/RxPage.tsx b/src/components/Pages/RxPage.tsx index 39f3b24..0408a7f 100644 --- a/src/components/Pages/RxPage.tsx +++ b/src/components/Pages/RxPage.tsx @@ -302,7 +302,7 @@ const RxPage = (props: IProps): JSX.Element | null => { } > - +
diff --git a/src/components/Pages/RxTabs/Files.tsx b/src/components/Pages/RxTabs/Files.tsx index fabd07f..d77e9bb 100644 --- a/src/components/Pages/RxTabs/Files.tsx +++ b/src/components/Pages/RxTabs/Files.tsx @@ -1,31 +1,46 @@ import FileGrid from 'components/Pages/Grids/FileGrid'; +import DisabledSpinner from 'components/Pages/ListGroups/DisabledSpinner'; +import FileEdit from 'components/Pages/Modals/FileEdit'; import {RX_TAB_KEY} from 'components/Pages/RxPage'; import {ChangeEvent} from 'react'; import Form from 'react-bootstrap/Form'; import React, {useGlobal, useState} from 'reactn'; import {TClient} from 'reactn/default'; +import {FileRecord} from 'types/RecordTypes'; interface IProps { - activeClient: TClient; rxTabKey: string; } const Files = (props: IProps) => { const [, setErrorDetails] = useGlobal('__errorDetails'); + const [activeClient, setActiveClient] = useGlobal('activeClient'); + const [invalidMaxSize, setInvalidMaxSize] = useState(false); const [isBusy, setIsIsBusy] = useState(false); + const [providers] = useGlobal('providers'); + const [showEditFile, setShowEditFile] = useState(null); const defaultFileLabelText = 'Select a File to Upload'; const [uploadedFileName, setUploadedFileName] = useState(defaultFileLabelText); - const [invalidMaxSize, setInvalidMaxSize] = useState(false); - const [providers] = useGlobal('providers'); - const documentProvider = providers.fileProvider; - const fileList = props.activeClient.fileList; - - const clientInfo = props.activeClient.clientInfo; + const fileProvider = providers.fileProvider; const activeRxTab = props.rxTabKey; + const saveFile = async (fileRecord: FileRecord) => { + return await fileProvider.update(fileRecord); + }; + + /** + * Rehydrates the fileList for the active client + * @returns {Promise} + */ + const refreshFileList = async () => { + const loadedFileList = await fileProvider.load(activeClient?.clientInfo.Id as number); + await setActiveClient({...(activeClient as TClient), fileList: loadedFileList}); + }; + /** * Handle when the user clicked the Select a File to Upload component * @param {React.ChangeEvent} fileInputEvent The file InputElement + * @link https://www.slimframework.com/docs/v4/cookbook/uploading-files.html * @returns {Promise} */ const handleFileUpload = async (fileInputEvent: ChangeEvent) => { @@ -39,13 +54,11 @@ const Files = (props: IProps) => { if (file.size <= 104_857_600) { setIsIsBusy(true); try { - // See: https://www.slimframework.com/docs/v4/cookbook/uploading-files.html const formData = new FormData(); formData.append('single_file', file); setUploadedFileName(file.name); - const documentRecord = await documentProvider.uploadFile(formData, clientInfo.Id as number); - - alert('documentRecord: ' + JSON.stringify(documentRecord)); + await fileProvider.uploadFile(formData, activeClient?.clientInfo.Id as number); + await refreshFileList(); } catch (error) { await setErrorDetails(error); } @@ -64,6 +77,7 @@ const Files = (props: IProps) => { return (
+ {isBusy && } {
File exceeds maximum size allowed
- { + {activeClient && ( alert('delete: ' + d)} onDownload={(d) => alert('download: ' + d)} - onEdit={(f) => alert('Edit: ' + f)} + onEdit={(f) => setShowEditFile(f)} /> - } + )} + + { + setShowEditFile(null); + if (f) { + const updatedFile = await saveFile(f); + if (updatedFile) { + await refreshFileList(); + } + } + }} + onHide={() => setShowEditFile(null)} + /> ); }; diff --git a/src/providers/FileProvider.ts b/src/providers/FileProvider.ts index d2d778e..ce1582f 100644 --- a/src/providers/FileProvider.ts +++ b/src/providers/FileProvider.ts @@ -3,6 +3,7 @@ import {FileRecord} from 'types/RecordTypes'; export interface IFileProvider { setApiKey: (apiKey: string) => void; + update: (fileRecord: FileRecord) => Promise; uploadFile: (formData: FormData, clientId: number) => Promise; load: (clientId: number) => Promise; } @@ -14,6 +15,12 @@ type FileUploadRecord = { Type: string | null; }; +type UpdateResponse = { + status: number; + success: boolean; + data: null | FileRecord; +}; + type UploadResponse = { status: number; success: boolean; @@ -41,7 +48,22 @@ const FileProvider = (baseUrl: string): IFileProvider => { }, /** - * Upload a file as a FormData object. Note that Frak isn't used because the data is not JSON + * Insert or update a File record + * @param {FileRecord} fileRecord The file record object + * @returns {Promise} An updated file record object as a promise + */ + update: async (fileRecord: FileRecord): Promise => { + const uri = `${_baseUrl}file?api_key=${_apiKey}`; + const response = await _frak.post(uri, fileRecord); + if (response.success) { + return response.data as FileRecord; + } else { + throw response; + } + }, + + /** + * Upload a file as a FormData object. Note that Frak isn't used because the data sent is not JSON * @param {FormData} formData The FormData object containing the name and file * @param {number} clientId The Client PK * @returns {Promise} A FileUploadRecord object as a promise From 962eb6308921af166168c0d7c754589bf2fa1bad Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 3 Mar 2022 06:17:02 -0700 Subject: [PATCH 16/25] =?UTF-8?q?=E2=9B=B2=20feat=20Added=20some=20validat?= =?UTF-8?q?ion=20to=20FileEdit=20preventing=20saving=20if=20the=20FileName?= =?UTF-8?q?=20field=20is=20empty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Pages/Modals/FileEdit.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/Pages/Modals/FileEdit.tsx b/src/components/Pages/Modals/FileEdit.tsx index b802e4b..0983d63 100644 --- a/src/components/Pages/Modals/FileEdit.tsx +++ b/src/components/Pages/Modals/FileEdit.tsx @@ -26,11 +26,24 @@ const FileEdit = (props: IProps) => { setFileInfo(fileRecord); }, [props.fileInfo]); + const [canSave, setCanSave] = useState(false); + useEffect(() => { + if (!fileInfo || fileInfo.FileName?.length === 0) { + setCanSave(false); + } else { + setCanSave(true); + } + }, [fileInfo]); + const [show, setShow] = useState(props.show); useEffect(() => { setShow(props.show); }, [props.show]); + /** + * Handle user keyboard changes + * @param {React.ChangeEvent} changeEvent The change event object + */ const handleOnChange = (changeEvent: React.ChangeEvent) => { const target = changeEvent.target as HTMLInputElement; const value = target.type === 'checkbox' ? target.checked : target.value; @@ -60,7 +73,9 @@ const FileEdit = (props: IProps) => { show={show} size="lg" > - Edit File Info + +

Edit File/Document Info

+
@@ -101,7 +116,7 @@ const FileEdit = (props: IProps) => { - From be41efdc18d5dcc1f9db76b6e983478192fc9f4e Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 4 Mar 2022 03:22:54 -0700 Subject: [PATCH 17/25] =?UTF-8?q?=E2=9B=B2=20feat=20Fleshed=20out=20the=20?= =?UTF-8?q?FileEdit=20modal=20UI=20with=20validations=20and=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added read-only fields: Size, Modified Date, and Media Type - A warning alert will show if the user changes the file extension --- src/components/Pages/Modals/FileEdit.tsx | 60 ++++++++++++++++++++---- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/src/components/Pages/Modals/FileEdit.tsx b/src/components/Pages/Modals/FileEdit.tsx index 0983d63..6a7640e 100644 --- a/src/components/Pages/Modals/FileEdit.tsx +++ b/src/components/Pages/Modals/FileEdit.tsx @@ -1,10 +1,13 @@ +import Alert from 'react-bootstrap/Alert'; import Button from 'react-bootstrap/Button'; +import ButtonGroup from 'react-bootstrap/ButtonGroup'; import Col from 'react-bootstrap/Col'; import Form from 'react-bootstrap/Form'; import Modal from 'react-bootstrap/Modal'; import Row from 'react-bootstrap/Row'; import React, {useEffect, useRef, useState} from 'reactn'; import {FileRecord} from 'types/RecordTypes'; +import {getFormattedDate} from 'utility/common'; interface IProps { fileInfo: FileRecord; @@ -16,6 +19,11 @@ interface IProps { const FileEdit = (props: IProps) => { const textInput = useRef(null); const {onHide, onClose} = props; + const [showFileExtensionWarning, setShowFileExtensionWarning] = useState(false); + + const getFileExtension = (fileName: string) => { + return fileName ? fileName.split('.').pop() : ''; + }; const [fileInfo, setFileInfo] = useState(props.fileInfo); useEffect(() => { @@ -31,9 +39,12 @@ const FileEdit = (props: IProps) => { if (!fileInfo || fileInfo.FileName?.length === 0) { setCanSave(false); } else { + setShowFileExtensionWarning( + getFileExtension(fileInfo.FileName) !== getFileExtension(props?.fileInfo?.FileName || '') + ); setCanSave(true); } - }, [fileInfo]); + }, [fileInfo, props?.fileInfo?.FileName]); const [show, setShow] = useState(props.show); useEffect(() => { @@ -78,7 +89,7 @@ const FileEdit = (props: IProps) => { - + File Name @@ -86,16 +97,18 @@ const FileEdit = (props: IProps) => { 0 ? '' : 'is-invalid'} type="input" name="FileName" ref={textInput} onChange={(changeEvent) => handleOnChange(changeEvent)} value={fileInfo.FileName} /> + File Name can not be blank - + Description @@ -109,16 +122,45 @@ const FileEdit = (props: IProps) => { /> + + + + Size + + + + + + Modified + + + + + + Type + + + + + - - + + WARNING: The file extension changed. This may cause problems. + + + + + ); From cec82d2b0691a42183f2c2365bc255a7c7e5c950 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 4 Mar 2022 03:32:35 -0700 Subject: [PATCH 18/25] =?UTF-8?q?=F0=9F=91=97=20style=20Changed=20the=20Fi?= =?UTF-8?q?leEdit=20to=20accommodate=20the=20`Updated`=20field=20length=20?= =?UTF-8?q?display?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed the `` --- src/components/Pages/Modals/FileEdit.tsx | 28 ++++++++++-------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/components/Pages/Modals/FileEdit.tsx b/src/components/Pages/Modals/FileEdit.tsx index 6a7640e..7955acf 100644 --- a/src/components/Pages/Modals/FileEdit.tsx +++ b/src/components/Pages/Modals/FileEdit.tsx @@ -1,6 +1,5 @@ import Alert from 'react-bootstrap/Alert'; import Button from 'react-bootstrap/Button'; -import ButtonGroup from 'react-bootstrap/ButtonGroup'; import Col from 'react-bootstrap/Col'; import Form from 'react-bootstrap/Form'; import Modal from 'react-bootstrap/Modal'; @@ -127,23 +126,20 @@ const FileEdit = (props: IProps) => { Size - - + + Modified - - + + Type - + @@ -153,14 +149,12 @@ const FileEdit = (props: IProps) => { WARNING: The file extension changed. This may cause problems. - - - - + + ); From dae2fc0ea01133eafc28d49833d0b8c3db6de55c Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 4 Mar 2022 04:40:51 -0700 Subject: [PATCH 19/25] =?UTF-8?q?=F0=9F=91=97=20style=20Changed=20the=20fo?= =?UTF-8?q?nt=20sizes=20of=20all=20the=20read-only=20textboxes=20in=20File?= =?UTF-8?q?Edit=20moda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Pages/Modals/FileEdit.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/Pages/Modals/FileEdit.tsx b/src/components/Pages/Modals/FileEdit.tsx index 7955acf..24d88c5 100644 --- a/src/components/Pages/Modals/FileEdit.tsx +++ b/src/components/Pages/Modals/FileEdit.tsx @@ -127,19 +127,23 @@ const FileEdit = (props: IProps) => { Size - + Modified - - + + Type - - + + From 919e81dabe84c52bdb623e60cb1e9306d50c4b3d Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 4 Mar 2022 04:59:20 -0700 Subject: [PATCH 20/25] =?UTF-8?q?=F0=9F=91=97=20style=20Use=20``=20instead=20of=20``?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Client, Medicine, and Pillbox Edit modals all changed. --- src/components/Pages/Modals/ClientEdit.tsx | 25 +++++++++++--------- src/components/Pages/Modals/MedicineEdit.tsx | 2 +- src/components/Pages/Modals/PillboxEdit.tsx | 4 +++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/components/Pages/Modals/ClientEdit.tsx b/src/components/Pages/Modals/ClientEdit.tsx index 661ac5c..2103fd9 100644 --- a/src/components/Pages/Modals/ClientEdit.tsx +++ b/src/components/Pages/Modals/ClientEdit.tsx @@ -102,7 +102,7 @@ const ClientEdit = (props: IProps): JSX.Element | null => { -
+ First Name @@ -119,7 +119,7 @@ const ClientEdit = (props: IProps): JSX.Element | null => { type="text" value={clientInfo.FirstName} /> -
First name can not be blank.
+ First name can not be blank.
@@ -138,7 +138,7 @@ const ClientEdit = (props: IProps): JSX.Element | null => { type="text" value={clientInfo.LastName} /> -
Last name can not be blank.
+ Last name can not be blank. @@ -161,10 +161,11 @@ const ClientEdit = (props: IProps): JSX.Element | null => { DOB Month -
Invalid Date of Birth
+ Invalid Date of Birth
checkForDuplicates()} @@ -173,7 +174,7 @@ const ClientEdit = (props: IProps): JSX.Element | null => { type="text" value={clientInfo.DOB_MONTH} /> -
Enter the month (1-12).
+ Enter the month (1-12). @@ -181,6 +182,7 @@ const ClientEdit = (props: IProps): JSX.Element | null => { { type="text" value={clientInfo.DOB_DAY} /> -
Enter a valid day.
+ Enter a valid day. Year checkForDuplicates()} @@ -208,7 +211,7 @@ const ClientEdit = (props: IProps): JSX.Element | null => { type="text" value={clientInfo.DOB_YEAR} /> -
Enter a valid birth year.
+ Enter a valid birth year.
@@ -225,9 +228,9 @@ const ClientEdit = (props: IProps): JSX.Element | null => { rows={4} value={clientInfo.Notes} /> -
+ Notes can only be 500 characters long. length={clientInfo?.Notes?.trim().length} -
+
@@ -249,9 +252,9 @@ const ClientEdit = (props: IProps): JSX.Element | null => { > Save changes -
+ This client already exists -
+ ); diff --git a/src/components/Pages/Modals/MedicineEdit.tsx b/src/components/Pages/Modals/MedicineEdit.tsx index dcb351e..c8697da 100644 --- a/src/components/Pages/Modals/MedicineEdit.tsx +++ b/src/components/Pages/Modals/MedicineEdit.tsx @@ -248,7 +248,7 @@ const MedicineEdit = (props: IProps) => { /> )} -
Drug Name cannot be blank.
+ Drug Name cannot be blank. diff --git a/src/components/Pages/Modals/PillboxEdit.tsx b/src/components/Pages/Modals/PillboxEdit.tsx index c8cef9c..29cbfe6 100644 --- a/src/components/Pages/Modals/PillboxEdit.tsx +++ b/src/components/Pages/Modals/PillboxEdit.tsx @@ -96,7 +96,9 @@ const PillboxEdit = (props: IProps): JSX.Element | null => { type="text" value={pillboxInfo.Name} /> -
Pillbox Name field cannot be blank.
+ + Pillbox Name field cannot be blank. + From de14593668d97cfae76a066ab04bea0338cf92fb Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 5 Mar 2022 03:24:49 -0700 Subject: [PATCH 21/25] =?UTF-8?q?=E2=9B=B2=20feat=20Added=20download=20fun?= =?UTF-8?q?ctionality=20in=20the=20Rx/Documents=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Pages/Grids/FileGrid.tsx | 4 ++-- src/components/Pages/RxTabs/Files.tsx | 2 +- src/providers/FileProvider.ts | 26 +++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/components/Pages/Grids/FileGrid.tsx b/src/components/Pages/Grids/FileGrid.tsx index 00d1976..e33f3bb 100644 --- a/src/components/Pages/Grids/FileGrid.tsx +++ b/src/components/Pages/Grids/FileGrid.tsx @@ -6,7 +6,7 @@ import {randomString} from 'utility/common'; interface IProps extends TableProps { [key: string]: unknown; - onDownload: (docId: number) => void; + onDownload: (fileRecord: FileRecord) => void; onDelete: (docId: number) => void; onEdit: (fileRecord: FileRecord) => void; fileList: FileRecord[]; @@ -50,7 +50,7 @@ const FileGrid = (props: IProps): JSX.Element | null => {