441 lines
18 KiB
JavaScript
441 lines
18 KiB
JavaScript
import { Meteor } from 'meteor/meteor';
|
|
import React, { useState, useEffect } from 'react';
|
|
import { useTracker } from 'meteor/react-meteor-data';
|
|
import { useTheme } from '@mui/material/styles';
|
|
import _ from 'lodash';
|
|
import TextField from "@mui/material/TextField";
|
|
import Button from "@mui/material/Button";
|
|
import MenuItem from '@mui/material/MenuItem';
|
|
import {InputLabel, List, ListItem, ListItemButton, ListItemText, Switch} from "@mui/material";
|
|
import Box from "@mui/material/Box";
|
|
import ToggleButton from '@mui/material/ToggleButton';
|
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
|
import Dialog from '@mui/material/Dialog';
|
|
import DialogActions from '@mui/material/DialogActions';
|
|
import DialogContent from '@mui/material/DialogContent';
|
|
import DialogContentText from '@mui/material/DialogContentText';
|
|
import DialogTitle from '@mui/material/DialogTitle';
|
|
import {Assets, conditions} from "/imports/api/assets";
|
|
import {AssetTypes} from "/imports/api/asset-types";
|
|
import {Students} from "/imports/api/students";
|
|
import {Staff} from "/imports/api/staff";
|
|
import {Link, useLocation, useNavigate, useNavigationType} from "react-router-dom";
|
|
import { Action as NavigationType } from "@remix-run/router";
|
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
|
import Tab from '@mui/material/Tab';
|
|
import TabContext from '@mui/lab/TabContext';
|
|
import TabList from '@mui/lab/TabList';
|
|
import TabPanel from '@mui/lab/TabPanel';
|
|
|
|
const cssTwoColumnContainer = {
|
|
display: 'grid',
|
|
gridTemplateColumns: "1fr 1fr",
|
|
columnGap: '1rem',
|
|
rowGap: '0.4rem',
|
|
}
|
|
const cssEditorField = {
|
|
minWidth: '10rem'
|
|
}
|
|
|
|
const AssignmentsByPerson = () => {
|
|
const navigate = useNavigate()
|
|
const navigateType = useNavigationType()
|
|
const location = useLocation()
|
|
const state = location.state
|
|
const theme = useTheme()
|
|
// const [searchType, setSearchType] = useState("Email")
|
|
const [search, setSearch] = useState(state && state.search ? state.search : "")
|
|
const [includeInactive, setIncludeInactive] = useState(false)
|
|
const [selectedPerson, setSelectedPerson] = useState(state && state.person ? state.person : "")
|
|
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 [unassignDialogEditConditionOnly, setUnassignDialogEditConditionOnly] = useState(false)
|
|
const [unassignCondition, setUnassignCondition] = useState(conditions[2])
|
|
const [unassignComment, setUnassignComment] = useState("")
|
|
const [unassignConditionDetails, setUnassignConditionDetails] = useState("")
|
|
const [unassignAsset, setUnassignAsset] = useState(undefined)
|
|
|
|
const [searchInput, setSearchInput] = useState(undefined)
|
|
|
|
const {people} = useTracker(async () => {
|
|
let people = [];
|
|
|
|
if(search && search.length > 1) {
|
|
let query;
|
|
|
|
query = {$or: [{email: {$regex: search, $options: 'i'}}, {firstName: {$regex: search, $options: 'i'}}, {firstNameAlias: {$regex: search, $options: 'i'}}, {lastName: {$regex: search, $options: 'i'}}]}
|
|
// 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'}}
|
|
// }
|
|
|
|
// Look for students/staff that are active or whose active flag is not set.
|
|
if(!includeInactive) query = {$and: [query, {$or: [{active: true}, {active: {$exists: false}}]}]}
|
|
|
|
const students = await Students.find(query).fetchAsync();
|
|
const staff = await Staff.find(query).fetchAsync();
|
|
|
|
for(let next of students) next.type = "Student"
|
|
for(let next of staff) next.type = "Staff"
|
|
|
|
people = [...staff, ...students]
|
|
}
|
|
|
|
return {people}
|
|
}, [search]);
|
|
|
|
const {assets} = useTracker(async () => {
|
|
let assets = [];
|
|
|
|
if(selectedPerson) {
|
|
assets = await Assets.find({assigneeId: selectedPerson._id}).fetchAsync();
|
|
|
|
for(let next of assets) {
|
|
next.assetType = AssetTypes.findOne({_id: next.assetTypeId})
|
|
}
|
|
}
|
|
|
|
return {assets}
|
|
}, [selectedPerson]);
|
|
|
|
const {foundAsset} = useTracker(async () => {
|
|
let foundAsset = null;
|
|
|
|
if(assetId) {
|
|
foundAsset = await Assets.findOneAsync({assetId: assetId});
|
|
|
|
if(foundAsset) {
|
|
foundAsset.assetType = await AssetTypes.findOneAsync({_id: foundAsset.assetTypeId})
|
|
|
|
if(foundAsset.assigneeId)
|
|
foundAsset.assignee = foundAsset.assigneeType === "Student" ? await Students.findOneAsync({_id: foundAsset.assigneeId}) : await Staff.findOneAsync({_id: foundAsset.assigneeId})
|
|
}
|
|
}
|
|
|
|
return {foundAsset}
|
|
}, [assetId]);
|
|
|
|
const [usageData, setUsageData] = useState([])
|
|
const [assignmentData, setAssignmentData] = useState([])
|
|
|
|
// Collect the usage and assignment data when the selected person changes.
|
|
useEffect(async () => {
|
|
try {
|
|
if(selectedPerson) {
|
|
let query = selectedPerson.type === "Student" ? {studentId: selectedPerson._id} : {staffId: selectedPerson._id}
|
|
|
|
console.log("Collecting person history")
|
|
console.log(query)
|
|
|
|
await Meteor.callAsync('DataCollection.chromebookData', query, (err, result) => {
|
|
if (err) console.error(err)
|
|
else setUsageData(result)
|
|
})
|
|
await Meteor.callAsync('AssetAssignmentHistory.get', query, (err, result) => {
|
|
if (err) console.error(err)
|
|
else setAssignmentData(result)
|
|
})
|
|
}
|
|
else setUsageData({})
|
|
} catch(e) {console.log("Found error in collecting chromebook history & usage in ByPerson.jsx: " + e)}
|
|
}, [selectedPerson])
|
|
|
|
const getListItemStyle = (item) => {
|
|
return {
|
|
backgroundColor: selectedPerson === item ? '#EECFA6' : 'white'
|
|
}
|
|
}
|
|
const assign = () => {
|
|
if(foundAsset) {
|
|
//Open the dialog to get condition.
|
|
setAssignCondition(foundAsset.condition ? foundAsset.condition : conditions[2])
|
|
setAssignConditionDetails(foundAsset.conditionDetails || "")
|
|
setOpenAssignDialog(true)
|
|
}
|
|
}
|
|
const assignDialogClosed = async (assign) => {
|
|
setOpenAssignDialog(false)
|
|
|
|
if(assign === true) {
|
|
// Call assets.assign
|
|
await Meteor.callAsync('assets.assign', foundAsset.assetId, selectedPerson.type, selectedPerson._id, assignCondition, assignConditionDetails, (err, result) => {
|
|
if(err) console.error(err)
|
|
else {
|
|
// Clear the asset id field and set focus to it.
|
|
setAssetId("")
|
|
if(assetIdInput) assetIdInput.focus()
|
|
}
|
|
})
|
|
|
|
//document.getElementById('assetIdInput').focus()
|
|
}
|
|
}
|
|
|
|
//Force focus to the search input field when initially rendering.
|
|
useEffect(() => {
|
|
if(searchInput) searchInput.focus()
|
|
}, [searchInput])
|
|
|
|
const unassign = (asset, editConditionOnly) => {
|
|
// Open the dialog to get condition and comment.
|
|
setUnassignDialogEditConditionOnly(editConditionOnly)
|
|
setUnassignAsset(asset);
|
|
setUnassignComment("")
|
|
setUnassignCondition(asset.condition ? asset.condition : conditions[2])
|
|
setUnassignConditionDetails(asset.conditionDetails || "")
|
|
setOpenUnassignDialog(true);
|
|
}
|
|
const unassignDialogClosed = async (unassign) => {
|
|
setOpenUnassignDialog(false)
|
|
|
|
if(unassign === true) {
|
|
if(unassignDialogEditConditionOnly) {
|
|
await Meteor.callAsync('assets.updateCondition', unassignAsset._id, unassignCondition, unassignConditionDetails, (err, result) => {
|
|
if(err) console.error(err)
|
|
else if(assetIdInput) assetIdInput.focus()
|
|
})
|
|
}
|
|
else {
|
|
// Call assets.unassign(assetId, comment, condition, conditionDetails, date)
|
|
await Meteor.callAsync('assets.unassign', unassignAsset.assetId, unassignComment, unassignCondition, unassignConditionDetails, (err, result) => {
|
|
if(err) console.error(err)
|
|
else if(assetIdInput) assetIdInput.focus()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const getAssetTileStyles = (index) => {
|
|
return index % 2 ? {backgroundColor: '#FFF'} : {backgroundColor: '#d2d2d2'}
|
|
}
|
|
const cssAssetTile = {
|
|
padding: '.8rem',
|
|
userSelect: 'none',
|
|
// '&:nthChild(even)': {backgroundColor: '#935e5e'}
|
|
}
|
|
|
|
// Changes the selected person and updates the browser history.
|
|
const changeSelectedPerson = (person) => {
|
|
setSelectedPerson(person)
|
|
navigate("/assignments/byPerson", {replace: false, state: {person, search}});
|
|
}
|
|
|
|
// Restore the state if the forward/back/refresh functionality of the browser was utilized.
|
|
useEffect(() => {
|
|
if(!state) navigate("/assignments/byPerson", {replace: true, state: {search: "", person: null}})
|
|
else {
|
|
if(navigateType === "POP" || navigateType === 'REPLACE' || navigateType === "PUSH") {
|
|
setSearch(state.search)
|
|
setSelectedPerson(state.person)
|
|
}
|
|
}
|
|
}, [state, navigateType])
|
|
|
|
const [tab, setTab] = useState('assignments')
|
|
|
|
const RenderUsage = ({data}) => {
|
|
return (
|
|
<>
|
|
<ul>
|
|
{data.map((next, i) => (
|
|
<li key={next._id}>
|
|
{/*{next.person && (*/}
|
|
{/* <>*/}
|
|
{/* User: {next.person.firstName} {next.person.lastName} {next.person.grade ? "~ " + next.person.grade : ""} (<Link to={"/search?email=" + encodeURIComponent(next.email)}>{next.email}</Link>)<br/>*/}
|
|
{/* </>*/}
|
|
{/*)}*/}
|
|
{/*Device ID: <Link to={"/search?deviceId=" + encodeURIComponent(next.deviceId)}>{next.deviceId}</Link><br/>*/}
|
|
{next.asset && (
|
|
<>Asset ID: <Link to={"/assignments/byAsset"} state={{assetId: next.asset.assetId}}>{next.asset.assetId}</Link><br/></>
|
|
)}
|
|
<>Asset Type: {next.assetType ? next.assetType.name : "Unknown"}<br/></>
|
|
Serial: {next.serial}<br/>
|
|
{new Date(next.startTime).toLocaleDateString("en-US") + "-" + new Date(next.endTime).toLocaleDateString("en-US") + " @ " + new Date(next.endTime).toLocaleTimeString("en-US")} ({Math.ceil(((next.endTime ? next.endTime : new Date().getTime()) - next.startTime) / (1000*60*60*24))} days)<br/>
|
|
{next.assignedTo && (
|
|
<>Currently assigned to: {next.assignedTo.firstName} {next.assignedTo.lastName} {next.assignedTo.grade ? "~ " + next.assignedTo.grade : ""} ({next.assignedTo.email})</>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const RenderAssignments = ({data}) => {
|
|
return (
|
|
<>
|
|
<ul>
|
|
{data.map((next, i) => (
|
|
<li key={next._id + "/" + next.assetId}>
|
|
{/*{next.assignee && (*/}
|
|
{/* <>*/}
|
|
{/* User: {next.assignee.firstName} {next.assignee.lastName} {next.assignee.grade ? "~ " + next.assignee.grade : ""} (<Link to={"/search?email=" + encodeURIComponent(next.assignee.email)}>{next.assignee.email}</Link>)<br/>*/}
|
|
{/* </>*/}
|
|
{/*)}*/}
|
|
{next.asset && (
|
|
<>
|
|
Asset ID: <Link to={"/assignments/byAsset"} state={{assetId: next.asset.assetId}}>{next.asset.assetId}</Link><br/>
|
|
</>
|
|
)}
|
|
{next.assetType && (
|
|
<>
|
|
Asset Type: {next.assetType.name}<br/>
|
|
</>
|
|
)}
|
|
Serial: {next.serial}<br/>
|
|
{new Date(next.startDate).toLocaleDateString("en-US") + (next.endDate ? "-" + new Date(next.endDate).toLocaleDateString("en-US") : " - Still Assigned")} ({Math.ceil(((next.endDate ? next.endDate : new Date().getTime()) - next.startDate) / (1000*60*60*24))} days)<br/>
|
|
{next.comment && (
|
|
<>Comment: {next.comment}<br/></>
|
|
)}
|
|
Start Condition: {next.startCondition}<br/>
|
|
{next.startConditionDetails && <>Details: {next.startConditionDetails}<br/></>}
|
|
{next.endDate && (
|
|
<>
|
|
End Condition: {next.endCondition}<br/>
|
|
{next.endConditionDetails && <>Details: {next.endConditionDetails}<br/></>}
|
|
</>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</>
|
|
)
|
|
}
|
|
|
|
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,v) => {setAssignConditionDetails(v)}}/>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => assignDialogClosed(true)}>Assign</Button>
|
|
<Button onClick={() => assignDialogClosed(false)}>Cancel</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
<Dialog open={openUnassignDialog} onClose={unassignDialogClosed}>
|
|
<DialogTitle>{unassignDialogEditConditionOnly ? "Edit Condition" : "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>
|
|
{!unassignDialogEditConditionOnly && <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)}>{unassignDialogEditConditionOnly ? "Save" : "Unassign"}</Button>
|
|
<Button onClick={() => unassignDialogClosed(false)}>Cancel</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
<Box style={{margin: '1rem 0 0 0', padding: '0 1rem',...cssTwoColumnContainer, width: "24rem"}}>
|
|
{/*<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>*/}
|
|
<FormControlLabel sx={{marginTop: '0.7rem'}} control={<Switch variant="standard" checked={includeInactive} onChange={(e) => {setIncludeInactive(e.target.checked)}}/>} label="Inactive"/>
|
|
<TextField style={cssEditorField} variant="standard" label="Search" inputRef={input=>setSearchInput(input)} value={search} onChange={(e) => {setSearch(e.target.value)}}/>
|
|
</Box>
|
|
<Box style={{...cssTwoColumnContainer, gridTemplateColumns: "24rem 1fr"}}>
|
|
<div style={{maxHeight: '26rem', overflowY:'auto', minWidth: '10rem', minHeight: '10rem', maxWidth: '40rem'}}>
|
|
<List>
|
|
{people.map((next, i) => {
|
|
return (
|
|
<ListItemButton key={next._id} style={getListItemStyle(next)} selected={selectedPerson === next} onClick={(e) => {changeSelectedPerson(next)}}>
|
|
<ListItemText primary={next.firstName + " " + (next.firstNameAlias ? "'" + next.firstNameAlias + "' " : "") + next.lastName + (next.grade ? " (" + next.grade + ")" : "")} secondary={next.email}/>
|
|
</ListItemButton>
|
|
)
|
|
})}
|
|
</List>
|
|
</div>
|
|
<div style={{display: 'flex', flexDirection: 'column', margin: '0 0 0 .5rem'}}>
|
|
{selectedPerson && (
|
|
<>
|
|
<h3 style={{margin: "0 0 0.5rem 0"}}>{selectedPerson.firstName + " " + (selectedPerson.firstNameAlias ? "'" + selectedPerson.firstNameAlias + "' " : "") + selectedPerson.lastName + (selectedPerson.grade ? " (" + selectedPerson.grade + ")" : "")}</h3>
|
|
<TabContext value={tab}>
|
|
<Box sx={{borderBottom: 1, borderColor: 'divider'}}>
|
|
<TabList onChange={(e, v)=>setTab(v)}>
|
|
<Tab label="Assignments" value="assignments"/>
|
|
<Tab label="Assignment History" value="assignmentHistory"/>
|
|
<Tab label="Usage History" value="usageHistory"/>
|
|
</TabList>
|
|
</Box>
|
|
<TabPanel value="assignments">
|
|
<div style={{...cssAssetTile, paddingTop: "0"}}>
|
|
<div style={{marginBottom: '1rem'}}><TextField id='assetIdInput' style={cssEditorField} variant="standard" label="Asset ID" value={assetId} onChange={(e) => {setAssetId(e.target.value.toUpperCase())}}/></div>
|
|
{foundAsset && (
|
|
<>
|
|
<div>{foundAsset && foundAsset.assetType.name}</div>
|
|
<div>Asset ID: <Link to={"/search?assetId=" + encodeURIComponent(foundAsset.assetId)}>{foundAsset.assetId}</Link></div>
|
|
<div>Serial: <Link to={"/search?serial=" + encodeURIComponent(foundAsset.serial)}>{foundAsset.serial}</Link></div>
|
|
</>
|
|
)}
|
|
{foundAsset && foundAsset.assignee && (
|
|
<>
|
|
<div>Assigned To: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName}</div>
|
|
<div>Assigned: {new Date(foundAsset.assignmentDate).toLocaleDateString("en-US")} ({Math.ceil((new Date().getTime() - foundAsset.assignmentDate) / (1000*60*60*24))} days)</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 style={{fontWeight: 800}}>{next.assetType.name}</div>
|
|
<div>Asset ID: <Link to={"/assignments/byAsset"} state={{assetId: next.assetId}}>{next.assetId}</Link></div>
|
|
<div>Serial: {next.serial}</div>
|
|
<div>Assigned: {new Date(next.assignmentDate).toLocaleDateString("en-US")} ({Math.ceil((new Date().getTime() - next.assignmentDate) / (1000*60*60*24))} days)</div>
|
|
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign(next, false)}>Unassign</Button>
|
|
{" "}
|
|
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign(next, true)}>Edit</Button>
|
|
</div>
|
|
)
|
|
})}
|
|
</TabPanel>
|
|
<TabPanel value="assignmentHistory">
|
|
<RenderAssignments data={assignmentData}/>
|
|
</TabPanel>
|
|
<TabPanel value="usageHistory">
|
|
<RenderUsage data={usageData}/>
|
|
</TabPanel>
|
|
</TabContext>
|
|
</>
|
|
)}
|
|
</div>
|
|
</Box>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default () => {
|
|
Meteor.subscribe('students');
|
|
Meteor.subscribe('staff');
|
|
Meteor.subscribe('assetTypes');
|
|
Meteor.subscribe('assets');
|
|
|
|
return (
|
|
<AssignmentsByPerson/>
|
|
)
|
|
} |