Initial check in; All but the history pages working.

This commit is contained in:
2022-09-07 08:58:00 -07:00
commit d6bd620207
109 changed files with 13170 additions and 0 deletions

55
imports/ui/App.jsx Normal file
View File

@@ -0,0 +1,55 @@
import { Meteor } from 'meteor/meteor';
import {Roles} from 'meteor/alanning:roles';
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import {BrowserRouter, Routes, Route} from 'react-router-dom';
import {Page} from './Page'
import Assignments from './pages/Assignments'
import Assets from './pages/Assets'
import History from './pages/History'
import Users from './pages/Users'
import Admin from './pages/Admin'
export const App = () => {
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
}
})
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Page>
<div className="container">
<div className="row">
TODO: Some statistics and such.
</div>
</div>
</Page>}/>
<Route path="/assignments/*" element={<Page>
{canManageLaptops && <Assignments/>}
</Page>}/>
<Route path="/assets/*" element={<Page>
{isAdmin && <Assets/>}
</Page>}/>
<Route path="/admin/*" element={<Page>
{isAdmin && <Admin/>}
</Page>}/>
<Route path="/history/*" element={<Page>
{canManageLaptops && <History/>}
</Page>}/>
<Route path="/users/*" element={<Page>
{isAdmin && <Users/>}
</Page>}/>
</Routes>
</BrowserRouter>
)
}

77
imports/ui/Page.jsx Normal file
View File

@@ -0,0 +1,77 @@
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';
export const Page = (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 (
<div>
<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">
{!user ?
<button type="button" role="button" onClick={performLogin}>Login</button>
:
<button type="button" role="button" onClick={performLogout}>Logout</button>
}
</div>
</div>
<div className="col-12 center title">K12 Tempest</div>
<div className="col-12 center">
<div className="nav-separator"/>
</div>
</header>
</div>
</div>
<div className='pageNavContainer'>
<div className='container'>
<header className='row pageNavHeader'>
<nav className="col-12 center">
<Link to='/'>Home</Link>
{canManageLaptops && <Link to="/history">History</Link>}
{canManageLaptops && <Link to="/assignments">Assignments</Link>}
{isAdmin && <Link to="/assets">Assets</Link>}
{isAdmin && <Link to="/users">Users</Link>}
{isAdmin && <Link to="/admin">Admin</Link>}
</nav>
</header>
</div>
</div>
<div className='pageContentContainer'>
{props.children}
</div>
</div>
)
}

View File

@@ -0,0 +1,58 @@
import React, {lazy, useState} from 'react';
import TabNav from '../util/TabNav';
// This is needed because there is some problem with the lazy loading of the pages when they import this file.
// Importing it here pre-loads it and avoids the issue.
// It has something to do with the students.js file and its import of the csv parsing library.
import {Students} from "/imports/api/students";
export default () => {
let tabs = [
{
title: "Sites",
getElement: () => {
const Sites = lazy(()=>import('./Admin/Sites'))
return <Sites/>
},
path: '/sites',
href: 'sites'
},
{
title: "Students",
getElement: () => {
const Students = lazy(()=>import('./Admin/Students'))
return <Students/>
},
path: '/students',
href: 'students'
},
{
title: "Staff",
getElement: () => {
const Staff = lazy(()=>import('./Admin/Staff'))
return <Staff/>
},
path: '/staff',
href: 'staff'
},
{
title: "Asset Types",
getElement: () => {
const AssetTypes = lazy(()=>import('./Admin/AssetTypes'))
return <AssetTypes/>
},
path: '/assetTypes',
href: 'assetTypes'
},
{
title: "Functions",
getElement: () => {
const Functions = lazy(()=>import('./Admin/Functions'))
return <Functions/>
},
path: '/functions',
href: 'functions'
},
]
return <TabNav tabs={tabs}/>
}

View File

@@ -0,0 +1,113 @@
import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import SimpleTable from "/imports/ui/util/SimpleTable";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import {AssetTypes} from "/imports/api/asset-types";
const cssFieldColumnContainer = {
display: 'flex',
flexDirection: 'column',
backgroundColor: '#DDD',
padding: '0.5rem',
border: '1px solid #999',
borderRadius: '0.2rem'
}
const cssEditorField = {
marginTop: '0.6rem',
}
const cssGridFieldContainer = {
display: 'grid',
gridTemplateColumns: "1fr 1fr",
columnGap: '1rem',
rowGap: '0.4rem',
marginBottom: '1.5rem'
}
const cssButtonContainer = {
display: 'flex',
gap: '1rem',
justifyContent: 'flex-end'
}
const AssetTypeEditor = ({value, close}) => {
const [year, setYear] = useState(value.year || "")
const [name, setName] = useState(value.name || "")
const [description, setDescription] = useState(value.description || "")
const applyChanges = () => {
close()
//TODO Should invert this and only close if there was success on the server.
if(value._id)
Meteor.call("assetType.update", value._id, name, description, year);
else
Meteor.call("assetType.add", name, description, year);
}
const rejectChanges = () => {
close()
}
const change = (e) => {
console.log(e);
setYear(e.target.value);
}
return (
<div style={cssFieldColumnContainer}>
<h1>Asset Type Editor</h1>
<div style={cssGridFieldContainer}>
<TextField variant="standard" style={cssEditorField} label="Year" value={year} onChange={(e) => change}/>
<TextField variant="standard" style={cssEditorField} label="Name" value={name} onChange={(e) => {setName(e.target.value)}}/>
<TextField variant="outlined" style={{gridColumn: '1 / span 2',...cssEditorField}} multiline rows={4} label="Description" value={description} onChange={(e) => {setDescription(e.target.value)}}/>
</div>
<div style={cssButtonContainer}>
<Button variant="contained" className="button accept-button" onClick={applyChanges}>Accept</Button>
<Button variant="outlined" className="button reject-button" onClick={rejectChanges}>Reject</Button>
</div>
</div>
)
}
export default () => {
Meteor.subscribe('assetTypes');
const {assetTypes} = useTracker(() => {
let assetTypes = AssetTypes.find().fetch();
return {assetTypes}
});
const columns = [
{
name: "Year",
value: (row) => row.year,
},
{
name: "Name",
value: (row) => row.name,
},
{
name: "Description",
value: (row) => row.description,
},
]
const options = {
key: (row) => row._id,
editor: (row, close) => {return (<AssetTypeEditor value={row} close={close}/>)},
add: true,
maxHeight: '40rem',
keyHandler: (e, selected) => {
if(selected && selected._id && e.key === "Delete") {
Meteor.call("assetType.remove", selected._id);
}
}
}
return (
<SimpleTable rows={assetTypes} columns={columns} options={options}/>
)
}

View File

@@ -0,0 +1,6 @@
import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
export default () => {return (<div>None</div>)}

View File

@@ -0,0 +1,90 @@
import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import SimpleTable from "/imports/ui/util/SimpleTable";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import {Sites} from "/imports/api/sites";
const cssEditorField = {
margin: '0.6rem 0',
}
const cssFieldContainer = {
display: 'flex',
flexDirection: 'column',
backgroundColor: '#DDD',
padding: '0.5rem',
border: '1px solid #999',
borderRadius: '0.2rem',
}
const cssButtonContainer = {
display: 'flex',
gap: '1rem',
justifyContent: 'flex-end'
}
const SiteEditor = ({value, close}) => {
const [name, setName] = useState(value.name || "")
const applyChanges = () => {
close()
//TODO Should invert this and only close if there was success on the server.
if(value._id)
Meteor.call("sites.update", value._id, name);
else
Meteor.call("sites.add", name);
}
const rejectChanges = () => {
close()
}
return (
<div style={cssFieldContainer}>
<h1>Site Editor</h1>
<TextField style={cssEditorField} variant="standard" label="Name" value={name} onChange={(e) => {setName(e.target.value)}}/>
<div style={cssButtonContainer}>
<Button variant="contained" className="button accept-button" onClick={applyChanges}>Accept</Button>
<Button variant="outlined" className="button reject-button" onClick={rejectChanges}>Reject</Button>
</div>
</div>
)
}
export default () => {
Meteor.subscribe('sites');
const {sites} = useTracker(() => {
const sites = Sites.find({}).fetch();
return {
sites
}
});
const columns = [
{
name: "Name",
value: (row) => row.name,
},
]
const options = {
key: (row) => row._id,
editor: (row, close) => {return (<SiteEditor value={row} close={close}/>)},
add: true,
maxHeight: '40rem',
keyHandler: (e, selected) => {
if(selected && selected._id && e.key === "Delete") {
Meteor.call("sites.remove", selected._id);
}
}
}
return (
<>
<SimpleTable rows={sites} columns={columns} options={options}/>
</>
)
}

View File

@@ -0,0 +1,153 @@
import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import SimpleTable from "/imports/ui/util/SimpleTable";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import {Staff} from "/imports/api/staff";
import {Sites} from "/imports/api/sites";
const cssSitesSelect = {
margin: '0.6rem 0',
minWidth: '20rem',
}
const cssFieldColumnContainer = {
display: 'flex',
flexDirection: 'column',
backgroundColor: '#DDD',
padding: '0.5rem',
border: '1px solid #999',
borderRadius: '0.2rem'
}
const cssGridFieldContainer = {
display: 'grid',
gridTemplateColumns: "1fr 1fr 1fr",
columnGap: '1rem',
rowGap: '0.4rem',
marginBottom: '1.5rem'
}
const cssButtonContainer = {
display: 'flex',
gap: '1rem',
justifyContent: 'flex-end'
}
const StaffEditor = ({value, close, defaultSiteId}) => {
const [email, setEmail] = useState(value.email || "")
const [id, setId] = useState(value.id || "")
const [firstName, setFirstName] = useState(value.firstName || "")
const [lastName, setLastName] = useState(value.lastName || "")
const [siteId, setSiteId] = useState(value.siteId ? value.siteId : defaultSiteId)
const {sites} = useTracker(() => {
let sites = Sites.find({}).fetch();
return {sites}
});
if(!siteId && sites && sites.length > 0) {
setSiteId(sites[0]._id)
}
const applyChanges = () => {
close()
//TODO Should invert this and only close if there was success on the server.
if(value._id)
Meteor.call("staff.update", value._id, id, firstName, lastName, email, siteId);
else
Meteor.call("staff.add", id, firstName, lastName, email, siteId);
}
const rejectChanges = () => {
close()
}
return (
<div style={cssFieldColumnContainer}>
<h1>Staff Editor</h1>
<div style={cssGridFieldContainer}>
<TextField variant="standard" label="ID" value={id} onChange={(e) => {setId(e.target.value)}}/>
<TextField variant="standard" label="Email" value={email} onChange={(e) => {setEmail(e.target.value)}}/>
<div/>
<TextField variant="standard" label="First Name" value={firstName} onChange={(e) => {setFirstName(e.target.value)}}/>
<TextField variant="standard" label="Last Name" value={lastName} onChange={(e) => {setLastName(e.target.value)}}/>
<TextField select variant="standard" label="Site" value={siteId} onChange={(e) => {setSiteId(e.target.value)}}>
{sites.map((next, i) => {
return <MenuItem key={next._id} value={next._id}>{next.name}</MenuItem>
})}
</TextField>
</div>
<div style={cssButtonContainer}>
<Button variant="contained" className="button accept-button" onClick={applyChanges}>Accept</Button>
<Button variant="outlined" className="button reject-button" onClick={rejectChanges}>Reject</Button>
</div>
</div>
)
}
export default () => {
const siteAll = {_id: 0, name: "All"}
const [site, setSite] = useState(siteAll._id)
Meteor.subscribe('sites');
Meteor.subscribe('staff');
const {sites} = useTracker(() => {
const sites = Sites.find({}).fetch();
sites.push(siteAll);
return {sites}
});
const {staff} = useTracker(() => {
const staffQuery = site === siteAll._id ? {} : {siteId: site}
let staff = Staff.find(staffQuery).fetch();
return {staff}
});
const columns = [
{
name: "ID",
value: (row) => row.id,
},
{
name: "Email",
value: (row) => row.email,
},
{
name: "First Name",
value: (row) => row.firstName,
},
{
name: "Last Name",
value: (row) => row.lastName,
},
]
const options = {
key: (row) => row._id,
editor: (row, close) => {return (<StaffEditor value={row} close={close} defaultSiteId={site}/>)},
add: true,
maxHeight: '40rem',
keyHandler: (e, selected) => {
if(selected && selected._id && e.key === "Delete") {
Meteor.call("staff.remove", selected._id);
}
}
}
return (
<>
<TextField label="Site" style={cssSitesSelect} select variant="standard" value={site} onChange={(e)=>{setSite(e.target.value)}}>
{sites.map((next, i) => {
return <MenuItem key={next._id} value={next._id}>{next.name}</MenuItem>
})}
</TextField>
<SimpleTable rows={staff} columns={columns} options={options}/>
</>
)
}

View File

@@ -0,0 +1,158 @@
import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import SimpleTable from "/imports/ui/util/SimpleTable";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import {Students} from "/imports/api/students";
import {Sites} from "/imports/api/sites";
const cssSitesSelect = {
margin: '0.6rem 0',
minWidth: '20rem',
}
const cssFieldColumnContainer = {
display: 'flex',
flexDirection: 'column',
backgroundColor: '#DDD',
padding: '0.5rem',
border: '1px solid #999',
borderRadius: '0.2rem'
}
const cssGridFieldContainer = {
display: 'grid',
gridTemplateColumns: "1fr 1fr 1fr",
columnGap: '1rem',
rowGap: '0.4rem',
marginBottom: '1.5rem'
}
const cssButtonContainer = {
display: 'flex',
gap: '1rem',
justifyContent: 'flex-end'
}
const StudentEditor = ({value, close, defaultSiteId}) => {
const [email, setEmail] = useState(value.email || "")
const [id, setId] = useState(value.id || "")
const [firstName, setFirstName] = useState(value.firstName || "")
const [lastName, setLastName] = useState(value.lastName || "")
const [grade, setGrade] = useState(value.grade || "")
const [siteId, setSiteId] = useState(value.siteId ? value.siteId : defaultSiteId)
const {sites} = useTracker(() => {
let sites = Sites.find({}).fetch();
return {sites}
});
if(!siteId && sites && sites.length > 0) {
setSiteId(sites[0]._id)
}
const applyChanges = () => {
close()
//TODO Should invert this and only close if there was success on the server.
if(value._id)
Meteor.call("students.update", value._id, id, firstName, lastName, email, siteId, grade);
else
Meteor.call("students.add", id, firstName, lastName, email, siteId, grade);
}
const rejectChanges = () => {
close()
}
return (
<div style={cssFieldColumnContainer}>
<h1>Student Editor</h1>
<div style={cssGridFieldContainer}>
<TextField variant="standard" label="ID" value={id} onChange={(e) => {setId(e.target.value)}}/>
<TextField variant="standard" label="Email" value={email} onChange={(e) => {setEmail(e.target.value)}}/>
<TextField variant="standard" label="Grade" value={grade} onChange={(e) => {setGrade(e.target.value)}}/>
<TextField variant="standard" label="First Name" value={firstName} onChange={(e) => {setFirstName(e.target.value)}}/>
<TextField variant="standard" label="Last Name" value={lastName} onChange={(e) => {setLastName(e.target.value)}}/>
<TextField select variant="standard" label="Site" value={siteId} onChange={(e) => {setSiteId(e.target.value)}}>
{sites.map((next, i) => {
return <MenuItem key={next._id} value={next._id}>{next.name}</MenuItem>
})}
</TextField>
</div>
<div style={cssButtonContainer}>
<Button variant="contained" className="button accept-button" onClick={applyChanges}>Accept</Button>
<Button variant="outlined" className="button reject-button" onClick={rejectChanges}>Reject</Button>
</div>
</div>
)
}
export default () => {
const siteAll = {_id: 0, name: "All"}
const [site, setSite] = useState(siteAll._id)
Meteor.subscribe('sites');
Meteor.subscribe('students');
const {sites} = useTracker(() => {
const sites = Sites.find({}).fetch();
sites.push(siteAll);
return {sites}
});
const {students} = useTracker(() => {
const studentQuery = site === siteAll._id ? {} : {siteId: site}
let students = Students.find(studentQuery).fetch();
return {students}
});
const columns = [
{
name: "ID",
value: (row) => row.id,
},
{
name: "Email",
value: (row) => row.email,
},
{
name: "First Name",
value: (row) => row.firstName,
},
{
name: "Last Name",
value: (row) => row.lastName,
},
{
name: "GRD",
value: (row) => row.grade,
},
]
const options = {
key: (row) => row._id,
editor: (row, close) => {return (<StudentEditor value={row} close={close} defaultSiteId={site}/>)},
add: true,
maxHeight: '40rem',
keyHandler: (e, selected) => {
if(selected && selected._id && e.key === "Delete") {
Meteor.call("students.remove", selected._id);
}
}
}
return (
<>
<TextField label="Site" style={cssSitesSelect} select variant="standard" value={site} onChange={(e)=>{setSite(e.target.value)}}>
{sites.map((next, i) => {
return <MenuItem key={next._id} value={next._id}>{next.name}</MenuItem>
})}
</TextField>
<SimpleTable rows={students} columns={columns} options={options}/>
</>
)
}

View File

@@ -0,0 +1,30 @@
import { Meteor } from 'meteor/meteor';
import React, {lazy, Suspense, useState} from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import TabNav from '../util/TabNav';
export default () => {
let tabs = [
{
title: "Asset List",
getElement: () => {
const AssetList = lazy(()=>import('./Assets/AssetList'))
return <AssetList/>
},
path: '/assetList',
href: 'assetList'
},
{
title: "Add Assets",
getElement: () => {
const AddAssets = lazy(()=>import('./Assets/AddAssets'))
return <AddAssets/>
},
path: '/addAssets',
href: 'addAssets'
},
]
return <TabNav tabs={tabs}/>
}

View File

@@ -0,0 +1,134 @@
import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import { useTheme } from '@mui/material/styles';
import _ from 'lodash';
import SimpleTable from "/imports/ui/util/SimpleTable";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Select from '@mui/material/Select';
import Chip from '@mui/material/Chip';
import MenuItem from '@mui/material/MenuItem';
import {InputLabel, List, ListItem, ListItemButton, ListItemText} from "@mui/material";
import {Assets, conditions} from "/imports/api/assets";
import {AssetTypes} from "/imports/api/asset-types";
import Box from "@mui/material/Box";
import OutlinedInput from '@mui/material/OutlinedInput';
import FormControl from '@mui/material/FormControl';
const cssContainer = {
display: 'flex',
flexDirection: 'row',
marginTop: '2rem',
backgroundColor: '#DDD',
padding: '0.5rem',
border: '1px solid #999',
borderRadius: '0.2rem'
}
const cssComponent = {
width: '100%',
marginTop: '1rem',
}
const cssEditorField = {
margin: '0.6rem 1rem',
minWidth: '10rem'
}
const AddAssets = ({assetTypes}) => {
const theme = useTheme();
const [selectedAssetTypes, setSelectedAssetTypes] = useState([])
const [selectedAssetType, setSelectedAssetType] = useState("")
const [assetId, setAssetId] = useState("")
const [serial, setSerial] = useState("")
const [condition, setCondition] = useState("New")
const [conditionDetails, setConditionDetails] = useState("")
const getSelectItemStyles = (value) => {
return {
fontWeight: selectedAssetTypes.indexOf(value) === -1 ? theme.typography.fontWeightRegular : theme.typography.fontWeightBold
}
}
const getAssetTypeListItemStyle = (assetType) => {
return {
backgroundColor: selectedAssetType === assetType ? '#EECFA6' : 'white'
}
}
const addAsset = () => {
//TODO: Check the inputs.
Meteor.call("assets.add", selectedAssetType._id, assetId, serial, condition, conditionDetails);
setAssetId("")
setSerial("")
}
return (
<>
<Box style={cssContainer}>
<FormControl style={cssComponent}>
<InputLabel id="selectAssetTypesLabel">Available Asset Types</InputLabel>
<Select labelId='selectAssetTypesLabel' multiple variant="standard"
value={selectedAssetTypes} onChange={(e)=>{setSelectedAssetTypes(e.target.value)}}
renderValue={(selected) => (
<Box sx={{display: 'flex', flexWrap: 'wrap', gap: 0.5}}>
{selected.map((value) => (
<Chip key={value.name} label={value.name}/>
))}
</Box>
)}
>
{assetTypes.map((assetType, i) => {
return <MenuItem key={i} value={assetType} style={getSelectItemStyles(assetType)}>{assetType.name}</MenuItem>
})}
</Select>
</FormControl>
</Box>
<Box style={cssContainer}>
<div style={{maxHeight: '26rem', overflowY:'auto', minWidth: '10rem', minHeight: '10rem'}}>
<List>
{selectedAssetTypes.map((next, i) => {
return (
<ListItemButton key={next._id} style={getAssetTypeListItemStyle(next)} selected={selectedAssetType === next} onClick={(e) => {setSelectedAssetType(next)}}>
<ListItemText primary={next.name} secondary={next.description}/>
</ListItemButton>
)
})}
</List>
</div>
<div style={{marginLeft: '1rem', display: 'flex', flexDirection: 'column'}}>
<div style={{display: 'flex', flexDirection: 'row'}}>
<TextField style={cssEditorField} variant="standard" label="Asset ID" value={assetId} onChange={(e) => {setAssetId(e.target.value)}}/>
<TextField style={cssEditorField} variant="standard" label="Serial" value={serial} onChange={(e) => {setSerial(e.target.value)}}/>
</div>
<div style={{display: 'flex', flexDirection: 'row'}}>
<TextField style={cssEditorField} select variant="standard" label="Condition" value={condition} onChange={(e)=>{setCondition(e.target.value)}}>
{conditions.map((condition, i) => {
return <MenuItem key={i} value={condition}>{condition}</MenuItem>
})}
</TextField>
</div>
<div style={{display: 'flex', flexDirection: 'row'}}>
<TextField style={{width: '100%', margin: '1rem'}} multiline variant="outlined" rows={4} label="Condition Details" value={conditionDetails} onChange={(e) => {setConditionDetails(e.target.value)}}/>
</div>
</div>
</Box>
<div style={{display: 'flex', flexDirection: 'row-reverse'}}>
<Button variant="contained" className="button" onClick={addAsset}>Add</Button>
</div>
</>
)
}
export default () => {
Meteor.subscribe('assetTypes');
const {assetTypes} = useTracker(() => {
const assetTypes = AssetTypes.find({}, {sort: {year: -1}}).fetch();
return {
assetTypes
}
});
return (
<AddAssets assetTypes={assetTypes}/>
)
}

View File

@@ -0,0 +1,135 @@
import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import SimpleTable from "/imports/ui/util/SimpleTable";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import {Assets, conditions} from "/imports/api/assets";
import {AssetTypes} from "/imports/api/asset-types";
const cssEditorField = {
margin: '0.6rem 0',
}
const cssGridFieldContainer = {
display: 'grid',
gridTemplateColumns: "1fr 1fr",
columnGap: '1rem',
rowGap: '0.4rem',
marginBottom: '1.5rem'
}
const cssFieldContainer = {
display: 'flex',
flexDirection: 'column',
backgroundColor: '#DDD',
padding: '0.5rem',
border: '1px solid #999',
borderRadius: '0.2rem',
}
const cssButtonContainer = {
display: 'flex',
gap: '1rem',
justifyContent: 'flex-end'
}
const AssetEditor = ({value, close}) => {
const [assetId, setAssetId] = useState(value.assetId || "")
const [serial, setSerial] = useState(value.serial || "")
const [condition, setCondition] = useState(value.condition || "")
const [conditionDetails, setConditionDetails] = useState(value.conditionDetails || "")
const [assetTypeId, setAssetTypeId] = useState(value.assetTypeId || "")
const assetTypes = AssetTypes.find({}, {sort: {year: -1}});
const applyChanges = () => {
close()
//TODO Should invert this and only close if there was success on the server.
if(value._id)
Meteor.call("assets.update", value._id, assetTypeId, assetId, serial, condition, conditionDetails);
else
Meteor.call("assets.add", assetTypeId, assetId, serial, condition, conditionDetails);
}
const rejectChanges = () => {
close()
}
return (
<div style={cssFieldContainer}>
<h1>Asset Editor</h1>
<div style={cssGridFieldContainer}>
<TextField style={cssEditorField} select variant="standard" value={assetTypeId} onChange={(e)=>{setAssetTypeId(e.target.value)}} label="Asset Type">
{assetTypes.map((assetType, i) => {
return <MenuItem key={i} value={assetType._id}>{assetType.name}</MenuItem>
})}
</TextField>
<TextField style={cssEditorField} variant="standard" label="Asset ID" value={assetId} onChange={(e) => {setAssetId(e.target.value)}}/>
<TextField style={cssEditorField} select variant="standard" label="Condition" value={condition} onChange={(e)=>{setCondition(e.target.value)}}>
{conditions.map((condition, i) => {
return <MenuItem key={i} value={condition}>{condition}</MenuItem>
})}
</TextField>
<TextField style={cssEditorField} variant="standard" label="Serial" value={serial} onChange={(e) => {setSerial(e.target.value)}}/>
<TextField style={{gridColumn: '1 / span 2',...cssEditorField}} multiline variant="outlined" rows={4} label="Condition Details" value={conditionDetails} onChange={(e) => {setConditionDetails(e.target.value)}}/>
</div>
<div style={cssButtonContainer}>
<Button variant="contained" style={{gridColumn: '2/2'}} className="button accept-button" onClick={applyChanges}>Accept</Button>
<Button type="outlined" style={{gridColumn: '3/3'}} className="button reject-button" onClick={rejectChanges}>Reject</Button>
</div>
</div>
)
}
export default () => {
Meteor.subscribe('assetTypes');
Meteor.subscribe('assets');
const {assets} = useTracker(() => {
const assets = Assets.find({}).fetch();
const assetTypes = AssetTypes.find({}, {sort: {year: -1}}).fetch();
const assetTypeNameMap = assetTypes.reduce((map, obj) => {
map[obj._id] = obj;
return map;
}, {})
for(let asset of assets) {
asset.assetType = assetTypeNameMap[asset.assetTypeId]
}
return {
assets
}
});
const columns = [
{
name: "Asset ID",
value: (row) => row.assetId,
},
{
name: "Serial",
value: (row) => row.serial,
},
{
name: "Condition",
value: (row) => row.condition,
},
{
name: "AssetType",
value: (row) => row.assetType.name,
},
]
const options = {
key: (row) => row._id,
editor: (row, close) => {return (<AssetEditor value={row} close={close}/>)},
add: true,
maxHeight: '40rem'
}
return (
<>
<SimpleTable rows={assets} columns={columns} options={options}/>
</>
)
}

View File

@@ -0,0 +1,30 @@
import { Meteor } from 'meteor/meteor';
import React, {lazy, Suspense, useState} from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import TabNav from '../util/TabNav';
export default () => {
let tabs = [
{
title: "By Person",
getElement: () => {
const ByPerson = lazy(()=>import('./Assignments/ByPerson'))
return <ByPerson/>
},
path: '/byPerson',
href: 'byPerson'
},
{
title: "By Asset",
getElement: () => {
const ByAsset = lazy(()=>import('./Assignments/ByAsset'))
return <ByAsset/>
},
path: '/byAsset',
href: 'byAsset'
},
]
return <TabNav tabs={tabs}/>
}

View File

@@ -0,0 +1,139 @@
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 SimpleTable from "/imports/ui/util/SimpleTable";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Select from '@mui/material/Select';
import Chip from '@mui/material/Chip';
import MenuItem from '@mui/material/MenuItem';
import {InputLabel, List, ListItem, ListItemButton, ListItemText} from "@mui/material";
import Box from "@mui/material/Box";
import OutlinedInput from '@mui/material/OutlinedInput';
import FormControl from '@mui/material/FormControl';
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";
const cssTwoColumnContainer = {
display: 'grid',
gridTemplateColumns: "1fr 1fr",
columnGap: '1rem',
rowGap: '0.4rem',
}
const cssEditorField = {
minWidth: '10rem'
}
const AssignmentsByAsset = () => {
const theme = useTheme();
const [assetId, setAssetId] = useState("")
//Dialog stuff.
const [openUnassignDialog, setOpenUnassignDialog] = useState(false)
const [unassignCondition, setUnassignCondition] = useState(conditions[2])
const [unassignComment, setUnassignComment] = useState("")
const [unassignConditionDetails, setUnassignConditionDetails] = useState("")
const [assetIdInput, setAssetIdInput] = useState(undefined)
const {foundAsset} = useTracker(() => {
let foundAsset = null;
if(assetId) {
foundAsset = Assets.findOne({assetId: assetId});
if(foundAsset) {
foundAsset.assetType = AssetTypes.findOne({_id: foundAsset.assetTypeId})
if(foundAsset.assigneeId)
foundAsset.assignee = foundAsset.assigneeType === "Student" ? Students.findOne({_id: foundAsset.assigneeId}) : Staff.findOne({_id: foundAsset.assigneeId})
}
}
return {foundAsset}
});
useEffect(() => {
if(assetIdInput) assetIdInput.focus()
})
const unassign = () => {
// Open the dialog to get condition and comment.
setUnassignComment("")
setUnassignCondition(foundAsset.condition ? foundAsset.condition : conditions[2])
setUnassignConditionDetails(foundAsset.conditionDetails || "")
setOpenUnassignDialog(true);
}
const unassignDialogClosed = (unassign) => {
setOpenUnassignDialog(false)
if(unassign === true) {
// Call assets.unassign(assetId, comment, condition, conditionDetails, date)
Meteor.call('assets.unassign', foundAsset.assetId, unassignComment, unassignCondition, unassignConditionDetails)
}
}
return (
<>
<Dialog open={openUnassignDialog} onClose={unassignDialogClosed}>
<DialogTitle>Unassign Asset</DialogTitle>
<DialogContent style={{display: 'flex', flexDirection: 'column'}}>
<div>
<TextField style={cssEditorField} select variant="standard" label="Condition" value={unassignCondition} onChange={(e)=>{setUnassignCondition(e.target.value)}}>
{conditions.map((condition, i) => {
return <MenuItem key={i} value={condition}>{condition}</MenuItem>
})}
</TextField>
</div>
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} variant="standard" label="Comment" value={unassignComment} onChange={(e) => {setUnassignComment(e.target.value)}}/>
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} multiline rows={4} variant="outlined" label="Condition Details" value={unassignConditionDetails} onChange={(e) => {setUnassignConditionDetails(e.target.value)}}/>
</DialogContent>
<DialogActions>
<Button onClick={() => unassignDialogClosed(true)}>Unassign</Button>
<Button onClick={() => unassignDialogClosed(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Box style={{marginTop: '1rem',...cssTwoColumnContainer}}>
<TextField style={cssEditorField} variant="standard" label="Asset ID" inputRef={input=>setAssetIdInput(input)} value={assetId} onChange={(e) => {setAssetId(e.target.value)}}/>
</Box>
{foundAsset && (
<div>
<div>Serial: {foundAsset.serial}</div>
<div>Condition: {foundAsset.condition}</div>
<div>Condition Details: {foundAsset.conditionDetails}</div>
{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>
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign()}>Unassign</Button>
</>
)}
</div>
)}
</>
)
}
export default () => {
Meteor.subscribe('students');
Meteor.subscribe('staff');
Meteor.subscribe('assetTypes');
Meteor.subscribe('assets');
return (
<AssignmentsByAsset/>
)
}

View File

@@ -0,0 +1,282 @@
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 SimpleTable from "/imports/ui/util/SimpleTable";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Select from '@mui/material/Select';
import Chip from '@mui/material/Chip';
import MenuItem from '@mui/material/MenuItem';
import {InputLabel, List, ListItem, ListItemButton, ListItemText} from "@mui/material";
import Box from "@mui/material/Box";
import OutlinedInput from '@mui/material/OutlinedInput';
import FormControl from '@mui/material/FormControl';
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";
const cssTwoColumnContainer = {
display: 'grid',
gridTemplateColumns: "1fr 1fr",
columnGap: '1rem',
rowGap: '0.4rem',
}
const cssEditorField = {
minWidth: '10rem'
}
const AssignmentsByPerson = () => {
const theme = useTheme();
const [searchType, setSearchType] = useState("Email")
const [search, setSearch] = useState("")
const [selectedPerson, setSelectedPerson] = useState("")
const [assetId, setAssetId] = useState("")
const [openAssignDialog, setOpenAssignDialog] = useState(false)
const [assignCondition, setAssignCondition] = useState(conditions[2])
const [assignConditionDetails, setAssignConditionDetails] = useState("")
//Dialog stuff.
const [openUnassignDialog, setOpenUnassignDialog] = useState(false)
const [unassignCondition, setUnassignCondition] = useState(conditions[2])
const [unassignComment, setUnassignComment] = useState("")
const [unassignConditionDetails, setUnassignConditionDetails] = useState("")
const [unassignAsset, setUnassignAsset] = useState(undefined)
const [assetIdInput, setAssetIdInput] = useState(undefined)
const {people} = useTracker(() => {
let people = [];
if(search && search.length > 1) {
let query;
if(searchType === "Email") {
query = {email: {$regex: search, $options: 'i'}};
} else if(searchType === 'First Name') {
query = {firstName: {$regex: search, $options: 'i'}}
} else {
query = {lastName: {$regex: search, $options: 'i'}}
}
const students = Students.find(query).fetch();
const staff = Staff.find(query).fetch();
for(let next of students) next.type = "Student"
for(let next of staff) next.type = "Staff"
people = [...staff, ...students]
}
return {people}
});
const {assets} = useTracker(() => {
let assets = [];
if(selectedPerson) {
assets = Assets.find({assigneeId: selectedPerson._id}).fetch();
for(let next of assets) {
next.assetType = AssetTypes.findOne({_id: next.assetTypeId})
}
}
return {assets}
});
const {foundAsset} = useTracker(() => {
let foundAsset = null;
if(assetId) {
foundAsset = Assets.findOne({assetId: assetId});
if(foundAsset) {
foundAsset.assetType = AssetTypes.findOne({_id: foundAsset.assetTypeId})
if(foundAsset.assigneeId)
foundAsset.assignee = foundAsset.assigneeType === "Student" ? Students.findOne({_id: foundAsset.assigneeId}) : Staff.findOne({_id: foundAsset.assigneeId})
}
}
return {foundAsset}
});
const getListItemStyle = (item) => {
return {
backgroundColor: selectedPerson === item ? '#EECFA6' : 'white'
}
}
const assign = () => {
if(foundAsset) {
//Open the dialog to get condition.
setUnassignCondition(foundAsset.condition ? foundAsset.condition : conditions[2])
setUnassignConditionDetails(foundAsset.conditionDetails || "")
setOpenAssignDialog(true)
}
}
const assignDialogClosed = (assign) => {
setOpenAssignDialog(false)
if(assign === true) {
// Call assets.assign
Meteor.call('assets.assign', foundAsset.assetId, selectedPerson.type, selectedPerson._id, assignCondition, assignConditionDetails)
setAssetId("")
// Set the focus back to the asset id text field
// document.getElementById('assetIdInput').focus()
// useEffect(() => {
// if(assetIdInput) assetIdInput.focus()
// })
}
}
useEffect(() => {
if(assetIdInput) assetIdInput.focus()
})
const unassign = (asset) => {
// Open the dialog to get condition and comment.
setUnassignAsset(asset);
setUnassignComment("")
setUnassignCondition(asset.condition ? asset.condition : conditions[2])
setUnassignConditionDetails(asset.conditionDetails || "")
setOpenUnassignDialog(true);
}
const unassignDialogClosed = (unassign) => {
setOpenUnassignDialog(false)
if(unassign === true) {
// Call assets.unassign(assetId, comment, condition, conditionDetails, date)
Meteor.call('assets.unassign', unassignAsset.assetId, unassignComment, unassignCondition, unassignConditionDetails)
}
}
const getAssetTileStyles = (index) => {
return index % 2 ? {backgroundColor: '#FFF'} : {backgroundColor: '#d2d2d2'}
}
const cssAssetTile = {
padding: '.8rem',
userSelect: 'none',
// '&:nthChild(even)': {backgroundColor: '#935e5e'}
}
return (
<>
<Dialog open={openAssignDialog} onClose={assignDialogClosed}>
<DialogTitle>Assign Asset</DialogTitle>
<DialogContent style={{display: 'flex', flexDirection: 'column'}}>
<div>
<TextField style={cssEditorField} select variant="standard" label="Condition" value={assignCondition} onChange={(e)=>{setAssignCondition(e.target.value)}}>
{conditions.map((condition, i) => {
return <MenuItem key={i} value={condition}>{condition}</MenuItem>
})}
</TextField>
</div>
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} multiline rows={4} variant="outlined" label="Condition Details" value={assignConditionDetails} onChange={(e) => {setAssignConditionDetails(e.target.value)}}/>
</DialogContent>
<DialogActions>
<Button onClick={() => assignDialogClosed(true)}>Assign</Button>
<Button onClick={() => assignDialogClosed(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={openUnassignDialog} onClose={unassignDialogClosed}>
<DialogTitle>Unassign Asset</DialogTitle>
<DialogContent style={{display: 'flex', flexDirection: 'column'}}>
<div>
<TextField style={cssEditorField} select variant="standard" label="Condition" value={unassignCondition} onChange={(e)=>{setUnassignCondition(e.target.value)}}>
{conditions.map((condition, i) => {
return <MenuItem key={i} value={condition}>{condition}</MenuItem>
})}
</TextField>
</div>
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} variant="standard" label="Comment" value={unassignComment} onChange={(e) => {setUnassignComment(e.target.value)}}/>
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} multiline rows={4} variant="outlined" label="Condition Details" value={unassignConditionDetails} onChange={(e) => {setUnassignConditionDetails(e.target.value)}}/>
</DialogContent>
<DialogActions>
<Button onClick={() => unassignDialogClosed(true)}>Unassign</Button>
<Button onClick={() => unassignDialogClosed(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Box style={{marginTop: '1rem',...cssTwoColumnContainer}}>
<ToggleButtonGroup color="primary" value={searchType} exclusive onChange={(e, type)=>setSearchType(type)} aria-label="Search Type">
<ToggleButton value="Email">Email</ToggleButton>
<ToggleButton value="First Name">First Name</ToggleButton>
<ToggleButton value="Last Name">Last Name</ToggleButton>
</ToggleButtonGroup>
<TextField style={cssEditorField} variant="standard" label="Search" value={search} onChange={(e) => {setSearch(e.target.value)}}/>
</Box>
<Box style={cssTwoColumnContainer}>
<div style={{maxHeight: '26rem', overflowY:'auto', minWidth: '10rem', minHeight: '10rem'}}>
<List>
{people.map((next, i) => {
return (
<ListItemButton key={next._id} style={getListItemStyle(next)} selected={selectedPerson === next} onClick={(e) => {setSelectedPerson(next)}}>
<ListItemText primary={next.firstName + " " + next.lastName} secondary={next.email} tertiary={"Hello World"}/>
</ListItemButton>
)
})}
</List>
</div>
<div style={{display: 'flex', flexDirection: 'column', margin: '1rem 0 0 .5rem'}}>
{selectedPerson && (
<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)}}/></div>
<div>{foundAsset && foundAsset.assetType.name}</div>
<div>{foundAsset && foundAsset.serial}</div>
{foundAsset && foundAsset.assignee && (
<div>Assigned To: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName}</div>
)}
<Button variant="contained" color='primary' className="button" disabled={!foundAsset || foundAsset.assignee !== undefined} onClick={()=>assign()}>Assign</Button>
</div>
)}
{assets.map((next, i) => {
return (
<div key={next._id} style={{...getAssetTileStyles(i), ...cssAssetTile}}>
<div>{next.assetType.name}</div>
<div>{next.assetId}</div>
<div>{next.serial}</div>
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign(next)}>Unassign</Button>
</div>
)
})}
{/*<div style={{display: 'flex', flexDirection: 'row'}}>*/}
{/* <TextField style={cssEditorField} variant="standard" label="Asset ID" value={assetId} onChange={(e) => {setAssetId(e.target.value)}}/>*/}
{/* <TextField style={cssEditorField} variant="standard" label="Serial" value={serial} onChange={(e) => {setSerial(e.target.value)}}/>*/}
{/*</div>*/}
{/*<div style={{display: 'flex', flexDirection: 'row'}}>*/}
{/* <TextField style={cssEditorField} select variant="standard" label="Condition" value={condition} onChange={(e)=>{setCondition(e.target.value)}}>*/}
{/* {conditions.map((condition, i) => {*/}
{/* return <MenuItem key={i} value={condition}>{condition}</MenuItem>*/}
{/* })}*/}
{/* </TextField>*/}
{/*</div>*/}
{/*<div style={{display: 'flex', flexDirection: 'row'}}>*/}
{/* <TextField style={{width: '100%', margin: '1rem'}} multiline variant="outlined" rows={4} label="Condition Details" value={conditionDetails} onChange={(e) => {setConditionDetails(e.target.value)}}/>*/}
{/*</div>*/}
</div>
</Box>
</>
)
}
export default () => {
Meteor.subscribe('students');
Meteor.subscribe('staff');
Meteor.subscribe('assetTypes');
Meteor.subscribe('assets');
return (
<AssignmentsByPerson/>
)
}

View File

@@ -0,0 +1,30 @@
import { Meteor } from 'meteor/meteor';
import React, {lazy, Suspense, useState} from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import TabNav from '../util/TabNav';
export default () => {
let tabs = [
{
title: "Chromebook Usage",
getElement: () => {
const ChromebookUsage = lazy(()=>import('./History/ChromebookUsage'))
return <ChromebookUsage/>
},
path: '/chromebookUsage',
href: 'chromebookUsage'
},
{
title: "Asset Assignments",
getElement: () => {
const AssetAssignments = lazy(()=>import('./History/AssetAssignments'))
return <AssetAssignments/>
},
path: '/assetAssignments',
href: 'assetAssignments'
},
]
return <TabNav tabs={tabs}/>
}

View File

@@ -0,0 +1,17 @@
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 SimpleTable from "/imports/ui/util/SimpleTable";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Select from '@mui/material/Select';
import Chip from '@mui/material/Chip';
import MenuItem from '@mui/material/MenuItem';
import {InputLabel, List, ListItem, ListItemButton, ListItemText} from "@mui/material";
import Box from "@mui/material/Box";
import OutlinedInput from '@mui/material/OutlinedInput';
import FormControl from '@mui/material/FormControl';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';

View File

@@ -0,0 +1,38 @@
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 SimpleTable from "/imports/ui/util/SimpleTable";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Select from '@mui/material/Select';
import Chip from '@mui/material/Chip';
import MenuItem from '@mui/material/MenuItem';
import {InputLabel, List, ListItem, ListItemButton, ListItemText} from "@mui/material";
import Paper from '@mui/material/Paper';
import InputBase from '@mui/material/InputBase';
import IconButton from '@mui/material/IconButton';
import SearchIcon from '@mui/icons-material/Search';
import Box from "@mui/material/Box";
import OutlinedInput from '@mui/material/OutlinedInput';
import FormControl from '@mui/material/FormControl';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
export default () => {
return (
<div style={{display: "flex", flexDirection: "column"}} sx={{ p: '2px 4px', display: 'flex', alignItems: 'center', width: 400 }}>
<Paper componet='form'>
<InputBase
sx={{ ml: 1, flex: 1 }}
placeholder="Email"
inputProps={{ 'aria-label': 'Search Email' }}
/>
<IconButton type="button" sx={{ p: '10px' }} aria-label="search">
<SearchIcon />
</IconButton>
</Paper>
</div>
)
}

122
imports/ui/pages/Users.jsx Normal file
View File

@@ -0,0 +1,122 @@
import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import Button from '@mui/material/Button';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import classNames from 'classnames';
Meteor.subscribe('allUsers');
Meteor.subscribe('allRoleAssignments');
let UsersTable = ({rows}) => {
const [selected, setSelected] = useState(undefined);
let selectRow = (e, row) => {
setSelected(row);
}
const [edited, setEdited] = useState(undefined);
const [permissions, setPermissions] = useState(undefined);
let editRow = (e, row) => {
if(row) {
setPermissions({
isAdmin: Roles.userIsInRole(row, "admin", {anyScope: true}),
laptopManagement: Roles.userIsInRole(row, "laptop-management", {anyScope: true}),
})
} else setPermissions(undefined)
setEdited(row);
}
let togglePermission = (permission) => {
permissions[permission] = !permissions[permission]
setPermissions({...permissions})
}
let applyChanges = (e) => {
let roles = [];
if(permissions.isAdmin) {
roles.push('admin');
}
else {
if(permissions.laptopManagement) {
roles.push('laptop-management');
}
}
Meteor.call("users.setUserRoles", edited._id, roles);
setEdited(undefined);
}
let rejectChanges = (e) => {
setEdited(undefined)
}
return (
<TableContainer className="userTable" component={Paper}>
<Table size="small" aria-label="User Table">
<TableHead className="sticky">
<TableRow>
<TableCell className="headerCell">Name</TableCell>
<TableCell className="headerCell">Email</TableCell>
<TableCell className="headerCell">Roles</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row)=>(
<TableRow key={row._id} className={classNames({tableRow: true, selected: (!edited || edited._id !== row._id) && selected && selected._id === row._id})} onDoubleClick={(e) => {editRow(e, row)}} onClick={(e) => selectRow(e, row)}>
{edited && edited._id === row._id ?
<TableCell className="userEditorContainer" colSpan="3">
<div className="userEditorGrid">
<label style={{gridColumn: "1/4", fontWeight: "800", borderBottom: "2px solid #888", marginBottom: "0.5rem"}}>{edited.profile.name}</label>
<FormControlLabel style={{gridColumn: "1/4"}} control={<Checkbox checked={permissions && permissions.isAdmin} onChange={() => togglePermission('isAdmin')}/>} label="Administrator"/>
<div className="insetPermissions" style={{gridColumn: "1/4"}}>
<FormControlLabel control={<Checkbox disabled={permissions && permissions.isAdmin} checked={permissions && permissions.laptopManagement} onChange={() => togglePermission('laptopManagement')}/>} label="Laptop Management"/>
</div>
<Button variant="contained" style={{gridColumn: '2/2'}} className="button accept-button" onClick={applyChanges}>Accept</Button>
<Button type="outlined" style={{gridColumn: '3/3'}} className="button reject-button" onClick={rejectChanges}>Reject</Button>
</div>
</TableCell>
: <>
<TableCell align="left">{row.profile.name}</TableCell>
<TableCell align="left">{row.services && row.services.google ? row.services.google.email : ""}</TableCell>
<TableCell align="left">{row.roles}</TableCell>
</>
}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
/*
* Separate the Meteor calls as much as possible to avoid them being run repeatedly unnecessarily (were being run on every selection in the table).
*/
export default () => {
const {rows} = useTracker(() => {
const rows = Meteor.users.find({}).fetch();
for(let row of rows) {
row.roles = Roles.getRolesForUser(row, {anyScope: true})
}
return {
rows
}
});
return <UsersTable rows={rows}/>
}

2
imports/ui/util/JsBarcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,137 @@
import React, { useState } from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import classNames from 'classnames';
import Button from "@mui/material/Button";
import _ from 'lodash';
// let columns = [
// {
// name: "ID",
// value: (row) => row._id
// }
// ]
//
// let rows = [
// {
// _id: 1234,
// name: "abc",
// value: 123
// }
// ]
//
// let options = {
// key: (row) => row._id,
// editor: (row) => {return (<MyRowEditor value={row}/>)}
// }
const cssTopControls = {
display: 'flex',
flexDirection: 'row-reverse',
}
/*
* Separate the Meteor calls as much as possible to avoid them being run repeatedly unnecessarily (were being run on every selection in the table).
*/
export default ({columns, rows, options}) => {
const [selected, setSelected] = useState(undefined);
let selectRow = (e, row) => {
setSelected(row);
}
const [edited, setEdited] = useState(undefined);
let editRow = (e, row) => {
setEdited(row);
}
const closeEditor = () => {
setEdited(undefined)
}
const addRow = () => {
setEdited({});
}
let containerStyle = options.maxHeight ? {maxHeight: options.maxHeight} : {}
let keyHandler = (e) => {
!edited && options.keyHandler && options.keyHandler(e, selected)
// Close the editor if the user hits escape.
if(edited && e.key === 'Escape') {
setEdited(undefined)
e.stopPropagation()
}
if(!edited && e.key === 'Insert') {
setEdited({})
e.stopPropagation()
}
}
return (
<div className='simpleTableContainer'>
{options.add && <div style={cssTopControls}><Button variant="text" className="button" onClick={addRow}>Add</Button></div>}
<TableContainer className="simpleTable" component={Paper} style={containerStyle}>
<Table size="small" aria-label="Table" tabIndex="0" onKeyDown={keyHandler}>
<TableHead className="sticky">
<TableRow>
{columns.map((column, i) => {return (
<TableCell key={i} className="headerCell">{column.name}</TableCell>
)})}
{/*<TableCell className="headerCell">Name</TableCell>*/}
{/*<TableCell className="headerCell">Email</TableCell>*/}
{/*<TableCell className="headerCell">Roles</TableCell>*/}
</TableRow>
</TableHead>
<TableBody>
{edited && !options.key(edited) && (
<TableRow key="NewEditor" className="tableRow">
<TableCell className="editorContainer" colSpan={columns.length}>
{options.editor(edited, closeEditor)}
</TableCell>
</TableRow>
)}
{rows.map((row, i)=>{
// console.log("Rendering Row " + i)
// console.log(row);
return (
<TableRow key={options.key(row)} className={classNames({tableRow: true, selected: (!edited || options.key(edited) !== options.key(row)) && selected && options.key(selected) === options.key(row)})} onDoubleClick={(e) => {editRow(e, row)}} onClick={(e) => selectRow(e, row)}>
{edited && options.key(edited) === options.key(row) ?
<TableCell className="editorContainer" colSpan={columns.length}>
{options.editor(edited, closeEditor)}
</TableCell>
:
<>
{columns.map((column, ci) => {
// console.log("Rendering Cell")
// console.log(column);
// console.log(column.value(row));
let value = column.value(row);
if(_.isObject(value)) {
console.error("Cannot have an object returned as the value in a table.")
value = JSON.stringify(value)
}
return (
<TableCell key={ci} align="left">{value}</TableCell>
)
})}
</>
}
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
</div>
)
}

View File

@@ -0,0 +1,74 @@
import { Meteor } from 'meteor/meteor';
import React, { useState, useEffect, lazy, Suspense } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box';
import {Route, Routes, useNavigate} from "react-router-dom";
//Example Tabs:
// let tabs = [
// {
// title: "Asset List",
// getElement: () => {
// const AssetList = lazy(()=>import('./Assets/AssetList'))
// return <AssetList/>
// },
// path: '/assetList',
// href: 'assetList'
// },
// {
// title: "Add Assets",
// getElement: () => {
// const AddAssets = lazy(()=>import('./Assets/AddAssets'))
// return <AddAssets/>
// },
// path: '/addAssets',
// href: 'addAssets'
// },
// ]
const LinkTab = (props) => {
let nav = useNavigate()
return <Tab component='a' onClick={(e) => {
e.preventDefault()
nav(props.href)
}} {...props}/>
}
export default ({tabs}) => {
let pathName = location.pathname;
let initialTab = tabs.findIndex(tab => {return pathName.endsWith(tab.path)})
let defaultTabPath = tabs[0].path.slice(0, tabs[0].path.lastIndexOf('/'))
if(initialTab === -1) {
initialTab = 0
}
const [value, setValue] = useState(initialTab)
const valueChanged = (e, newValue) => {
setValue(newValue)
}
// console.log(defaultTabPath)
return (
<>
<Box sx={{width: '100%'}}>
<Tabs value={value} onChange={valueChanged} aria-label='nav tabs'>
{tabs.map((tab, i) => {return (
<LinkTab key={i} label={tab.title} href={tab.href}/>
)})}
</Tabs>
</Box>
<Suspense fallback={<div/>}>
<Routes>
<Route path={defaultTabPath} element={tabs[0].getElement()}/>
{tabs.map((tab, i) => {return (
<Route key={i} path={tab.path} element={tab.getElement()}/>
)})}
</Routes>
</Suspense>
</>
)
}

2363
imports/ui/util/qrious.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6
imports/ui/util/qrious.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long