diff --git a/pharmd-app/package.json b/pharmd-app/package.json index db5e4dd7..75d1f1bb 100644 --- a/pharmd-app/package.json +++ b/pharmd-app/package.json @@ -55,7 +55,7 @@ "@storybook/addon-links": "^5.3.13", "@storybook/addons": "^5.3.13", "@storybook/preset-create-react-app": "^1.5.2", - "@storybook/react": "^5.3.13", + "@storybook/react": "^5.3.21", "@svgr/webpack": "^5.1.0", "babel-loader": "^8.0.6", "babel-plugin-macros": "^2.8.0", diff --git a/pharmd-app/src/App.js b/pharmd-app/src/App.js index 20da452b..853a9b0d 100644 --- a/pharmd-app/src/App.js +++ b/pharmd-app/src/App.js @@ -11,6 +11,7 @@ import { AuthProvider, DataProvider } from "./services"; import createLigthTheme from "./themes/light-theme"; import DashboardLayout from "./components/Layout/DashboardLayout"; import customRoutes from "./config/customRoutes"; +import customReducers from './redux/reducers'; const App = () => { return ( @@ -23,6 +24,7 @@ const App = () => { authProvider={AuthProvider} dashboard={Dashboard} theme={theme} + customReducers={customReducers} customRoutes={customRoutes} > diff --git a/pharmd-app/src/components/Basic/BasicTag.js b/pharmd-app/src/components/Basic/BasicTag.js new file mode 100644 index 00000000..1f376b19 --- /dev/null +++ b/pharmd-app/src/components/Basic/BasicTag.js @@ -0,0 +1,27 @@ +import React from "react"; +import tw, { styled } from "twin.macro"; +import Chip from "@material-ui/core/Chip"; + +// ${props => props.theme.typography.size4} +const Pill = styled(Chip)` + + ${tw`rounded-lg capitalize w-16 fontStyle-4 text-xs font-bold tracking-wider`} + margin: 3px; + color: ${props => props.color}; + background-color: #b5c7f8; +`; + +const Field = styled.a` + ${tw`p-0`} +`; + +const BasicTag = ({ text, color }) => { + const tagColor = color || '#f2994a' + return ( + + + + ); +}; + +export default BasicTag; \ No newline at end of file diff --git a/pharmd-app/src/components/Basic/NoteIcon.js b/pharmd-app/src/components/Basic/NoteIcon.js index 321d12cf..602d7cdb 100644 --- a/pharmd-app/src/components/Basic/NoteIcon.js +++ b/pharmd-app/src/components/Basic/NoteIcon.js @@ -16,6 +16,12 @@ const SVG = styled(SvgIcon)` color: black; `} + ${props => + props.color === "red" && + css` + color: red; + `} + ${props => props.primary && css` diff --git a/pharmd-app/src/components/Fields/NoteBoxField.js b/pharmd-app/src/components/Fields/NoteBoxField.js new file mode 100644 index 00000000..c8457dae --- /dev/null +++ b/pharmd-app/src/components/Fields/NoteBoxField.js @@ -0,0 +1,78 @@ +import React from 'react' +import tw, { styled } from "twin.macro"; +import MuiPaper from "@material-ui/core/Paper"; +import {NOTE} from "../../constants/apiObjects"; +import CardHeader from "@material-ui/core/CardHeader"; +import CardContent from "@material-ui/core/CardContent"; +import BasicTag from "../Basic/BasicTag"; +import {DeleteButton} from 'react-admin'; + +const CardRoot = styled.div` + flex-grow: 1; + margin: 15px; +` + +const Paper = styled(MuiPaper)` + ${tw`rounded-xl h-64 w-56 shadow-cardLight`} + padding: 7px; + margin: auto; + +`; + +const Tags = styled.div` + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-around; +` + +const MAX_TITLE_LENGTH = 25; +const MAX_BODY_LENGTH = 30; + +const NoteBoxField = ({ record, studentId, onDelete }) => { + const id = record[NOTE.ID]; + const title = record[NOTE.TITLE]; + const body = record[NOTE.BODY]; + const tags = record[NOTE.TAGS]; + const date = new Date(record[NOTE.DATE]).toLocaleDateString(); + + function truncate(str, n){ + return (str.length > n) ? str.substr(0, n-1) + '…' : str; + } + + return ( + + + + + + + } + title={truncate(title, MAX_TITLE_LENGTH)} + subheader={date} + /> + +

+ {truncate(body, MAX_BODY_LENGTH)} +

+ + {tags.map((tag, ind) => { + if (ind < 2) { + return ; + } else { + const more = `${tags.length - 2} more` + return + } + + })} + + +
+
+
+ ) +} + +export default NoteBoxField; \ No newline at end of file diff --git a/pharmd-app/src/components/Fields/NoteListField.js b/pharmd-app/src/components/Fields/NoteListField.js index f2faaffb..7ee79f0c 100644 --- a/pharmd-app/src/components/Fields/NoteListField.js +++ b/pharmd-app/src/components/Fields/NoteListField.js @@ -1,7 +1,12 @@ import React from "react"; -import { Error, Loading, useQuery } from "react-admin"; +import { useDispatch } from "react-redux"; +import { setNotesModal } from "../../redux/actions"; +import { useQuery, Loading, Error } from "react-admin"; import tw, { styled } from "twin.macro"; import NoteField from "./NoteField"; +import IconButton from "@material-ui/core/IconButton"; +import AddCircleOutlineOutlinedIcon from '@material-ui/icons/AddCircleOutlineOutlined'; +import NoteIcon from "../Basic/NoteIcon"; const Label = styled.h1` ${tw`fontStyle-6 font-medium inline-flex`} @@ -32,6 +37,12 @@ const NoteListField = ({ record = {}, source }) => { } }); + const dispatch = useDispatch(); + + function addNote() { + dispatch(setNotesModal({ isOpen: true })) + } + if (loading) return ; if (error) return ; if (!data) return null; @@ -39,9 +50,13 @@ const NoteListField = ({ record = {}, source }) => { return ( - {data.map(note => { - return ; - })} + + + + {data.map((note, ind) => { + return + }) + } ); }; diff --git a/pharmd-app/src/components/Layout/NoteCreate.js b/pharmd-app/src/components/Layout/NoteCreate.js new file mode 100644 index 00000000..2888c4c3 --- /dev/null +++ b/pharmd-app/src/components/Layout/NoteCreate.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { Create, TextInput, SimpleForm, required } from 'react-admin'; + +const NoteCreate = props => { + // Input --> Record + const parseInput = input => input && input.split(" ") + + // Record --> Input + const formatInput = record => record && record.join(" ") + + return ( + + + + + + + + ) +} + +export default NoteCreate; diff --git a/pharmd-app/src/components/Layout/PharmDModal.js b/pharmd-app/src/components/Layout/PharmDModal.js new file mode 100644 index 00000000..63b4934d --- /dev/null +++ b/pharmd-app/src/components/Layout/PharmDModal.js @@ -0,0 +1,54 @@ +import React from 'react'; +import Modal from '@material-ui/core/Modal'; +import { styled } from "twin.macro"; +import PropTypes from "prop-types"; + +const ModalFrame = styled(Modal)` + margin-top: 10%; + margin-bottom: 10%; + margin-left: 15%; + margin-right: 15%; + +` + +const InnerModal = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + color: ${props => props.theme.palette.primary.main}; + background-color: white; + padding: 20px; +` + +const PharmDModal = props => { + + return ( + + + {typeof props.title === "string" ? +

{props.title}

+ : props.title + } + {props.children} +
+
+ ) +} + +PharmDModal.propTypes = { + title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + open: PropTypes.bool, + onClose: PropTypes.func, +} + +PharmDModal.defaultProps = { + title: "", + open: false, + onClose: null +} + +export default PharmDModal; \ No newline at end of file diff --git a/pharmd-app/src/redux/actions/index.js b/pharmd-app/src/redux/actions/index.js new file mode 100644 index 00000000..8849cd64 --- /dev/null +++ b/pharmd-app/src/redux/actions/index.js @@ -0,0 +1,5 @@ +import { UI_NOTES_MODAL } from "../constants/action-types"; + +export function setNotesModal(payload) { + return { type: UI_NOTES_MODAL, payload} +} diff --git a/pharmd-app/src/redux/constants/action-types.js b/pharmd-app/src/redux/constants/action-types.js new file mode 100644 index 00000000..2ce8b8a5 --- /dev/null +++ b/pharmd-app/src/redux/constants/action-types.js @@ -0,0 +1 @@ +export const UI_NOTES_MODAL = "UI_MODAL"; \ No newline at end of file diff --git a/pharmd-app/src/redux/reducers/index.js b/pharmd-app/src/redux/reducers/index.js new file mode 100644 index 00000000..d895a12d --- /dev/null +++ b/pharmd-app/src/redux/reducers/index.js @@ -0,0 +1,7 @@ +import studentSideBarReducer from "./studentSideBarReducer"; +import notesModalReducer from "./notesModalReducer"; + +export default { + studentSidebarOpen: studentSideBarReducer, + notesModalOpen: notesModalReducer +} \ No newline at end of file diff --git a/pharmd-app/src/redux/reducers/notesModalReducer.js b/pharmd-app/src/redux/reducers/notesModalReducer.js new file mode 100644 index 00000000..9b23024d --- /dev/null +++ b/pharmd-app/src/redux/reducers/notesModalReducer.js @@ -0,0 +1,8 @@ +import { UI_NOTES_MODAL} from "../constants/action-types"; + +export default (previousState = [false, () => {}], {type, payload}) => { + if (type === UI_NOTES_MODAL) { + return [payload.isOpen, payload.onClose] + } + return previousState; +} diff --git a/pharmd-app/src/screens/student/StudentNoteDrawer.js b/pharmd-app/src/screens/student/StudentNoteDrawer.js new file mode 100644 index 00000000..ceaeed0f --- /dev/null +++ b/pharmd-app/src/screens/student/StudentNoteDrawer.js @@ -0,0 +1,207 @@ +import React, {cloneElement, useState} from 'react' +import tw, { styled } from "twin.macro"; +import { useDispatch } from "react-redux"; +import MuiGrid from "@material-ui/core/Grid"; +import HelpIcon from "@material-ui/icons/HelpOutline"; +import LeftIcon from '@material-ui/icons/ChevronLeftOutlined'; +import RightIcon from '@material-ui/icons/ChevronRightOutlined'; +import AddIcon from '@material-ui/icons/AddCircleOutlineOutlined'; +import { + List, + useListContext, + TopToolbar, + CreateButton, + ExportButton, + ListBase, + ListToolbar, + BulkActionsToolbar, + Button, + sanitizeListRestProps, + Pagination, + useQuery, + Loading, + Error +} from "react-admin"; +import NoteBoxField from "../../components/Fields/NoteBoxField"; +import MuiPaper from "@material-ui/core/Paper"; +import IconButton from '@material-ui/core/IconButton'; +import NoteIcon from "../../components/Basic/NoteIcon"; +import {setNotesModal} from "../../redux/actions"; + +const NotesActions = (props) => { + const { + className, + exporter, + filters, + maxResults, + ...rest + } = props; + const { + currentSort, + resource, + displayedFilters, + filterValues, + hasCreate, + basePath, + selectedIds, + showFilter, + total, + } = useListContext(); + return ( + + + + {/*Custom Action*/} + + + ) +} + +const notesExporter = (records, fetchRelatedRecords) => { + +} + +const NoteGrid = styled(MuiGrid)` + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-around; + +` + +const Paper = styled(MuiPaper)` + ${tw`rounded-xl shadow-cardLight`} + padding: 15px; + margin: 20px; + ` + +const NoteDrawerContainer = styled(Paper)` + +` + +const NotesTitle = styled.h2` + color: ${props => props.theme.palette.secondary.main}; +` + +const NotesHeader = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; +` + +const NoNotes = styled.h3` + text-align: center +` + +const StudentNoteDrawer = ({record = {}, source }) => { + const [page, setPage] = useState(0); + const dispatch = useDispatch(); + + const { data, loading, error } = useQuery({ + type: 'getManyReference', + resource: 'notes', + payload: { + target: 'student', + id: record[source], + pagination: { + page: 1, + perPage: 3 + }, + sort: { + field: '', + order: '' + } + } + }) + + function getDataPage(data) { + const cursor = page*3; + return data.slice(cursor, cursor + 3); + } + const noNotes = () => data.length === 0; + const totalPages = () => Math.ceil(data.length / 3) + const onLastPage = () => page === totalPages() - 1; + const onFirstPage = () => page === 0; + + function pagesDisplay() { + return data.length > 0 ? `${page + 1} / ${totalPages()}` : '0 / 0' + } + + function flipPage(isIncrement) { + isIncrement ? setPage(page + 1) : setPage(page - 1); + } + + function updateNotes() { + // TODO how is this done??? + } + + function addNote() { + dispatch(setNotesModal({ isOpen: true, onClose: updateNotes })) + } + + if (loading) return + if (error) return + if (!data) return null + + return ( + + + Notes +
+ + + + flipPage(false)} disabled={onFirstPage() || noNotes()}> + + + flipPage(true)} disabled={onLastPage() || noNotes()}> + + + {pagesDisplay()} +
+
+ {data.length > 0 ? + + {getDataPage(data).map((note, ind) => { + return + })} + + : No notes to show! + } +
+ ) +} + +const StudentNoteDrawer2 = ({ source, ...props }) => { + return ( + + {/**/} + } + bulkActionButtons={false} + > +

Notes

+ + + {props.bulkActionButtons} + + +
+ {/*
*/} +
+ ) +} + +export default StudentNoteDrawer; \ No newline at end of file diff --git a/pharmd-app/src/screens/studentDetails/StudentDetailsScreen.js b/pharmd-app/src/screens/studentDetails/StudentDetailsScreen.js index e96d1091..41d6d356 100644 --- a/pharmd-app/src/screens/studentDetails/StudentDetailsScreen.js +++ b/pharmd-app/src/screens/studentDetails/StudentDetailsScreen.js @@ -1,13 +1,18 @@ -import React from "react"; +import React, {Fragment} from "react"; -import tw, { styled } from "twin.macro"; +import tw, {styled} from "twin.macro"; import Paper from "@material-ui/core/Paper"; -import { Loading, useGetOne } from "react-admin"; import AppBar from "../../components/Nav/AppBar"; import StudentDetailsSide from "./StudentDetailsSide"; +import {Loading, useGetOne} from "react-admin"; import StudentDetailsContentGrid from "./StudentDetailsContentGrid"; +import StudentNoteDrawer from "../student/StudentNoteDrawer"; +import PharmDModal from "../../components/Layout/PharmDModal"; +import {setNotesModal} from "../../redux/actions"; +import NoteCreate from "../../components/Layout/NoteCreate"; +import { useSelector, useDispatch } from "react-redux"; const MainContent = styled.div` ${tw`p-12 pt-2 `} @@ -25,25 +30,35 @@ const SideContent = styled(Paper)` `; const StudentDetailsScreen = props => { - const { data, loading, error } = useGetOne("students", props.match.params.id); - if (loading) { - return ; - } - if (error) { - return

Error, id:{id} is not found

; - } - return ( - <> - - - - - - - - - - ); + const {data, loading, error} = useGetOne('students', props.match.params.id); + const dispatch = useDispatch(); + const [isNotesModalOpen, onNotesClose] = useSelector(state => state.notesModalOpen) + if (loading) { + return ; + } + if (error) { + return

Error, id: {id} is not found

; + } + return ( + + + + + + + + + + + dispatch(setNotesModal({isOpen: false}))}> + { + onNotesClose(); + return dispatch(setNotesModal({isOpen: false})); + }}/> + + + ); }; export default StudentDetailsScreen; diff --git a/pharmd-app/src/stories/notefield.stories.js b/pharmd-app/src/stories/notefield.stories.js new file mode 100644 index 00000000..88306083 --- /dev/null +++ b/pharmd-app/src/stories/notefield.stories.js @@ -0,0 +1,28 @@ +import NoteField from "../components/Fields/NoteField"; +import { action } from '@storybook/addon-actions'; +import React from "react"; + +export default { + title: "NoteField", + component: NoteField +} + +const note1 = { + text: 'FooFooFoo', + title: "FooTitle", + created: new Date(2018, 8, 10), + lastEdit: new Date(2018, 9, 10), + id: 1 +} + +const note2 = { + text: 'BarBarBar', + title: "BarTitle", + created: new Date(2018, 8, 10), + lastEdit: new Date(2020, 8, 15), + id: 2 +} + +export const Default = () => ; + +export const Note2 = () => ; \ No newline at end of file diff --git a/pharmd-app/src/stories/studentNoteBox.stories.js b/pharmd-app/src/stories/studentNoteBox.stories.js new file mode 100644 index 00000000..402692ac --- /dev/null +++ b/pharmd-app/src/stories/studentNoteBox.stories.js @@ -0,0 +1,26 @@ +import React from "react"; +import StudentNoteBox from "../screens/student/StudentNoteBox"; + +export default { + title: "Student Note Box", + component: StudentNoteBox, +}; + +const note1 = { + id: 1, + date: new Date(), + tags: [], + title: "Fun Note!", + body: "Four score and seven years ago..." +} + +const note2 = { + id: 2, + date: new Date(2000, 10, 3), + tags: ["Urgent", "Extreme", "Expired"], + title: "Fun Note!", + body: "We the people, in order to form a more perfect Union..." +} + +export const Note1 = () => +export const Note2 = () =>