Initial check in; All but the history pages working.
This commit is contained in:
58
imports/ui/pages/Admin.jsx
Normal file
58
imports/ui/pages/Admin.jsx
Normal 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}/>
|
||||
}
|
||||
113
imports/ui/pages/Admin/AssetTypes.jsx
Normal file
113
imports/ui/pages/Admin/AssetTypes.jsx
Normal 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}/>
|
||||
)
|
||||
}
|
||||
6
imports/ui/pages/Admin/Functions.jsx
Normal file
6
imports/ui/pages/Admin/Functions.jsx
Normal 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>)}
|
||||
90
imports/ui/pages/Admin/Sites.jsx
Normal file
90
imports/ui/pages/Admin/Sites.jsx
Normal 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}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
153
imports/ui/pages/Admin/Staff.jsx
Normal file
153
imports/ui/pages/Admin/Staff.jsx
Normal 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}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
158
imports/ui/pages/Admin/Students.jsx
Normal file
158
imports/ui/pages/Admin/Students.jsx
Normal 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}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
30
imports/ui/pages/Assets.jsx
Normal file
30
imports/ui/pages/Assets.jsx
Normal 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}/>
|
||||
}
|
||||
134
imports/ui/pages/Assets/AddAssets.jsx
Normal file
134
imports/ui/pages/Assets/AddAssets.jsx
Normal 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}/>
|
||||
)
|
||||
}
|
||||
135
imports/ui/pages/Assets/AssetList.jsx
Normal file
135
imports/ui/pages/Assets/AssetList.jsx
Normal 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}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
30
imports/ui/pages/Assignments.jsx
Normal file
30
imports/ui/pages/Assignments.jsx
Normal 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}/>
|
||||
}
|
||||
139
imports/ui/pages/Assignments/ByAsset.jsx
Normal file
139
imports/ui/pages/Assignments/ByAsset.jsx
Normal 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/>
|
||||
)
|
||||
}
|
||||
282
imports/ui/pages/Assignments/ByPerson.jsx
Normal file
282
imports/ui/pages/Assignments/ByPerson.jsx
Normal 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/>
|
||||
)
|
||||
}
|
||||
30
imports/ui/pages/History.jsx
Normal file
30
imports/ui/pages/History.jsx
Normal 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}/>
|
||||
}
|
||||
17
imports/ui/pages/History/AssetAssignments.jsx
Normal file
17
imports/ui/pages/History/AssetAssignments.jsx
Normal 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';
|
||||
38
imports/ui/pages/History/ChromebookUsage.jsx
Normal file
38
imports/ui/pages/History/ChromebookUsage.jsx
Normal 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
122
imports/ui/pages/Users.jsx
Normal 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}/>
|
||||
}
|
||||
Reference in New Issue
Block a user