diff --git a/package-lock.json b/package-lock.json index 92a7096..672131e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4744,9 +4744,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash-es": { "version": "4.17.15", @@ -5022,6 +5022,19 @@ "minimist": "^1.2.5" } }, + "moment": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", + "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" + }, + "moment-timezone": { + "version": "0.5.31", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz", + "integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==", + "requires": { + "moment": ">= 2.9.0" + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -6079,6 +6092,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-moment": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-0.9.7.tgz", + "integrity": "sha512-ifzUrUGF6KRsUN2pRG5k56kO0mJBr8kRkWb0wNvtFIsBIxOuPxhUpL1YlXwpbQCbHq23hUu6A0VEk64HsFxk9g==" + }, "react-router": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", diff --git a/package.json b/package.json index 627d30f..5958b74 100755 --- a/package.json +++ b/package.json @@ -13,10 +13,13 @@ "dependencies": { "formik": "^2.1.4", "history": "^4.10.1", + "moment": "^2.27.0", + "moment-timezone": "^0.5.31", "prop-types": "^15.7.2", "query-string": "^6.11.0", "react": "^16.8.6", "react-dom": "^16.8.6", + "react-moment": "^0.9.7", "react-router-dom": "^5.0.0", "rxjs": "^6.3.3", "yup": "^0.28.1" diff --git a/src/_components/Nav.jsx b/src/_components/Nav.jsx index f709a73..6e55e6e 100644 --- a/src/_components/Nav.jsx +++ b/src/_components/Nav.jsx @@ -39,6 +39,7 @@ function AdminNav({ match }) { ); diff --git a/src/_services/index.js b/src/_services/index.js index b4fa8ab..4d51f4b 100644 --- a/src/_services/index.js +++ b/src/_services/index.js @@ -1,2 +1,3 @@ export * from './account.service'; export * from './alert.service'; +export * from './utilities.service'; diff --git a/src/_services/utilities.service.js b/src/_services/utilities.service.js new file mode 100644 index 0000000..6444d7a --- /dev/null +++ b/src/_services/utilities.service.js @@ -0,0 +1,48 @@ +import { BehaviorSubject } from 'rxjs'; + +import config from 'config'; +import { fetchWrapper } from '@/_helpers'; + +const utilitySubject = new BehaviorSubject(null); +const baseUrl = `${config.apiUrl}/utilities`; + +export const utilitiesService = { + enable, + disable, + getAll, + getById, + create, + update, + delete: _delete, + user: utilitySubject.asObservable(), + get utilityValue () { return utilitySubject.value } +}; + +function enable(id) { + return fetchWrapper.post(`${baseUrl}/enable/${id}`); +} + +function disable(id) { + return fetchWrapper.post(`${baseUrl}/disable/${id}`); +} + +function getAll() { + return fetchWrapper.get(baseUrl); +} + +function getById(id) { + return fetchWrapper.get(`${baseUrl}/${id}`); +} + +function create(params) { + return fetchWrapper.post(baseUrl, params); +} + +function update(id, params) { + return fetchWrapper.put(`${baseUrl}/${id}`, params) +} + +// prefixed with underscore because 'delete' is a reserved word in javascript +function _delete(id) { + return fetchWrapper.delete(`${baseUrl}/${id}`) +} diff --git a/src/admin/Index.jsx b/src/admin/Index.jsx index e352597..ee264b7 100644 --- a/src/admin/Index.jsx +++ b/src/admin/Index.jsx @@ -3,6 +3,7 @@ import { Route, Switch } from 'react-router-dom'; import { Overview } from './Overview'; import { Users } from './users'; +import { Utilities } from './utilities'; function Admin({ match }) { const { path } = match; @@ -13,6 +14,7 @@ function Admin({ match }) { + diff --git a/src/admin/Overview.jsx b/src/admin/Overview.jsx index ed0bdcd..2b4212a 100644 --- a/src/admin/Overview.jsx +++ b/src/admin/Overview.jsx @@ -8,7 +8,10 @@ function Overview({ match }) {

Admin

This section can only be accessed by administrators.

-

Manage Users

+

+ Manage Users + Manage Utilities +

); } diff --git a/src/admin/utilities/AddEdit.jsx b/src/admin/utilities/AddEdit.jsx new file mode 100644 index 0000000..bae1dfd --- /dev/null +++ b/src/admin/utilities/AddEdit.jsx @@ -0,0 +1,102 @@ +import React, { useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { Formik, Field, Form, ErrorMessage } from 'formik'; +import * as Yup from 'yup'; + +import { utilitiesService, alertService } from '@/_services'; + +function AddEdit({ history, match }) { + const { id } = match.params; + const isAddMode = !id; + + const initialValues = { + name: '', + status: 'true' + }; + + const validationSchema = Yup.object().shape({ + name: Yup.string() + .required('Utility name is required'), + status: Yup.boolean() + .required('Utility status is required') + }); + + function onSubmit(fields, { setStatus, setSubmitting }) { + setStatus(); + if (isAddMode) { + createUtility(fields, setSubmitting); + } else { + updateUtility(id, fields, setSubmitting); + } + } + + function createUtility(fields, setSubmitting) { + utilitiesService.create(fields) + .then(() => { + alertService.success('Utility added successfully', { keepAfterRouteChange: true }); + history.push('.'); + }) + .catch(error => { + setSubmitting(false); + alertService.error(error); + }); + } + + function updateUtility(id, fields, setSubmitting) { + utilitiesService.update(id, fields) + .then(() => { + alertService.success('Update successful', { keepAfterRouteChange: true }); + history.push('..'); + }) + .catch(error => { + setSubmitting(false); + alertService.error(error); + }); + } + + return ( + + {({ errors, touched, isSubmitting, setFieldValue }) => { + useEffect(() => { + if (!isAddMode) { + // get utility and set form fields + utilitiesService.getById(id).then(utility => { + const fields = ['name', 'status']; + fields.forEach(field => setFieldValue(field, utility[field], false)); + }); + } + }, []); + + return ( +
+

{isAddMode ? 'Add Utility' : 'Edit Utility'}

+
+
+ + + +
+
+ + + + + + +
+
+
+ + Cancel +
+
+ ); + }} +
+ ); +} + +export { AddEdit }; \ No newline at end of file diff --git a/src/admin/utilities/Index.jsx b/src/admin/utilities/Index.jsx new file mode 100644 index 0000000..66417ca --- /dev/null +++ b/src/admin/utilities/Index.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { List } from './List'; +import { AddEdit } from './AddEdit'; + +function Utilities({ match }) { + const { path } = match; + + return ( + + + + + + ); +} + +export { Utilities }; \ No newline at end of file diff --git a/src/admin/utilities/List.jsx b/src/admin/utilities/List.jsx new file mode 100644 index 0000000..1496ad1 --- /dev/null +++ b/src/admin/utilities/List.jsx @@ -0,0 +1,107 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import Moment from 'react-moment'; + +import { utilitiesService } from '@/_services'; + +function List({ match }) { + const { path } = match; + const [utilities, setUtilities] = useState(null); + + useEffect(() => { + utilitiesService.getAll().then(x => setUtilities(x)); + }, []); + + function enableUtility(id) { + utilitiesService.enable(id).then(() => { + utilitiesService.getAll().then(x => setUtilities(x)); + }); + } + + function disableUtility(id) { + utilitiesService.disable(id).then(() => { + utilitiesService.getAll().then(x => setUtilities(x)); + }); + } + + function deleteUtility(id) { + setUtilities(utilities.map(x => { + if (x.id === id) { x.isDeleting = true; } + return x; + })); + utilitiesService.delete(id).then(() => { + setUtilities(utilities => utilities.filter(x => x.id !== id)); + }); + } + + function utilityStatus(status) { + if (status) { return 🟢; } + else if (!status) { return 🔴; } + else { return ⚠️; } + } + + function toggleUtility(id, status) { + if (status) { + disableUtility(id) + } else if (!status) { + enableUtility(id) + } + } + + function defaultChecked(status) { + if (status) { return "checked"; } + else { return null; } + } + + function parseDateTime(timestamp) { + return {timestamp}; + } + + return ( +
+

Utilities

+

All utilities from secure (admin only) api end point:

+ Add Utility + + + + + + + + + + + {utilities && utilities.map(utility => + + + + + + + )} + {!utilities && + + + + } + +
StatusUtilityLast Modified
{utilityStatus(utility.status)}{utility.name}{parseDateTime(utility.modified)} +
+ toggleUtility(utility.id, utility.status)} defaultChecked={defaultChecked(utility.status)} className="custom-control-input" id={utility.id}/> +
+ +
+ +
+
+ ); +} + +export { List }; \ No newline at end of file