From 4d20bf73b7d9c64c7c46550b843ba1f3c96fa016 Mon Sep 17 00:00:00 2001 From: "DESKTOP-C9V2M01\\Zeeri" Date: Sun, 30 Jul 2023 14:11:12 -0700 Subject: [PATCH] Modified Assets to update the assetId in the history records when it gets changed. Changing the assetId is handy when a sticker is removed, making it possible to just alter the ID instead of re-printing the sticker. --- imports/api/asset-assignment-history.js | 109 +++++------ imports/api/assets.js | 13 +- imports/api/data-collection.js | 2 +- imports/ui/pages/Assignments/ByAsset.jsx | 214 +++++++++++++++++++--- imports/ui/pages/Assignments/ByPerson.jsx | 210 ++++++++++++++++----- package.json | 2 + 6 files changed, 408 insertions(+), 142 deletions(-) diff --git a/imports/api/asset-assignment-history.js b/imports/api/asset-assignment-history.js index 3a7af45..1c6f5ef 100644 --- a/imports/api/asset-assignment-history.js +++ b/imports/api/asset-assignment-history.js @@ -34,8 +34,16 @@ if (Meteor.isServer) { * @returns {any} Array of Asset Assignment History objects. */ 'AssetAssignmentHistory.get'(params) { + let result + if(Roles.userIsInRole(Meteor.userId(), "laptop-management", {anyScope:true})) { - let query = {}; + let query = {} + let person + let asset + let assetType + + // console.log("AssetAssignmentHistory: ") + // console.log(params) if(params.studentId) check(params.studentId, String) if(params.staffId) check(params.staffId, String) @@ -56,81 +64,52 @@ if (Meteor.isServer) { } } - if(params.serial) query.serial = params.serial; - else if(params.assetId) query.assetId = params.assetId; - else if(params.deviceId) query.deviceId = params.deviceId; - else if(params.studentId) { - query.assigneeId = params.studentId - query.assigneeType = "Student" - } - else if(params.staffId) { - query.assigneeId = params.staffId - query.assigneeType = "Staff" + if(params.serial || params.assetId || params.deviceId) { + if(params.serial) query.serial = params.serial; + else if(params.assetId) query.assetId = params.assetId; + else if(params.deviceId) query.deviceId = params.deviceId; + + asset = Assets.findOne(query) + if(asset) assetType = AssetTypes.findOne({_id: asset.assetTypeId}) } else { - query = undefined; - } - - if(query) { - //Sort by the last time the record was updated from most to least recent. - let result = AssetAssignmentHistory.find(query, {sort: {endDate: -1}}).fetch(); - let assets = []; - - // Get the current assignment for the device or person. - if(query.assetId || query.deviceId || query.serial) { - let asset = Assets.findOne(query) - - if(asset) assets = [asset] + if(params.studentId) { + query.assigneeId = params.studentId + query.assigneeType = "Student" + } + else if(params.staffId) { + query.assigneeId = params.staffId + query.assigneeType = "Staff" } else { - // Find the assets assigned to the person. - assets = Assets.find({assigneeId: params.studentId ? params.studentId : params.staffId}).fetch() + query = undefined; } + + person = query.assigneeType === "Student" ? Students.findOne({id: query.assigneeId}) : Staff.findOne({id: query.assigneeId}) + } + + if(query) { + //Sort by the last time the record was updated from most to least recent. + result = AssetAssignmentHistory.find(query, {sort: {endDate: -1}}).fetch(); - // Prepend a partial assignment history record to the list. We want to show active assignments in the results. - for(let asset of assets) { - if(asset && asset.assigneeId) { - let assetType = AssetTypes.findOne(asset.assetTypeId) - let current = { - _id: 0, - assetKey: asset._id, - assetId: asset.assetId, - serial: asset.serial, - assetTypeName: assetType.name, - assigneeType: asset.assigneeType, - assigneeId: asset.assigneeId, - startDate: asset.assignmentDate, - startCondition: asset.condition, - startConditionDetails: asset.conditionDetails - } - - result = [current, ...result] - } - } - - //Add some additional data to the records. + //Expand the assignee, asset, and asset type data. for(let next of result) { - // console.log(next) - if(next.assetKey) { - next.asset = Assets.findOne({_id: next.assetKey}) + if(person) next.assignee = person + else next.assignee = next.assigneeType === "Student" ? Students.findOne({_id: next.assigneeId}) : Staff.findOne({_id: next.assigneeId}) + + if(asset) { + next.asset = asset + next.assetType = assetType } - else if(next.assetId) { - next.asset = Assets.findOne({assetId: next.assetId}); - } - - if(next.asset) { - next.assetType = AssetTypes.findOne({_id: next.asset.assetTypeId}) - } - - if(next.assigneeId) { - next.assignee = next.asset.assigneeType === "Student" ? Students.findOne({_id: next.assigneeId}) : Staff.findOne({_id: next.assigneeId}) + else { + next.asset = Assets.findOne({assetId: next.assetId}) + if(next.asset) next.assetType = AssetTypes.findOne({_id: next.asset.assetTypeId}) } } - - return result; - } else return null; + } } - else return null; + + return result } }); } \ No newline at end of file diff --git a/imports/api/assets.js b/imports/api/assets.js index f388846..35f7e1e 100644 --- a/imports/api/assets.js +++ b/imports/api/assets.js @@ -123,6 +123,7 @@ Meteor.methods({ if(serial) check(serial, String); check(condition, String); if(conditionDetails) check(conditionDetails, String); + const existing = Assets.findOne({_id}) if(!conditions.includes(condition)) { //Should never happen. @@ -133,6 +134,12 @@ Meteor.methods({ if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) { //TODO: Need to first verify there are no checked out assets to the staff member. Assets.update({_id}, {$set: {assetTypeId, assetId, serial, condition, conditionDetails}}); + + if(assetId !== existing.assetId) { + //When changing the asset id we also need to update the other locations in the data where that ID exists. + // assetAssignmentHistory.assetId + AssetAssignmentHistory.updateMany({assetId: existing.assetId}, {$set: {assetId}}) + } } else throw new Meteor.Error("User Permission Error"); }, @@ -217,7 +224,7 @@ Meteor.methods({ throw new Meteor.Error("Asset is already assigned.", "Cannot assign an asset that has already been assigned."); } else { - Assets.update({assetId}, {$set: {assigneeType, assigneeId, assignmentDate: date, condition, conditionDetails}}); + Assets.update({assetId}, {$set: {assigneeType, assigneeId, assignmentDate: date, condition, conditionDetails, assignedBy: Meteor.userId()}}); } } else { @@ -257,11 +264,11 @@ Meteor.methods({ let assetType = AssetTypes.findOne({_id: asset.assetTypeId}); try { - AssetAssignmentHistory.insert({assetKey: asset._id, assetId, serial: asset.serial, assetTypeName: (assetType ? assetType.name : "UNK"), assigneeType: asset.assigneeType, assigneeId: asset.assigneeId, startDate: asset.assignmentDate, endDate: date, startCondition: asset.condition, endCondition: condition, startConditionDetails: asset.conditionDetails, endConditionDetails: conditionDetails, comment}); + AssetAssignmentHistory.insert({assetKey: asset._id, assetId, serial: asset.serial, assetTypeName: (assetType ? assetType.name : "UNK"), assigneeType: asset.assigneeType, assigneeId: asset.assigneeId, startDate: asset.assignmentDate, endDate: date, startCondition: asset.condition, endCondition: condition, startConditionDetails: asset.conditionDetails, endConditionDetails: conditionDetails, comment, unassignedBy: Meteor.userId(), assignedBy: asset.assignedBy}); } catch (e) { console.error(e); } - Assets.update({assetId}, {$unset: {assigneeType: "", assigneeId: "", assignmentDate: ""}, $set: {condition, conditionDetails}}); + Assets.update({assetId}, {$unset: {assigneeType: "", assigneeId: "", assignmentDate: "", assignedBy: ""}, $set: {condition, conditionDetails}}); } else { console.error("Could not find the asset: " + assetId); diff --git a/imports/api/data-collection.js b/imports/api/data-collection.js index 04031ce..bb536b7 100644 --- a/imports/api/data-collection.js +++ b/imports/api/data-collection.js @@ -72,7 +72,7 @@ if (Meteor.isServer) { else if(params.studentId) { const student = Students.findOne({_id: params.studentId}) - console.log(student) + // console.log(student) if(student) query.email = student.email; else query = undefined } diff --git a/imports/ui/pages/Assignments/ByAsset.jsx b/imports/ui/pages/Assignments/ByAsset.jsx index f107a67..e186051 100644 --- a/imports/ui/pages/Assignments/ByAsset.jsx +++ b/imports/ui/pages/Assignments/ByAsset.jsx @@ -24,7 +24,12 @@ import {Assets, conditions} from "/imports/api/assets"; import {AssetTypes} from "/imports/api/asset-types"; import {Students} from "/imports/api/students"; import {Staff} from "/imports/api/staff"; -import {Link} from "react-router-dom"; +import {Link, useLocation, useNavigate, useNavigationType} from "react-router-dom"; +import {Action as NavigationType} from "@remix-run/router/history"; +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', @@ -37,6 +42,10 @@ const cssEditorField = { } const AssignmentsByAsset = () => { + const navigate = useNavigate() + const navigateType = useNavigationType() + const location = useLocation() + const state = location.state const theme = useTheme(); const [assetId, setAssetId] = useState("") @@ -48,8 +57,7 @@ const AssignmentsByAsset = () => { const [unassignConditionDetails, setUnassignConditionDetails] = useState("") const [assetIdInput, setAssetIdInput] = useState(undefined) - - + const {foundAsset} = useTracker(() => { let foundAsset = null; @@ -65,12 +73,72 @@ const AssignmentsByAsset = () => { } return {foundAsset} - }); + }, [assetId]); + + // Set a timer function to create history for the browser if the user pauses on an asset long enough. + useEffect(() => { + let clearTimer; + + // Only setup the timer to update navigation if we have found an asset for the current text input, and that asset is not the same as the one already current in the browser history. + if(foundAsset && (!state || state.assetId !== foundAsset.assetId)) { + const prevFoundAssetId = foundAsset.assetId + + // If the asset id doesn't change in 3 seconds then add this asset to the browser history so the back functionality works. + const timer = setTimeout(() => { + if(foundAsset && foundAsset.assetId === prevFoundAssetId) navigate("/assignments/byAsset", {replace: false, state: {assetId: foundAsset.assetId}}); + }, 3000) + + clearTimer = () => clearTimeout(timer) + } + + return clearTimer + }, [foundAsset]) + + const [usageData, setUsageData] = useState([]) + const [assignmentData, setAssignmentData] = useState([]) - //This works too well. The field always gets focus anytime anything is typed anywhere. - // useEffect(() => { - // if(assetIdInput) assetIdInput.focus() - // }) + // Collect the usage and assignment data when the selected person changes. + useEffect(() => { + try { + if(foundAsset) { + let query = {assetId: foundAsset.assetId} + + console.log("Requesting asset historical data") + console.log(query) + + Meteor.call('DataCollection.chromebookData', query, (err, result) => { + if (err) console.error(err) + else setUsageData(result) + }) + Meteor.call('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 ByAsset.jsx: " + e)} + }, [foundAsset]) + + // Restore the state if the forward/back/refresh functionality of the browser was utilized. + useEffect(() => { + console.log("useEffect - navigation") + if(!state) { + console.log("no state") + navigate("/assignments/byAsset", {replace: true, state: {asset: null}}) + } + else { + console.log(navigateType) + console.log(state) + if(navigateType === "POP" || navigateType === 'REPLACE' || navigateType === "PUSH") { + setAssetId(state.assetId ? state.assetId : "") + } + } + }, [state]) + + //Set focus on initial rendering. + useEffect(() => { + if(assetIdInput) assetIdInput.focus() + }, [assetIdInput]) const unassign = (editConditionOnly) => { // Open the dialog to get condition and comment. @@ -100,6 +168,82 @@ const AssignmentsByAsset = () => { } } + const [tab, setTab] = useState('assignments') + + const RenderUsage = ({data}) => { + return ( + <> + + + ) + } + + const RenderAssignments = ({data}) => { + return ( + <> + + + ) + } + return ( <> @@ -122,24 +266,44 @@ const AssignmentsByAsset = () => { - setAssetIdInput(input)} value={assetId} onChange={(e) => {setAssetId(e.target.value.toUpperCase())}}/> + setAssetIdInput(input)} value={assetId} onChange={(e) => {setAssetId(e.target.value.toUpperCase())}}/> + {foundAsset && ( +
+

Asset ID: {foundAsset.assetId}

+

Serial: {foundAsset.serial}

+

Current Condition: {foundAsset.condition}

+ + + setTab(v)}> + + + + + + +
+
Condition Details: {foundAsset.conditionDetails}
+ {foundAsset.assignee && ( + <> +
Assigned on: {new Date(foundAsset.assignmentDate).toLocaleDateString("en-US")} ({Math.ceil((new Date().getTime() - foundAsset.assignmentDate) / (1000*60*60*24))} days)
+
Assigned to: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName} {foundAsset.assignee.grade && foundAsset.assignee.grade} ({foundAsset.assignee.email})
+ + {" "} + + + )} +
+
+ + + + + + +
+
+ )}
- {foundAsset && ( -
-
Serial: {foundAsset.serial}
-
Condition: {foundAsset.condition}
-
Condition Details: {foundAsset.conditionDetails}
- {foundAsset.assignee && ( - <> -
Assigned on: {foundAsset.assignmentDate.toString()}
-
Assigned to: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName} {foundAsset.assignee.grade && foundAsset.assignee.grade} ({foundAsset.assignee.email})
- - {" "} - - - )} -
- )} ) } diff --git a/imports/ui/pages/Assignments/ByPerson.jsx b/imports/ui/pages/Assignments/ByPerson.jsx index 0e71b07..af038b0 100644 --- a/imports/ui/pages/Assignments/ByPerson.jsx +++ b/imports/ui/pages/Assignments/ByPerson.jsx @@ -22,6 +22,10 @@ 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', @@ -56,7 +60,7 @@ const AssignmentsByPerson = () => { const [unassignConditionDetails, setUnassignConditionDetails] = useState("") const [unassignAsset, setUnassignAsset] = useState(undefined) - const [assetIdInput, setAssetIdInput] = useState(undefined) + const [searchInput, setSearchInput] = useState(undefined) const {people} = useTracker(() => { let people = []; @@ -86,7 +90,7 @@ const AssignmentsByPerson = () => { } return {people} - }); + }, [search]); const {assets} = useTracker(() => { let assets = []; @@ -100,7 +104,7 @@ const AssignmentsByPerson = () => { } return {assets} - }); + }, [selectedPerson]); const {foundAsset} = useTracker(() => { let foundAsset = null; @@ -117,7 +121,32 @@ const AssignmentsByPerson = () => { } return {foundAsset} - }); + }, [assetId]); + + const [usageData, setUsageData] = useState([]) + const [assignmentData, setAssignmentData] = useState([]) + + // Collect the usage and assignment data when the selected person changes. + useEffect(() => { + try { + if(selectedPerson) { + let query = selectedPerson.type === "Student" ? {studentId: selectedPerson._id} : {staffId: selectedPerson._id} + + console.log("Collecting person history") + console.log(query) + + Meteor.call('DataCollection.chromebookData', query, (err, result) => { + if (err) console.error(err) + else setUsageData(result) + }) + Meteor.call('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 { @@ -150,10 +179,10 @@ const AssignmentsByPerson = () => { } } - //This works too well. The field always gets focus anytime anything is typed anywhere. - // useEffect(() => { - // if(assetIdInput) 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. @@ -193,19 +222,93 @@ const AssignmentsByPerson = () => { // '&:nthChild(even)': {backgroundColor: '#935e5e'} } + // Changes the selected person and updates the browser history. const changeSelectedPerson = (person) => { setSelectedPerson(person) - navigate("/assignments", {replace: false, state: {person, search}}); + 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 === NavigationType.Pop) { + if(navigateType === "POP" || navigateType === 'REPLACE' || navigateType === "PUSH") { setSearch(state.search) setSelectedPerson(state.person) } } - }) + }, [state, navigateType]) + + const [tab, setTab] = useState('assignments') + + const RenderUsage = ({data}) => { + return ( + <> + + + ) + } + + const RenderAssignments = ({data}) => { + return ( + <> + + + ) + } return ( <> @@ -253,7 +356,7 @@ const AssignmentsByPerson = () => { {/* Last Name*/} {/**/} {setIncludeInactive(e.target.checked)}}/>} label="Inactive"/> - {setSearch(e.target.value)}}/> + setSearchInput(input)} value={search} onChange={(e) => {setSearch(e.target.value)}}/>
@@ -271,44 +374,55 @@ const AssignmentsByPerson = () => { {selectedPerson && ( <>

{selectedPerson.firstName + " " + (selectedPerson.firstNameAlias ? "'" + selectedPerson.firstNameAlias + "' " : "") + selectedPerson.lastName + (selectedPerson.grade ? " (" + selectedPerson.grade + ")" : "")}

-
-
setAssetIdInput(input)} style={cssEditorField} variant="standard" label="Asset ID" value={assetId} onChange={(e) => {setAssetId(e.target.value.toUpperCase())}}/>
-
{foundAsset && foundAsset.assetType.name}
-
{foundAsset && {foundAsset.assetId}}
-
{foundAsset && {foundAsset.serial}}
- {foundAsset && foundAsset.assignee && ( -
Assigned To: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName}
- )} - -
+ + + setTab(v)}> + + + + + + +
+
{setAssetId(e.target.value.toUpperCase())}}/>
+ {foundAsset && ( + <> +
{foundAsset && foundAsset.assetType.name}
+
Asset ID: {foundAsset.assetId}
+
Serial: {foundAsset.serial}
+ + )} + {foundAsset && foundAsset.assignee && ( + <> +
Assigned To: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName}
+
Assigned: {new Date(foundAsset.assignmentDate).toLocaleDateString("en-US")} ({Math.ceil((new Date().getTime() - foundAsset.assignmentDate) / (1000*60*60*24))} days)
+ + )} + +
+ {assets.map((next, i) => { + return ( +
+
{next.assetType.name}
+
Asset ID: {next.assetId}
+
Serial: {next.serial}
+
Assigned: {new Date(next.assignmentDate).toLocaleDateString("en-US")} ({Math.ceil((new Date().getTime() - next.assignmentDate) / (1000*60*60*24))} days)
+ + {" "} + +
+ ) + })} +
+ + + + + + +
)} - {assets.map((next, i) => { - return ( -
-
{next.assetType.name}
-
{next.assetId}
-
{next.serial}
- - {" "} - -
- ) - })} - {/*
*/} - {/* {setAssetId(e.target.value)}}/>*/} - {/* {setSerial(e.target.value)}}/>*/} - {/*
*/} - {/*
*/} - {/* {setCondition(e.target.value)}}>*/} - {/* {conditions.map((condition, i) => {*/} - {/* return {condition}*/} - {/* })}*/} - {/* */} - {/*
*/} - {/*
*/} - {/* {setConditionDetails(e.target.value)}}/>*/} - {/*
*/}
diff --git a/package.json b/package.json index d0c5eb4..aa09f68 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "@lexical/link": "^0.5.0", "@lexical/react": "^0.5.0", "@mui/icons-material": "^5.11.16", + "@mui/lab": "^5.0.0-alpha.134", "@mui/material": "^5.13.4", + "@remix-run/router": "^1.6.3", "bcrypt": "^5.0.1", "classnames": "^2.2.6", "csv-parse": "^5.3.0",