Merge remote-tracking branch 'origin/master'

This commit is contained in:
2023-01-12 15:24:29 -08:00
23 changed files with 1194 additions and 37 deletions

View File

@@ -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.");

View File

@@ -7,5 +7,6 @@ import "./sites.js";
import "./asset-types.js";
import "./assets.js";
import "./asset-assignment-history.js";
import "./workshops.js";
// console.log("Finished setting up server side models.");

100
imports/api/workshops.js Normal file
View File

@@ -0,0 +1,100 @@
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 = [];
console.log(name)
console.log(description)
console.log(signupLimit)
check(name, String);
check(description, Match.Maybe(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.")

View File

@@ -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 {WorkshopList} from './pages/Student/WorkshopList'
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 = () => {
<Routes>
<Route path="/" element={
<Page>
<div className="container">
<div className="row">
TODO: Some statistics and such.
</div>
</div>
</Page>}
/>
<Home/>
</Page>
}/>
<Route path="/student" element={
<StudentPage>
{user && <WorkshopList/>}
</StudentPage>
}/>
<Route path="/assignments/*" element={
<Page>
{canManageLaptops && <Assignments/>}
</Page>}
/>
</Page>
}/>
<Route path="/assets/*" element={
<Page>
{isAdmin && <Assets/>}
</Page>}
/>
</Page>
}/>
<Route path="/admin/*" element={
<Page>
{isAdmin && <Admin/>}
</Page>}
/>
</Page>
}/>
<Route path="/history/*" element={
<Page>
{canManageLaptops && <History/>}
</Page>}
/>
</Page>
}/>
<Route path="/search" element={
<Page>
{canManageLaptops && <Search/>}
</Page>}
/>
</Page>
}/>
<Route path="/users/*" element={
<Page>
{isAdmin && <Users/>}
</Page>}
/>
</Page>
}/>
</Routes>
</BrowserRouter>
</ThemeProvider>

View File

@@ -0,0 +1,122 @@
// import {RichTextPlugin} from "@lexical/react/LexicalRichTextPlugin";
// import {ContentEditable} from "@lexical/react/LexicalContentEditable";
// import {OnChangePlugin} from "@lexical/react/LexicalOnChangePlugin";
// import {HistoryPlugin} from "@lexical/react/LexicalHistoryPlugin";
// import {LinkPlugin} from "@lexical/react/LexicalLinkPlugin";
// import {MarkdownShortcutPlugin} from "@lexical/react/LexicalMarkdownShortcutPlugin";
// import {LexicalComposer} from "@lexical/react/LexicalComposer";
// import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
// import {AutoScrollPlugin} from '@lexical/react/LexicalAutoScrollPlugin';
// import {CharacterLimitPlugin} from '@lexical/react/LexicalCharacterLimitPlugin';
// import {CheckListPlugin} from '@lexical/react/LexicalCheckListPlugin';
// import {ClearEditorPlugin} from '@lexical/react/LexicalClearEditorPlugin';
// import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';
// import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin';
// import {ListPlugin} from '@lexical/react/LexicalListPlugin';
// // import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
// import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
// import {createWebsocketProvider} from './collaboration'
// import {useSettings} from './context/SettingsContext'
// import {useSharedHistoryContext} from './context/SharedHistoryContext'
// import TableCellNodes from './nodes/TableCellNodes'
// import ActionsPlugin from './plugins/ActionsPlugin'
// import AutocompletePlugin from './plugins/AutocompletePlugin'
// import AutoEmbedPlugin from './plugins/AutoEmbedPlugin'
// import AutoLinkPlugin from './plugins/AutoLinkPlugin'
// import ClickableLinkPlugin from './plugins/ClickableLinkPlugin'
// import CodeActionMenuPlugin from './plugins/CodeActionMenuPlugin';
// import CodeHighlightPlugin from './plugins/CodeHighlightPlugin'
// import CollapsiblePlugin from './plugins/CollapsiblePlugin'
// import CommentPlugin from './plugins/CommentPlugin'
// import ComponentPickerPlugin from './plugins/ComponentPickerPlugin'
// import DraggableBlockPlugin from './plugins/DraggableBlockPlugin'
// import EmojiPickerPlugin from './plugins/EmojiPickerPlugin'
// import EmojisPlugin from './plugins/EmojisPlugin'
// import EquationsPlugin from './plugins/EquationsPlugin'
// import ExcalidrawPlugin from './plugins/ExcalidrawPlugin'
// import FigmaPlugin from './plugins/FigmaPlugin'
// import FloatingLinkEditorPlugin from './plugins/FloatingLinkEditorPlugin'
// import FloatingTextFormatToolbarPlugin from './plugins/FloatingTextFormatToolbarPlugin'
// import HorizontalRulePlugin from './plugins/HorizontalRulePlugin'
// import ImagesPlugin from './plugins/ImagesPlugin'
// import KeywordsPlugin from './plugins/KeywordsPlugin'
// import ListMaxIndentLevelPlugin from './plugins/ListMaxIndentLevelPlugin'
// import MarkdownShortcutPlugin from './plugins/MarkdownShortcutPlugin'
// import {MaxLengthPlugin} from './plugins/MaxLengthPlugin'
// import MentionsPlugin from './plugins/MentionsPlugin'
// import PollPlugin from './plugins/PollPlugin'
// import SpeechToTextPlugin from './plugins/SpeechToTextPlugin'
// import TabFocusPlugin from './plugins/TabFocusPlugin'
// import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin'
// import TableCellResizer from './plugins/TableCellResizer'
// import TableOfContentsPlugin from './plugins/TableOfContentsPlugin'
// import {TablePlugin as NewTablePlugin} from './plugins/TablePlugin'
// import ToolbarPlugin from './plugins/ToolbarPlugin'
// import TreeViewPlugin from './plugins/TreeViewPlugin'
// import TwitterPlugin from './plugins/TwitterPlugin'
// import YouTubePlugin from './plugins/YouTubePlugin'
// import PlaygroundEditorTheme from './themes/PlaygroundEditorTheme'
// import ContentEditable from './ui/ContentEditable'
// import ErrorBoundary from './ui/ErrorBoundary'
// import Placeholder from './ui/Placeholder'
import * as React from 'react';
import {useRef, useState} from 'react';
import {
EditorComposer,
Editor as InternalEditor,
ToolbarPlugin,
AlignDropdown,
BackgroundColorPicker,
BoldButton,
CodeFormatButton,
FloatingLinkEditor,
FontFamilyDropdown,
FontSizeDropdown,
InsertDropdown,
InsertLinkButton,
ItalicButton,
TextColorPicker,
TextFormatDropdown,
UnderlineButton,
Divider,
} from 'verbum';
// import nodes from './Nodes'
export const Editor = (props) => {
return (
// <LexicalComposer initialConfig={{editorState: props.state, namespace: props.namespace, onError: props.onError, theme: props.theme, nodes}}>
// <ToolbarPlugin/>
// <RichTextPlugin contentEditable={<ContentEditable/>} placeholder={<div>Sample...</div>}/>
// {props.onChange && <OnChangePlugin onChange={props.onChange} ignoreSelectionChange={true}/>}
// <HistoryPlugin/>
// <LinkPlugin/>
// <MarkdownShortcutPlugin/>
// </LexicalComposer>
<EditorComposer>
<InternalEditor hashtagsEnabled={true} onChange={props.onChange} initialEditorState={props.state}>
<ToolbarPlugin defaultFontSize="20px">
<FontFamilyDropdown />
<FontSizeDropdown />
<Divider />
<BoldButton />
<ItalicButton />
<UnderlineButton />
<CodeFormatButton />
<InsertLinkButton />
<TextColorPicker />
<BackgroundColorPicker />
<TextFormatDropdown />
<Divider />
<InsertDropdown enablePoll={true} enableTable={true} />
<Divider />
<AlignDropdown />
</ToolbarPlugin>
</InternalEditor>
</EditorComposer>
)
}

View File

@@ -0,0 +1,65 @@
/*
import {CodeHighlightNode, CodeNode} from '@lexical/code';
import {HashtagNode} from '@lexical/hashtag';
import {AutoLinkNode, LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {MarkNode} from '@lexical/mark';
import {OverflowNode} from '@lexical/overflow';
import {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
// import {CollapsibleContainerNode} from '../plugins/CollapsiblePlugin/CollapsibleContainerNode';
// import {CollapsibleContentNode} from '../plugins/CollapsiblePlugin/CollapsibleContentNode';
// import {CollapsibleTitleNode} from '../plugins/CollapsiblePlugin/CollapsibleTitleNode';
// import {AutocompleteNode} from './AutocompleteNode';
// import {EmojiNode} from './EmojiNode';
// import {EquationNode} from './EquationNode';
// import {ExcalidrawNode} from './ExcalidrawNode';
// import {FigmaNode} from './FigmaNode';
// import {ImageNode} from './ImageNode';
// import {KeywordNode} from './KeywordNode';
// import {MentionNode} from './MentionNode';
// import {PollNode} from './PollNode';
// import {StickyNode} from './StickyNode';
// import {TableNode as NewTableNode} from './TableNode';
// import {TweetNode} from './TweetNode';
// import {YouTubeNode} from './YouTubeNode';
const Nodes = [
HeadingNode,
ListNode,
ListItemNode,
QuoteNode,
CodeNode,
TableNode,
TableCellNode,
TableRowNode,
HashtagNode,
CodeHighlightNode,
AutoLinkNode,
LinkNode,
OverflowNode,
HorizontalRuleNode,
MarkNode,
// NewTableNode,
// PollNode,
// StickyNode,
// ImageNode,
// MentionNode,
// EmojiNode,
// ExcalidrawNode,
// EquationNode,
// AutocompleteNode,
// KeywordNode,
// TweetNode,
// YouTubeNode,
// FigmaNode,
// CollapsibleContainerNode,
// CollapsibleContentNode,
// CollapsibleTitleNode,
];
export default Nodes;
*/

View File

@@ -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 && (
<>
<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>
</>
)}

View File

@@ -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 = () => {
<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>{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 && (
<div>Assigned To: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName}</div>
)}
@@ -246,8 +248,8 @@ const AssignmentsByPerson = () => {
return (
<div key={next._id} style={{...getAssetTileStyles(i), ...cssAssetTile}}>
<div>{next.assetType.name}</div>
<div>{next.assetId}</div>
<div>{next.serial}</div>
<div><Link to={"/search?assetId=" + encodeURIComponent(next.assetId)}>{next.assetId}</Link></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>
</div>
)

80
imports/ui/pages/Home.jsx Normal file
View 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/>
)
}

View File

@@ -26,7 +26,7 @@ const RenderUsage = ({data}) => {
{next.asset && (
<>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 && (
<>Currently assigned to: {next.assignedTo.firstName} {next.assignedTo.lastName} {next.assignedTo.grade ? "~ " + next.assignedTo.grade : ""} ({next.assignedTo.email})</>
)}

View 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>
</>
)}
</>
)
}

View File

@@ -0,0 +1,179 @@
import React, { useState, useEffect } 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 {Students} from "/imports/api/students";
import {Staff} from "/imports/api/staff";
import {conditions} from "/imports/api/assets";
import {Workshops} from "/imports/api/workshops";
import {$getRoot, $getSelection} from 'lexical';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {RichTextPlugin} from "@lexical/react/LexicalRichTextPlugin";
import {LinkPlugin} from "@lexical/react/LexicalLinkPlugin";
import {MarkdownShortcutPlugin} from "@lexical/react/LexicalMarkdownShortcutPlugin";
import {AutoLinkNode, LinkNode} from '@lexical/link';
import {Editor} from "/imports/ui/components/RichText/Editor";
const cssTwoColumnContainer = {
display: 'grid',
gridTemplateColumns: "1fr 1fr",
columnGap: '1rem',
rowGap: '0.4rem',
}
export const WorkshopList = () => {
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 [openWorkshopEditor, setOpenWorkshopEditor] = useState(false)
const [editedWorkshop, setEditedWorkshop] = useState({})
const [editedName, setEditedName] = useState("")
const [editedDescription, setEditedDescription] = useState("")
const [editedSignupLimit, setEditedSignupLimit] = useState("")
const newWorkshop = () => {
if(isAdmin) {
setEditedDescription("")
setEditedWorkshop({})
setEditedDescription("")
setEditedName("")
setEditedSignupLimit("")
setOpenWorkshopEditor(true)
}
}
const editWorkshop = () => {
if(isAdmin && selectedWorkshop) {
setEditedWorkshop({...selectedWorkshop})
setEditedDescription(selectedWorkshop.description ? selectedWorkshop.description : "")
setEditedName(selectedWorkshop.name ? selectedWorkshop.name : "")
setEditedSignupLimit(selectedWorkshop.signupLimit ? selectedWorkshop.signupLimit : "")
setOpenWorkshopEditor(true)
}
}
const workshopEditorClosed = (save) => {
const completeHandler = (err, result) => {
if(err) console.error(err)
else {
setOpenWorkshopEditor(false)
}
}
if(save) {
if(editedWorkshop._id) Meteor.call('workshops.update', editedWorkshop._id, editedName, editedDescription, editedSignupLimit, completeHandler)
else Meteor.call('workshops.add', editedName, editedDescription, editedSignupLimit, completeHandler)
}
else completeHandler()
}
const onLexicalComposerError = (err) => {
console.error(err)
}
const lexicalComposerTheme = {
}
const editorChanged = (content, editor) => {
console.log(content)
console.log(editor)
}
const descriptionEditorNodes = [LinkNode]
return (
<>
{openWorkshopEditor ? (
<>
<h1>Workshop Editor</h1>
<Box style={{display: 'flex', flexDirection: 'column'}}>
<TextField style={{marginTop: '1rem',minWidth: '30rem', maxWidth: '50rem'}} variant="standard" label="Name" value={editedName} onChange={(e) => {setEditedName(e.target.value)}}/>
<TextField style={{marginTop: '1rem',minWidth: '30rem', maxWidth: '50rem'}} variant="standard" type="number" label="Signup Limit" value={editedSignupLimit} onChange={(e) => {setEditedSignupLimit(e.target.value)}}/>
<Box style={{marginTop: '2rem'}}>
<InputLabel>Description</InputLabel>
<Box style={{border: "1px solid black"}}>
<Editor namespace='WorkshopDescriptionEditor' state={selectedWorkshop ? selectedWorkshop.description : ""} theme={lexicalComposerTheme} onChange={editorChanged}/>
</Box>
</Box>
</Box>
<Button onClick={() => workshopEditorClosed(true)}>Save</Button>
<Button onClick={() => workshopEditorClosed(false)}>Cancel</Button>
</>
) : (
<>
<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 && `${selectedWorkshop.description}`}
</Paper>
<List>
{selectedWorkshop && 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>
</>
)}
</>
)
}