From 77b420ea6f686f9873f1917bad65c238198c505d Mon Sep 17 00:00:00 2001 From: "DESKTOP-C9V2M01\\Zeeri" Date: Wed, 26 Oct 2022 08:45:21 -0700 Subject: [PATCH] Added an initial cut at a student segement of the site, with a list of workshops and the ability to sign up for them. --- client/main.html | 4 +- imports/api/assets.js | 10 +- imports/api/workshops.js | 97 +++++++++++++++ imports/ui/App.jsx | 44 +++---- imports/ui/pages/Assignments/ByAsset.jsx | 3 +- imports/ui/pages/Assignments/ByPerson.jsx | 8 +- imports/ui/pages/Home.jsx | 80 +++++++++++++ imports/ui/pages/Search.jsx | 2 +- imports/ui/pages/Student/StudentPage.jsx | 80 +++++++++++++ imports/ui/pages/Student/Workshops.jsx | 138 ++++++++++++++++++++++ package.json | 2 + public/images/student.svg | 102 ++++++++++++++++ 12 files changed, 538 insertions(+), 32 deletions(-) create mode 100644 imports/api/workshops.js create mode 100644 imports/ui/pages/Home.jsx create mode 100644 imports/ui/pages/Student/StudentPage.jsx create mode 100644 imports/ui/pages/Student/Workshops.jsx create mode 100644 public/images/student.svg diff --git a/client/main.html b/client/main.html index 193f9e8..96756e6 100644 --- a/client/main.html +++ b/client/main.html @@ -5,6 +5,6 @@ - -
+ +
diff --git a/imports/api/assets.js b/imports/api/assets.js index 73871a0..5e1a4fa 100644 --- a/imports/api/assets.js +++ b/imports/api/assets.js @@ -9,7 +9,7 @@ import {AssetAssignmentHistory} from "/imports/api/asset-assignment-history"; // console.log("Setting Up Assets...") export const Assets = new Mongo.Collection('assets'); -export const conditions = ['New','Like New','Good','Okay','Damaged'] +export const conditions = ['New','Like New','Good','Okay','Damaged', 'Missing', 'Decommissioned'] /* const AssetsSchema = new SimpleSchema({ @@ -90,7 +90,7 @@ Meteor.methods({ check(condition, String); if(conditionDetails) check(conditionDetails, String); - if(condition !== 'New' && condition !== 'Like New' && condition !== 'Good' && condition !== 'Okay' && condition !== 'Damaged') { + if(!conditions.includes(condition)) { //Should never happen. console.error("Invalid condition option in assets.add(..)"); throw new Meteor.Error("Invalid condition option."); @@ -123,7 +123,7 @@ Meteor.methods({ check(condition, String); if(conditionDetails) check(conditionDetails, String); - if(condition !== 'New' && condition !== 'Like New' && condition !== 'Good' && condition !== 'Okay' && condition !== 'Damaged') { + if(!conditions.includes(condition)) { //Should never happen. console.error("Invalid condition option in assets.update(..)"); throw new Meteor.Error("Invalid condition option."); @@ -176,7 +176,7 @@ Meteor.methods({ if(!date) date = new Date(); - if(condition !== 'New' && condition !== 'Like New' && condition !== 'Good' && condition !== 'Okay' && condition !== 'Damaged') { + if(!conditions.includes(condition)) { //Should never happen. console.error("Invalid condition option in assets.unassign(..)"); throw new Meteor.Error("Invalid condition option."); @@ -225,7 +225,7 @@ Meteor.methods({ if(!date) date = new Date(); - if(condition !== 'New' && condition !== 'Like New' && condition !== 'Good' && condition !== 'Okay' && condition !== 'Damaged') { + if(!conditions.includes(condition)) { //Should never happen. console.error("Invalid condition option in assets.unassign(..)"); throw new Meteor.Error("Invalid condition option."); diff --git a/imports/api/workshops.js b/imports/api/workshops.js new file mode 100644 index 0000000..74c57c0 --- /dev/null +++ b/imports/api/workshops.js @@ -0,0 +1,97 @@ +import {Mongo} from "meteor/mongo"; +import {Meteor} from "meteor/meteor"; +import { check, Match } from 'meteor/check'; +import { Roles } from 'meteor/alanning:roles'; + +// +// An asset type is a specific type of equipment. Example: Lenovo 100e Chromebook. +// +export const Workshops = new Mongo.Collection('workshops'); + +if(Meteor.isServer) { + // Drop any old indexes we no longer will use. Create indexes we need. + //try {Workshops._dropIndex("External ID")} catch(e) {} + //Workshops.createIndex({name: "text"}, {name: "name", unique: false}); + //Workshops.createIndex({id: 1}, {name: "External ID", unique: true}); + + //Debug: Show all indexes. + // Workshops.rawCollection().indexes((err, indexes) => { + // console.log(indexes); + // }); + + // This code only runs on the server + Meteor.publish('workshops', function() { + return Workshops.find({}); + }); +} +Meteor.methods({ + 'workshops.add'(name, description, signupLimit) { + let signupSheet = []; + + check(name, String); + check(description, String); + // Match a positive integer or undefined/null. + check(signupLimit, Match.Where((x) => { + check(x, Match.Maybe(Match.Integer)); + return x ? x > 0 : true + })) + + if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) { + Workshops.insert({name, description, signupLimit, signupSheet}); + } + }, + 'workshops.update'(_id, name, description, signupLimit) { + check(_id, String); + check(name, String); + check(description, String); + // Match a positive integer or undefined/null. + check(signupLimit, Match.Where((x) => { + check(x, Match.Maybe(Match.Integer)); + return x ? x > 0 : true + })) + + if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) { + Workshops.update({_id}, {$set: {name, description, signupLimit}}); + } + }, + 'workshops.signup'(_id) { + check(_id, String); + + if(Meteor.userId()) { + let workshop = Workshops.findOne(_id); + + if(workshop) { + if(!workshop.signupLimit || workshop.signedUp.length < workshop.signupLimit) { + Workshops.update({_id}, {$push: {signupSheet: {_id: Meteor.userId()}}}); + } + } + } + }, + 'workshops.unsignup'(_id) { + check(_id, String); + + if(Meteor.userId()) { + let workshop = Workshops.findOne(_id); + + if(workshop) { + Workshops.update({_id}, {$pull: {signupSheet: {_id: Meteor.userId()}}}); + } + } + }, + 'workshops.complete'(_id) { + check(_id, String); + + if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) { + Workshops.update({_id}, {$set: {isComplete: true}}) + } + }, + 'workshops.remove'(_id) { + check(_id, String); + + if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) { + Workshops.remove({_id}) + } + }, +}); + +// console.log("Asset types setup.") diff --git a/imports/ui/App.jsx b/imports/ui/App.jsx index cb7bd32..9acd569 100644 --- a/imports/ui/App.jsx +++ b/imports/ui/App.jsx @@ -12,6 +12,9 @@ import History from './pages/History' import Search from './pages/Search' import Users from './pages/Users' import Admin from './pages/Admin' +import Home from './pages/Home' +import {StudentPage} from './pages/Student/StudentPage' +import {Workshops} from './pages/Student/Workshops' const appTheme = createTheme({ components: { @@ -65,7 +68,7 @@ const appTheme = createTheme({ } }) -export const App = () => { +export const App = (props) => { const {user, canManageLaptops, isAdmin} = useTracker(() => { const user = Meteor.user(); const canManageLaptops = user && Roles.userIsInRole(user._id, 'laptop-management', 'global'); @@ -84,43 +87,44 @@ export const App = () => { -
-
- TODO: Some statistics and such. -
-
- } - /> + + + }/> + + {user && } + + }/> {canManageLaptops && } - } - /> + + }/> {isAdmin && } - } - /> + + }/> {isAdmin && } - } - /> + + }/> {canManageLaptops && } - } - /> + + }/> {canManageLaptops && } - } - /> + + }/> {isAdmin && } - } - /> + + }/>
diff --git a/imports/ui/pages/Assignments/ByAsset.jsx b/imports/ui/pages/Assignments/ByAsset.jsx index 3e19695..5264b99 100644 --- a/imports/ui/pages/Assignments/ByAsset.jsx +++ b/imports/ui/pages/Assignments/ByAsset.jsx @@ -24,6 +24,7 @@ import {Assets, conditions} from "/imports/api/assets"; import {AssetTypes} from "/imports/api/asset-types"; import {Students} from "/imports/api/students"; import {Staff} from "/imports/api/staff"; +import {Link} from "react-router-dom"; const cssTwoColumnContainer = { display: 'grid', @@ -121,7 +122,7 @@ const AssignmentsByAsset = () => { {foundAsset.assignee && ( <>
Assigned on: {foundAsset.assignmentDate.toString()}
-
Assigned to: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName} {foundAsset.assignee.grade && foundAsset.assignee.grade} ({foundAsset.assignee.email})
+
Assigned to: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName} {foundAsset.assignee.grade && foundAsset.assignee.grade} ({foundAsset.assignee.email})
)} diff --git a/imports/ui/pages/Assignments/ByPerson.jsx b/imports/ui/pages/Assignments/ByPerson.jsx index 6cefba6..7309a2f 100644 --- a/imports/ui/pages/Assignments/ByPerson.jsx +++ b/imports/ui/pages/Assignments/ByPerson.jsx @@ -19,6 +19,7 @@ import {Assets, conditions} from "/imports/api/assets"; import {AssetTypes} from "/imports/api/asset-types"; import {Students} from "/imports/api/students"; import {Staff} from "/imports/api/staff"; +import {Link} from "react-router-dom"; const cssTwoColumnContainer = { display: 'grid', @@ -235,7 +236,8 @@ const AssignmentsByPerson = () => {
setAssetIdInput(input)} style={cssEditorField} variant="standard" label="Asset ID" value={assetId} onChange={(e) => {setAssetId(e.target.value.toUpperCase())}}/>
{foundAsset && foundAsset.assetType.name}
-
{foundAsset && foundAsset.serial}
+
{foundAsset && {foundAsset.assetId}}
+
{foundAsset && {foundAsset.serial}}
{foundAsset && foundAsset.assignee && (
Assigned To: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName}
)} @@ -246,8 +248,8 @@ const AssignmentsByPerson = () => { return (
{next.assetType.name}
-
{next.assetId}
-
{next.serial}
+
{next.assetId}
+
{next.serial}
) diff --git a/imports/ui/pages/Home.jsx b/imports/ui/pages/Home.jsx new file mode 100644 index 0000000..c47c963 --- /dev/null +++ b/imports/ui/pages/Home.jsx @@ -0,0 +1,80 @@ +import { Meteor } from 'meteor/meteor'; +import React, { useState, useEffect } from 'react'; +import { useTracker } from 'meteor/react-meteor-data'; +import { useTheme } from '@mui/material/styles'; +import _ from 'lodash'; +import TextField from "@mui/material/TextField"; +import Button from "@mui/material/Button"; +import MenuItem from '@mui/material/MenuItem'; +import {InputLabel, List, ListItem, ListItemButton, ListItemText} from "@mui/material"; +import Box from "@mui/material/Box"; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import {Assets, conditions} from "/imports/api/assets"; +import {AssetTypes} from "/imports/api/asset-types"; +import {Students} from "/imports/api/students"; +import {Staff} from "/imports/api/staff"; +import {Link} from "react-router-dom"; + +const cssTwoColumnContainer = { + display: 'grid', + gridTemplateColumns: "1fr 1fr", + columnGap: '1rem', + rowGap: '0.4rem', +} +const cssEditorField = { + minWidth: '10rem' +} + +const Statistics = () => { + const [selectedMissingAsset, setSelectedMissingAsset] = useState("") + + const {missingAssets} = useTracker(() => { + let missingAssets = []; + + missingAssets = Assets.find({condition: 'Missing'}).fetch(); + + for(let next of missingAssets) { + next.assetType = AssetTypes.findOne({_id: next.assetTypeId}) + } + + return {missingAssets} + }); + + const getListItemStyle = (item) => { + return { + backgroundColor: selectedMissingAsset === item ? '#EECFA6' : 'white' + } + } + + return ( + <> +

Missing Equipment

+ + {missingAssets.map((next, i) => { + return ( + {setSelectedMissingAsset(next)}}> + + + ) + })} + + + ) +} + +export default () => { + // Meteor.subscribe('students'); + // Meteor.subscribe('staff'); + Meteor.subscribe('assetTypes'); + Meteor.subscribe('assets'); + + return ( + + ) +} \ No newline at end of file diff --git a/imports/ui/pages/Search.jsx b/imports/ui/pages/Search.jsx index 6b1ff14..6a2fd70 100644 --- a/imports/ui/pages/Search.jsx +++ b/imports/ui/pages/Search.jsx @@ -26,7 +26,7 @@ const RenderUsage = ({data}) => { {next.asset && ( <>Asset ID: {next.asset.assetId}
)} - {new Date(next.startTime).toLocaleDateString("en-US") + "-" + new Date(next.endTime).toLocaleDateString("en-US")}
+ {new Date(next.startTime).toLocaleDateString("en-US") + "-" + new Date(next.endTime).toLocaleDateString("en-US") + " @ " + new Date(next.endTime).toLocaleTimeString("en-US")}
{next.assignedTo && ( <>Currently assigned to: {next.assignedTo.firstName} {next.assignedTo.lastName} {next.assignedTo.grade ? "~ " + next.assignedTo.grade : ""} ({next.assignedTo.email}) )} diff --git a/imports/ui/pages/Student/StudentPage.jsx b/imports/ui/pages/Student/StudentPage.jsx new file mode 100644 index 0000000..98de2bf --- /dev/null +++ b/imports/ui/pages/Student/StudentPage.jsx @@ -0,0 +1,80 @@ +import { Meteor } from 'meteor/meteor'; +import {Roles} from 'meteor/alanning:roles'; +import React, { useState } from 'react'; +import { useTracker } from 'meteor/react-meteor-data'; +import {Link} from 'react-router-dom'; +import _ from 'lodash'; +import Button from "@mui/material/Button"; +import {Box, Grid} from "@mui/material"; + +export const StudentPage = (props) => { + const {user, canManageLaptops, isAdmin} = useTracker(() => { + const user = Meteor.user(); + const canManageLaptops = user && Roles.userIsInRole(user._id, 'laptop-management', 'global'); + const isAdmin = user && Roles.userIsInRole(user._id, 'admin', 'global'); + + return { + user, + canManageLaptops, + isAdmin + } + }) + + function performLogin() { + //Login style can be "popup" or "redirect". I am not sure we need to request and offline token. + Meteor.loginWithGoogle({loginStyle: "popup", requestOfflineToken: true}, (err) => { + if (err) { + console.log(err); + } else { + //console.log("Logged in"); + } + }) + } + + function performLogout() { + Meteor.logout(); + } + + return ( + <> + {!user && ( + <> +
+ +
+ */} +
+ + )} + {user && ( + <> +
+
+
+
+ Logo +
+ +
+
+
Tempest
+
+
+
+
+
+
+ +
+
+
+
+ {props.children} +
+ + )} + + ) +} \ No newline at end of file diff --git a/imports/ui/pages/Student/Workshops.jsx b/imports/ui/pages/Student/Workshops.jsx new file mode 100644 index 0000000..3ac8c8b --- /dev/null +++ b/imports/ui/pages/Student/Workshops.jsx @@ -0,0 +1,138 @@ +import React, { useState } from 'react'; +import {Meteor} from "meteor/meteor"; +import {Roles} from 'meteor/alanning:roles'; +import { useTracker } from 'meteor/react-meteor-data'; +import {Link} from 'react-router-dom'; +import _ from 'lodash'; +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; +import MenuItem from '@mui/material/MenuItem'; +import {InputLabel, List, ListItem, ListItemButton, ListItemText, Paper} from "@mui/material"; +import Box from "@mui/material/Box"; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import {Editor} from 'react-draft-wysiwyg' +import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css' +import {Students} from "/imports/api/students"; +import {Staff} from "/imports/api/staff"; +import {conditions} from "/imports/api/assets"; + +const cssTwoColumnContainer = { + display: 'grid', + gridTemplateColumns: "1fr 1fr", + columnGap: '1rem', + rowGap: '0.4rem', +} + +export const Workshops = () => { + Meteor.subscribe('students'); + Meteor.subscribe('staff'); + Meteor.subscribe('workshops'); + + const user = Meteor.user(); + const isAdmin = user && Roles.userIsInRole(user._id, 'admin', 'global'); + const [selectedWorkshop, setSelectedWorkshop] = useState("") + + const {workshops} = useTracker(() => { + let workshops = []; + + workshops = Workshops.find({isComplete: false}).fetch(); + + for(let workshop of workshops) { + for(let user of workshop.signupSheet) { + user.data = Students.findOne({_id: user._id}) + if(!user.data) user.data = Staff.findOne({_id: user._id}) + } + } + + return {workshops} + }); + + const getListItemStyle = (item) => { + return { + backgroundColor: selectedWorkshop === item ? '#EECFA6' : 'white' + } + } + + const newWorkshop = () => { + if(isAdmin) { + setEditedWorkshop({}) + setOpenWorkshopEditor(true) + } + } + const editWorkshop = () => { + if(isAdmin && selectedWorkshop) { + setEditedWorkshop({...selectedWorkshop}) + setOpenWorkshopEditor(true) + } + } + + const [openWorkshopEditor, setOpenWorkshopEditor] = useState(false) + const [editedWorkshop, setEditedWorkshop] = useState(false) + const workshopEditorClosed = (save) => { + const completeHandler = (err, result) => { + if(err) console.error(err) + else { + setOpenWorkshopEditor(false) + setEditedWorkshop(null) + } + } + + if(save) { + if(editedWorkshop._id) Meteor.call('workshops.update', editedWorkshop._id, editedWorkshop.name, editedWorkshop.description, editedWorkshop.signupLimit, completeHandler) + else Meteor.call('workshops.add', editedWorkshop.name, editedWorkshop.description, editedWorkshop.signupLimit, completeHandler) + } + else completeHandler() + } + + return ( + <> + + Workshop Editor + + {editedWorkshop.name = e.target.value; setEditedWorkshop(editedWorkshop)}}/> + {selectedWorkshop.description = e.target.value; setEditedWorkshop(editedWorkshop)}}/> + {editedWorkshop.signupLimit = e.target.value; setEditedWorkshop(editedWorkshop)}}/> + + + + + + + + + {isAdmin && } + + {workshops.map((next, i) => { + return ( + {setSelectedWorkshop(next)}}> + + + ) + })} + + + + + + {/* {selectedWorkshop.description = ""}}/>*/} + {`${selectedWorkshop.description}`} + + + {selectedWorkshop.signupSheet.map((next, i) => { + return ( + + + + ) + })} + + + + ) +} \ No newline at end of file diff --git a/package.json b/package.json index 3305aac..9b56887 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "classnames": "^2.2.6", "csv-parse": "^5.3.0", "dayjs": "^1.11.3", + "draft-js": "^0.11.7", "html5-qrcode": "^2.2.0", "jquery": "^3.6.0", "lodash": "^4.17.15", @@ -24,6 +25,7 @@ "mongodb": "^4.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draft-wysiwyg": "^1.15.0", "react-router-dom": "^6.3.0", "umbrellajs": "^3.3.1", "underscore": "^1.13.2", diff --git a/public/images/student.svg b/public/images/student.svg new file mode 100644 index 0000000..7679d2c --- /dev/null +++ b/public/images/student.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +