Added an initial cut at a student segement of the site, with a list of workshops and the ability to sign up for them.
This commit is contained in:
@@ -5,6 +5,6 @@
|
|||||||
<link rel="icon" type="image/x-icon" href="/favicon.png">
|
<link rel="icon" type="image/x-icon" href="/favicon.png">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body style="height: 100%">
|
||||||
<div id="react-target"></div>
|
<div id="react-target" style="height: 100%"></div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {AssetAssignmentHistory} from "/imports/api/asset-assignment-history";
|
|||||||
// console.log("Setting Up Assets...")
|
// console.log("Setting Up Assets...")
|
||||||
|
|
||||||
export const Assets = new Mongo.Collection('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({
|
const AssetsSchema = new SimpleSchema({
|
||||||
@@ -90,7 +90,7 @@ Meteor.methods({
|
|||||||
check(condition, String);
|
check(condition, String);
|
||||||
if(conditionDetails) check(conditionDetails, 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.
|
//Should never happen.
|
||||||
console.error("Invalid condition option in assets.add(..)");
|
console.error("Invalid condition option in assets.add(..)");
|
||||||
throw new Meteor.Error("Invalid condition option.");
|
throw new Meteor.Error("Invalid condition option.");
|
||||||
@@ -123,7 +123,7 @@ Meteor.methods({
|
|||||||
check(condition, String);
|
check(condition, String);
|
||||||
if(conditionDetails) check(conditionDetails, 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.
|
//Should never happen.
|
||||||
console.error("Invalid condition option in assets.update(..)");
|
console.error("Invalid condition option in assets.update(..)");
|
||||||
throw new Meteor.Error("Invalid condition option.");
|
throw new Meteor.Error("Invalid condition option.");
|
||||||
@@ -176,7 +176,7 @@ Meteor.methods({
|
|||||||
|
|
||||||
if(!date) date = new Date();
|
if(!date) date = new Date();
|
||||||
|
|
||||||
if(condition !== 'New' && condition !== 'Like New' && condition !== 'Good' && condition !== 'Okay' && condition !== 'Damaged') {
|
if(!conditions.includes(condition)) {
|
||||||
//Should never happen.
|
//Should never happen.
|
||||||
console.error("Invalid condition option in assets.unassign(..)");
|
console.error("Invalid condition option in assets.unassign(..)");
|
||||||
throw new Meteor.Error("Invalid condition option.");
|
throw new Meteor.Error("Invalid condition option.");
|
||||||
@@ -225,7 +225,7 @@ Meteor.methods({
|
|||||||
|
|
||||||
if(!date) date = new Date();
|
if(!date) date = new Date();
|
||||||
|
|
||||||
if(condition !== 'New' && condition !== 'Like New' && condition !== 'Good' && condition !== 'Okay' && condition !== 'Damaged') {
|
if(!conditions.includes(condition)) {
|
||||||
//Should never happen.
|
//Should never happen.
|
||||||
console.error("Invalid condition option in assets.unassign(..)");
|
console.error("Invalid condition option in assets.unassign(..)");
|
||||||
throw new Meteor.Error("Invalid condition option.");
|
throw new Meteor.Error("Invalid condition option.");
|
||||||
|
|||||||
97
imports/api/workshops.js
Normal file
97
imports/api/workshops.js
Normal file
@@ -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.")
|
||||||
@@ -12,6 +12,9 @@ import History from './pages/History'
|
|||||||
import Search from './pages/Search'
|
import Search from './pages/Search'
|
||||||
import Users from './pages/Users'
|
import Users from './pages/Users'
|
||||||
import Admin from './pages/Admin'
|
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({
|
const appTheme = createTheme({
|
||||||
components: {
|
components: {
|
||||||
@@ -65,7 +68,7 @@ const appTheme = createTheme({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const App = () => {
|
export const App = (props) => {
|
||||||
const {user, canManageLaptops, isAdmin} = useTracker(() => {
|
const {user, canManageLaptops, isAdmin} = useTracker(() => {
|
||||||
const user = Meteor.user();
|
const user = Meteor.user();
|
||||||
const canManageLaptops = user && Roles.userIsInRole(user._id, 'laptop-management', 'global');
|
const canManageLaptops = user && Roles.userIsInRole(user._id, 'laptop-management', 'global');
|
||||||
@@ -84,43 +87,44 @@ export const App = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={
|
<Route path="/" element={
|
||||||
<Page>
|
<Page>
|
||||||
<div className="container">
|
<Home/>
|
||||||
<div className="row">
|
</Page>
|
||||||
TODO: Some statistics and such.
|
}/>
|
||||||
</div>
|
<Route path="/student" element={
|
||||||
</div>
|
<StudentPage>
|
||||||
</Page>}
|
{user && <Workshops/>}
|
||||||
/>
|
</StudentPage>
|
||||||
|
}/>
|
||||||
<Route path="/assignments/*" element={
|
<Route path="/assignments/*" element={
|
||||||
<Page>
|
<Page>
|
||||||
{canManageLaptops && <Assignments/>}
|
{canManageLaptops && <Assignments/>}
|
||||||
</Page>}
|
</Page>
|
||||||
/>
|
}/>
|
||||||
<Route path="/assets/*" element={
|
<Route path="/assets/*" element={
|
||||||
<Page>
|
<Page>
|
||||||
{isAdmin && <Assets/>}
|
{isAdmin && <Assets/>}
|
||||||
</Page>}
|
</Page>
|
||||||
/>
|
}/>
|
||||||
<Route path="/admin/*" element={
|
<Route path="/admin/*" element={
|
||||||
<Page>
|
<Page>
|
||||||
{isAdmin && <Admin/>}
|
{isAdmin && <Admin/>}
|
||||||
</Page>}
|
</Page>
|
||||||
/>
|
}/>
|
||||||
<Route path="/history/*" element={
|
<Route path="/history/*" element={
|
||||||
<Page>
|
<Page>
|
||||||
{canManageLaptops && <History/>}
|
{canManageLaptops && <History/>}
|
||||||
</Page>}
|
</Page>
|
||||||
/>
|
}/>
|
||||||
<Route path="/search" element={
|
<Route path="/search" element={
|
||||||
<Page>
|
<Page>
|
||||||
{canManageLaptops && <Search/>}
|
{canManageLaptops && <Search/>}
|
||||||
</Page>}
|
</Page>
|
||||||
/>
|
}/>
|
||||||
<Route path="/users/*" element={
|
<Route path="/users/*" element={
|
||||||
<Page>
|
<Page>
|
||||||
{isAdmin && <Users/>}
|
{isAdmin && <Users/>}
|
||||||
</Page>}
|
</Page>
|
||||||
/>
|
}/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {Assets, conditions} from "/imports/api/assets";
|
|||||||
import {AssetTypes} from "/imports/api/asset-types";
|
import {AssetTypes} from "/imports/api/asset-types";
|
||||||
import {Students} from "/imports/api/students";
|
import {Students} from "/imports/api/students";
|
||||||
import {Staff} from "/imports/api/staff";
|
import {Staff} from "/imports/api/staff";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
const cssTwoColumnContainer = {
|
const cssTwoColumnContainer = {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@@ -121,7 +122,7 @@ const AssignmentsByAsset = () => {
|
|||||||
{foundAsset.assignee && (
|
{foundAsset.assignee && (
|
||||||
<>
|
<>
|
||||||
<div>Assigned on: {foundAsset.assignmentDate.toString()}</div>
|
<div>Assigned on: {foundAsset.assignmentDate.toString()}</div>
|
||||||
<div>Assigned to: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName} {foundAsset.assignee.grade && foundAsset.assignee.grade} ({foundAsset.assignee.email})</div>
|
<div>Assigned to: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName} {foundAsset.assignee.grade && foundAsset.assignee.grade} (<Link to={"/search?email=" + encodeURIComponent(foundAsset.assignee.email)}>{foundAsset.assignee.email}</Link>)</div>
|
||||||
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign()}>Unassign</Button>
|
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign()}>Unassign</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {Assets, conditions} from "/imports/api/assets";
|
|||||||
import {AssetTypes} from "/imports/api/asset-types";
|
import {AssetTypes} from "/imports/api/asset-types";
|
||||||
import {Students} from "/imports/api/students";
|
import {Students} from "/imports/api/students";
|
||||||
import {Staff} from "/imports/api/staff";
|
import {Staff} from "/imports/api/staff";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
const cssTwoColumnContainer = {
|
const cssTwoColumnContainer = {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@@ -235,7 +236,8 @@ const AssignmentsByPerson = () => {
|
|||||||
<div style={cssAssetTile}>
|
<div style={cssAssetTile}>
|
||||||
<div style={{marginBottom: '1rem'}}><TextField id='assetIdInput' inputRef={input=>setAssetIdInput(input)} style={cssEditorField} variant="standard" label="Asset ID" value={assetId} onChange={(e) => {setAssetId(e.target.value.toUpperCase())}}/></div>
|
<div style={{marginBottom: '1rem'}}><TextField id='assetIdInput' inputRef={input=>setAssetIdInput(input)} style={cssEditorField} variant="standard" label="Asset ID" value={assetId} onChange={(e) => {setAssetId(e.target.value.toUpperCase())}}/></div>
|
||||||
<div>{foundAsset && foundAsset.assetType.name}</div>
|
<div>{foundAsset && foundAsset.assetType.name}</div>
|
||||||
<div>{foundAsset && foundAsset.serial}</div>
|
<div>{foundAsset && <Link to={"/search?assetId=" + encodeURIComponent(foundAsset.assetId)}>{foundAsset.assetId}</Link>}</div>
|
||||||
|
<div>{foundAsset && <Link to={"/search?serial=" + encodeURIComponent(foundAsset.serial)}>{foundAsset.serial}</Link>}</div>
|
||||||
{foundAsset && foundAsset.assignee && (
|
{foundAsset && foundAsset.assignee && (
|
||||||
<div>Assigned To: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName}</div>
|
<div>Assigned To: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName}</div>
|
||||||
)}
|
)}
|
||||||
@@ -246,8 +248,8 @@ const AssignmentsByPerson = () => {
|
|||||||
return (
|
return (
|
||||||
<div key={next._id} style={{...getAssetTileStyles(i), ...cssAssetTile}}>
|
<div key={next._id} style={{...getAssetTileStyles(i), ...cssAssetTile}}>
|
||||||
<div>{next.assetType.name}</div>
|
<div>{next.assetType.name}</div>
|
||||||
<div>{next.assetId}</div>
|
<div><Link to={"/search?assetId=" + encodeURIComponent(next.assetId)}>{next.assetId}</Link></div>
|
||||||
<div>{next.serial}</div>
|
<div><Link to={"/search?serial=" + encodeURIComponent(next.serial)}>{next.serial}</Link></div>
|
||||||
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign(next)}>Unassign</Button>
|
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign(next)}>Unassign</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
80
imports/ui/pages/Home.jsx
Normal file
80
imports/ui/pages/Home.jsx
Normal file
@@ -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 (
|
||||||
|
<>
|
||||||
|
<h1>Missing Equipment</h1>
|
||||||
|
<List>
|
||||||
|
{missingAssets.map((next, i) => {
|
||||||
|
return (
|
||||||
|
<ListItemButton key={next._id} style={getListItemStyle(next)} selected={selectedMissingAsset === next} onClick={(e) => {setSelectedMissingAsset(next)}}>
|
||||||
|
<ListItemText primary={next.assetId} secondary={next.assetType.name}/>
|
||||||
|
</ListItemButton>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
// Meteor.subscribe('students');
|
||||||
|
// Meteor.subscribe('staff');
|
||||||
|
Meteor.subscribe('assetTypes');
|
||||||
|
Meteor.subscribe('assets');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Statistics/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ const RenderUsage = ({data}) => {
|
|||||||
{next.asset && (
|
{next.asset && (
|
||||||
<>Asset ID: <Link to={"/search?assetId=" + encodeURIComponent(next.asset.assetId)}>{next.asset.assetId}</Link><br/></>
|
<>Asset ID: <Link to={"/search?assetId=" + encodeURIComponent(next.asset.assetId)}>{next.asset.assetId}</Link><br/></>
|
||||||
)}
|
)}
|
||||||
{new Date(next.startTime).toLocaleDateString("en-US") + "-" + new Date(next.endTime).toLocaleDateString("en-US")}<br/>
|
{new Date(next.startTime).toLocaleDateString("en-US") + "-" + new Date(next.endTime).toLocaleDateString("en-US") + " @ " + new Date(next.endTime).toLocaleTimeString("en-US")}<br/>
|
||||||
{next.assignedTo && (
|
{next.assignedTo && (
|
||||||
<>Currently assigned to: {next.assignedTo.firstName} {next.assignedTo.lastName} {next.assignedTo.grade ? "~ " + next.assignedTo.grade : ""} ({next.assignedTo.email})</>
|
<>Currently assigned to: {next.assignedTo.firstName} {next.assignedTo.lastName} {next.assignedTo.grade ? "~ " + next.assignedTo.grade : ""} ({next.assignedTo.email})</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
80
imports/ui/pages/Student/StudentPage.jsx
Normal file
80
imports/ui/pages/Student/StudentPage.jsx
Normal file
@@ -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 && (
|
||||||
|
<>
|
||||||
|
<div style={{width: '100%', height: '100%', background: 'url(/images/student.svg)', backgroundSize: 'cover', backgroundPosition: 'center bottom', position: 'fixed', right: 0, bottom: 0, top: 0, left: 0}}> </div>
|
||||||
|
|
||||||
|
<div style={{display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', height: '100%', width: '100%'}}>
|
||||||
|
<Button css={{height: "100%", width: '100px', margin: 'auto'}} variant="contained" className="button" onClick={performLogin}>Login</Button>*/}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{user && (
|
||||||
|
<>
|
||||||
|
<div className='pageHeaderContainer'>
|
||||||
|
<div className="container">
|
||||||
|
<header className="row pageHeader">
|
||||||
|
<div className="col-12 logoContainer">
|
||||||
|
<img className="logo" src="/images/logo.svg" alt="Logo"/>
|
||||||
|
<div className="login">
|
||||||
|
<button type="button" role="button" onClick={performLogout}>Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 center title">Tempest</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='pageNavContainer'>
|
||||||
|
<div className='container'>
|
||||||
|
<header className='row pageNavHeader'>
|
||||||
|
<nav className="col-12 center">
|
||||||
|
<Link to='/'>Home</Link>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='pageContentContainer'>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
138
imports/ui/pages/Student/Workshops.jsx
Normal file
138
imports/ui/pages/Student/Workshops.jsx
Normal file
@@ -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 (
|
||||||
|
<>
|
||||||
|
<Dialog open={openWorkshopEditor} onClose={workshopEditorClosed}>
|
||||||
|
<DialogTitle>Workshop Editor</DialogTitle>
|
||||||
|
<DialogContent style={{display: 'flex', flexDirection: 'column'}}>
|
||||||
|
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} variant="standard" label="Name" value={editedWorkshop.name} onChange={(e) => {editedWorkshop.name = e.target.value; setEditedWorkshop(editedWorkshop)}}/>
|
||||||
|
<Editor editorState={selectedWorkshop.description} toolbarClassName="editorToolbar" wrapperClassName="editorWrapper" editorClassName="editor" onEditorStateChange={(e) => {selectedWorkshop.description = e.target.value; setEditedWorkshop(editedWorkshop)}}/>
|
||||||
|
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} variant="standard" type="number" label="Signup Limit" value={editedWorkshop.signupLimit} onChange={(e) => {editedWorkshop.signupLimit = e.target.value; setEditedWorkshop(editedWorkshop)}}/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => workshopEditorClosed(true)}>Save</Button>
|
||||||
|
<Button onClick={() => workshopEditorClosed(false)}>Cancel</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Box style={{marginTop: '1rem',...cssTwoColumnContainer}}>
|
||||||
|
{isAdmin && <Button onClick={() => newWorkshop()}>New...</Button>}
|
||||||
|
<List>
|
||||||
|
{workshops.map((next, i) => {
|
||||||
|
return (
|
||||||
|
<ListItemButton key={next._id} onDoubleClick={editWorkshop} style={getListItemStyle(next)} selected={selectedWorkshop === next} onClick={(e) => {setSelectedWorkshop(next)}}>
|
||||||
|
<ListItemText primary={next.name}/>
|
||||||
|
</ListItemButton>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box style={{...cssTwoColumnContainer}}>
|
||||||
|
<Paper>
|
||||||
|
{/*<Editor editorState={selectedWorkshop.description} toolbarClassName="editorToolbar" wrapperClassName="editorWrapper" editorClassName="editor" onEditorStateChange={(e) => {selectedWorkshop.description = ""}}/>*/}
|
||||||
|
{`${selectedWorkshop.description}`}
|
||||||
|
</Paper>
|
||||||
|
<List>
|
||||||
|
{selectedWorkshop.signupSheet.map((next, i) => {
|
||||||
|
return (
|
||||||
|
<ListItem key={next._id}>
|
||||||
|
<ListItemText primary={next.data.firstName + " " + next.data.lastName} secondary={next.data.email}/>
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"csv-parse": "^5.3.0",
|
"csv-parse": "^5.3.0",
|
||||||
"dayjs": "^1.11.3",
|
"dayjs": "^1.11.3",
|
||||||
|
"draft-js": "^0.11.7",
|
||||||
"html5-qrcode": "^2.2.0",
|
"html5-qrcode": "^2.2.0",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
"mongodb": "^4.4.1",
|
"mongodb": "^4.4.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-draft-wysiwyg": "^1.15.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"umbrellajs": "^3.3.1",
|
"umbrellajs": "^3.3.1",
|
||||||
"underscore": "^1.13.2",
|
"underscore": "^1.13.2",
|
||||||
|
|||||||
102
public/images/student.svg
Normal file
102
public/images/student.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 4.0 MiB |
Reference in New Issue
Block a user