Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -6,24 +6,25 @@
|
||||
|
||||
meteor-base@1.5.1 # Packages every Meteor app needs to have
|
||||
mobile-experience@1.1.0 # Packages for a great mobile UX
|
||||
mongo@1.15.0 # The database Meteor supports right now
|
||||
mongo@1.16.6 # The database Meteor supports right now
|
||||
jquery # Wrapper package for npm-installed jquery
|
||||
reactive-var@1.0.11 # Reactive variable for tracker
|
||||
reactive-var@1.0.12 # Reactive variable for tracker
|
||||
|
||||
standard-minifier-css@1.8.1 # CSS minifier run for production mode
|
||||
standard-minifier-js@2.8.0 # JS minifier run for production mode
|
||||
standard-minifier-css@1.9.2 # CSS minifier run for production mode
|
||||
standard-minifier-js@2.8.1 # JS minifier run for production mode
|
||||
es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers
|
||||
ecmascript@0.16.2 # Enable ECMAScript2015+ syntax in app code
|
||||
typescript # Enable TypeScript syntax in .ts and .tsx modules
|
||||
ecmascript@0.16.7 # Enable ECMAScript2015+ syntax in app code
|
||||
typescript@4.9.4 # Enable TypeScript syntax in .ts and .tsx modules
|
||||
shell-server@0.5.0 # Server-side component of the `meteor shell` command
|
||||
|
||||
static-html@1.3.2 # Define static page content in .html files
|
||||
react-meteor-data # React higher-order component for reactively tracking Meteor data
|
||||
accounts-ui@1.4.2
|
||||
accounts-password@2.3.1
|
||||
accounts-password@2.3.4
|
||||
accounts-google@1.4.0
|
||||
service-configuration@1.3.0
|
||||
service-configuration@1.3.1
|
||||
google-config-ui@1.0.3 # Adds the UI for logging in via Google
|
||||
alanning:roles # Adds roles to the user
|
||||
|
||||
msavin:mongol # Free version of MeteorToys - Provides access to the client side MongoDB for debugging. (Ctrl-M to activate :: https://atmospherejs.com/msavin/mongol)
|
||||
session
|
||||
|
||||
@@ -1 +1 @@
|
||||
METEOR@2.7.3
|
||||
METEOR@2.12
|
||||
|
||||
104
.meteor/versions
104
.meteor/versions
@@ -1,41 +1,41 @@
|
||||
accounts-base@2.2.4
|
||||
accounts-base@2.2.8
|
||||
accounts-google@1.4.0
|
||||
accounts-oauth@1.4.1
|
||||
accounts-password@2.3.1
|
||||
accounts-oauth@1.4.2
|
||||
accounts-password@2.3.4
|
||||
accounts-ui@1.4.2
|
||||
accounts-ui-unstyled@1.7.0
|
||||
alanning:roles@3.4.0
|
||||
allow-deny@1.1.1
|
||||
autoupdate@1.8.0
|
||||
babel-compiler@7.9.2
|
||||
babel-compiler@7.10.4
|
||||
babel-runtime@1.5.1
|
||||
base64@1.0.12
|
||||
binary-heap@1.0.11
|
||||
blaze@2.5.0
|
||||
blaze@2.6.2
|
||||
blaze-tools@1.1.3
|
||||
boilerplate-generator@1.7.1
|
||||
caching-compiler@1.2.2
|
||||
caching-html-compiler@1.2.1
|
||||
callback-hook@1.4.0
|
||||
check@1.3.1
|
||||
ddp@1.4.0
|
||||
ddp-client@2.5.0
|
||||
callback-hook@1.5.1
|
||||
check@1.3.2
|
||||
ddp@1.4.1
|
||||
ddp-client@2.6.1
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.1.0
|
||||
ddp-server@2.5.0
|
||||
diff-sequence@1.1.1
|
||||
dynamic-import@0.7.2
|
||||
ecmascript@0.16.2
|
||||
ecmascript-runtime@0.8.0
|
||||
ddp-rate-limiter@1.2.0
|
||||
ddp-server@2.6.1
|
||||
diff-sequence@1.1.2
|
||||
dynamic-import@0.7.3
|
||||
ecmascript@0.16.7
|
||||
ecmascript-runtime@0.8.1
|
||||
ecmascript-runtime-client@0.12.1
|
||||
ecmascript-runtime-server@0.11.0
|
||||
ejson@1.1.2
|
||||
email@2.2.1
|
||||
ejson@1.1.3
|
||||
email@2.2.5
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.1
|
||||
geojson-utils@1.0.10
|
||||
fetch@0.1.3
|
||||
geojson-utils@1.0.11
|
||||
google-config-ui@1.0.3
|
||||
google-oauth@1.4.2
|
||||
google-oauth@1.4.3
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.1.3
|
||||
htmljs@1.1.1
|
||||
@@ -43,57 +43,57 @@ id-map@1.1.1
|
||||
inter-process-messaging@0.1.1
|
||||
jquery@3.0.0
|
||||
launch-screen@1.3.0
|
||||
less@3.0.2
|
||||
less@4.0.0
|
||||
localstorage@1.2.0
|
||||
logging@1.3.1
|
||||
meteor@1.10.0
|
||||
logging@1.3.2
|
||||
meteor@1.11.2
|
||||
meteor-base@1.5.1
|
||||
meteortoys:toykit@10.0.0
|
||||
minifier-css@1.6.1
|
||||
minifier-css@1.6.4
|
||||
minifier-js@2.7.5
|
||||
minimongo@1.8.0
|
||||
minimongo@1.9.3
|
||||
mobile-experience@1.1.0
|
||||
mobile-status-bar@1.1.0
|
||||
modern-browsers@0.1.8
|
||||
modules@0.18.0
|
||||
modules-runtime@0.13.0
|
||||
mongo@1.15.0
|
||||
modern-browsers@0.1.9
|
||||
modules@0.19.0
|
||||
modules-runtime@0.13.1
|
||||
mongo@1.16.6
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
msavin:mongol@10.0.1
|
||||
npm-mongo@4.3.1
|
||||
oauth@2.1.2
|
||||
oauth2@1.3.1
|
||||
observe-sequence@1.0.20
|
||||
npm-mongo@4.16.0
|
||||
oauth@2.2.0
|
||||
oauth2@1.3.2
|
||||
observe-sequence@1.0.21
|
||||
ordered-dict@1.1.0
|
||||
promise@0.12.0
|
||||
random@1.2.0
|
||||
rate-limit@1.0.9
|
||||
react-fast-refresh@0.2.3
|
||||
react-meteor-data@2.5.1
|
||||
reactive-dict@1.3.0
|
||||
reactive-var@1.0.11
|
||||
promise@0.12.2
|
||||
random@1.2.1
|
||||
rate-limit@1.1.1
|
||||
react-fast-refresh@0.2.7
|
||||
react-meteor-data@2.7.2
|
||||
reactive-dict@1.3.1
|
||||
reactive-var@1.0.12
|
||||
reload@1.3.1
|
||||
retry@1.1.0
|
||||
routepolicy@1.1.1
|
||||
service-configuration@1.3.0
|
||||
session@1.2.0
|
||||
service-configuration@1.3.1
|
||||
session@1.2.1
|
||||
sha@1.0.9
|
||||
shell-server@0.5.0
|
||||
socket-stream-client@0.5.0
|
||||
spacebars@1.2.0
|
||||
socket-stream-client@0.5.1
|
||||
spacebars@1.3.0
|
||||
spacebars-compiler@1.3.1
|
||||
standard-minifier-css@1.8.2
|
||||
standard-minifier-css@1.9.2
|
||||
standard-minifier-js@2.8.1
|
||||
static-html@1.3.2
|
||||
templating@1.4.1
|
||||
templating@1.4.2
|
||||
templating-compiler@1.4.1
|
||||
templating-runtime@1.5.0
|
||||
templating-runtime@1.6.3
|
||||
templating-tools@1.2.2
|
||||
tracker@1.2.0
|
||||
typescript@4.5.4
|
||||
underscore@1.0.10
|
||||
tracker@1.3.2
|
||||
typescript@4.9.4
|
||||
underscore@1.0.13
|
||||
url@1.3.2
|
||||
webapp@1.13.1
|
||||
webapp-hashing@1.1.0
|
||||
webapp@1.13.5
|
||||
webapp-hashing@1.1.1
|
||||
|
||||
@@ -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(params.studentId) {
|
||||
query.assigneeId = params.studentId
|
||||
query.assigneeType = "Student"
|
||||
}
|
||||
else if(params.staffId) {
|
||||
query.assigneeId = params.staffId
|
||||
query.assigneeType = "Staff"
|
||||
}
|
||||
else {
|
||||
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.
|
||||
let result = AssetAssignmentHistory.find(query, {sort: {endDate: -1}}).fetch();
|
||||
let assets = [];
|
||||
result = AssetAssignmentHistory.find(query, {sort: {endDate: -1}}).fetch();
|
||||
|
||||
// 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]
|
||||
}
|
||||
else {
|
||||
// Find the assets assigned to the person.
|
||||
assets = Assets.find({assigneeId: params.studentId ? params.studentId : params.staffId}).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})
|
||||
}
|
||||
else if(next.assetId) {
|
||||
next.asset = Assets.findOne({assetId: next.assetId});
|
||||
}
|
||||
if(person) next.assignee = person
|
||||
else next.assignee = next.assigneeType === "Student" ? Students.findOne({_id: next.assigneeId}) : Staff.findOne({_id: next.assigneeId})
|
||||
|
||||
if(next.asset) {
|
||||
next.assetType = AssetTypes.findOne({_id: next.asset.assetTypeId})
|
||||
if(asset) {
|
||||
next.asset = asset
|
||||
next.assetType = assetType
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {AssetAssignmentHistory} from "/imports/api/asset-assignment-history";
|
||||
|
||||
export const Assets = new Mongo.Collection('assets');
|
||||
export const conditions = ['New','Like New','Good','Okay','Damaged', 'Missing', 'Decommissioned']
|
||||
export const functionalConditions = ['New','Like New','Good','Okay']
|
||||
|
||||
/*
|
||||
const AssetsSchema = new SimpleSchema({
|
||||
@@ -122,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.
|
||||
@@ -132,6 +134,30 @@ 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");
|
||||
},
|
||||
'assets.updateCondition'(_id, condition, conditionDetails) {
|
||||
console.log("updating condtition: " + condition + " / " + conditionDetails)
|
||||
check(_id, String)
|
||||
check(condition, String)
|
||||
if(conditionDetails) check(conditionDetails, String)
|
||||
|
||||
if(!conditions.includes(condition)) {
|
||||
//Should never happen.
|
||||
console.error("Invalid condition option in assets.update(..)");
|
||||
throw new Meteor.Error("Invalid condition option.");
|
||||
}
|
||||
|
||||
if(Roles.userIsInRole(Meteor.userId(), "laptop-management", {anyScope:true})) {
|
||||
console.log("updating .... ")
|
||||
Assets.update({_id}, {$set: {condition, conditionDetails}});
|
||||
}
|
||||
else throw new Meteor.Error("User Permission Error");
|
||||
},
|
||||
@@ -198,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 {
|
||||
@@ -238,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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@ if (Meteor.isServer) {
|
||||
});
|
||||
}
|
||||
Meteor.methods({
|
||||
'sites.update'(_id, name) {
|
||||
'sites.update'(_id, name, externalId) {
|
||||
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
|
||||
Sites.update({_id}, {$set: {name}});
|
||||
Sites.update({_id}, {$set: {name, externalId}});
|
||||
}
|
||||
},
|
||||
'sites.add'(name) {
|
||||
'sites.add'(name, externalId) {
|
||||
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
|
||||
Sites.insert({name});
|
||||
Sites.insert({name, externalId});
|
||||
}
|
||||
},
|
||||
'sites.remove'(_id) {
|
||||
|
||||
@@ -17,19 +17,23 @@ if (Meteor.isServer) {
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
'students.add'(id, firstName, lastName, email, siteId, grade) {
|
||||
'students.add'(id, firstName, firstNameAlias, lastName, email, siteId, grade, active) {
|
||||
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
|
||||
Students.insert({id, firstName, lastName, email, siteId, grade});
|
||||
Students.insert({id, firstName, firstNameAlias, lastName, email, siteId, grade, active, activeChangeTimestamp: active ? "" : new Date()});
|
||||
}
|
||||
},
|
||||
'students.update'(_id, id, firstName, lastName, email, siteId, grade) {
|
||||
'students.update'(_id, id, firstName, firstNameAlias, lastName, email, siteId, grade, active) {
|
||||
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
|
||||
Students.update({_id}, {$set: {id, firstName, lastName, email, siteId, grade}});
|
||||
Students.update({_id}, {$set: {id, firstName, firstNameAlias, lastName, email, siteId, grade, active, activeChangeTimestamp: active ? "" : new Date()}});
|
||||
}
|
||||
},
|
||||
'students.remove'(_id) {
|
||||
// Does not actually remove the student (not currently possible. Does set the student to not-active.
|
||||
// If we want to remove students we should allow it for non-active students if there are no assets assigned.
|
||||
// We may want to do this automatically, perhaps for students that have been non-active for a long period of time.
|
||||
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
|
||||
//TODO: Need to first verify there are no checked out assets to the staff member.
|
||||
// Set the student as non-active and set the timestamp for the change (so we know how long they have been inactive for - so we can potentially automatically remove them later.
|
||||
Students.update({_id}, {$set: {active: false, activeChangeTimestamp: new Date()}})
|
||||
}
|
||||
},
|
||||
'students.getPossibleGrades'() {
|
||||
@@ -56,95 +60,183 @@ if (Meteor.isServer) {
|
||||
* Expects the CSV string to contain comma delimited data in the form:
|
||||
* email, student ID, first name, last name, grade, first name alias, last name alias
|
||||
*
|
||||
* The query in Aeries is: `LIST STU ID SEM FN LN NG FNA`.
|
||||
* A more complete Aeries query: `LIST STU STU.ID STU.SEM STU.FN STU.LN STU.NG BY STU.NG STU.SEM IF STU.NG >= 7 AND NG <= 12 AND STU.NS = 5`
|
||||
* The query in Aeries is: `LIST STU NS ID SEM FN LN NG FNA IF NG <= 12`.
|
||||
* A more complete Aeries query (for grades 7-12 in school 5): `LIST STU STU.NS STU.ID STU.SEM STU.FN STU.LN STU.NG BY STU.NG STU.SEM IF STU.NG >= 7 AND NG <= 12 AND STU.NS = 5`
|
||||
* Note that FNA (First Name Alias) is optional.
|
||||
* Note that you might want to include a school ID in the IF if you have multiple schools in the district.
|
||||
* The query in SQL is: `SELECT [STU].[ID] AS [Student ID], [STU].[SEM] AS [StuEmail], STU.FN AS [First Name], STU.LN AS [Last Name], [STU].[GR] AS [Grade], [STU].[FNA] AS [First Name Alias], [STU].[LNA] AS [Last Name Alias] FROM (SELECT [STU].* FROM STU WHERE [STU].DEL = 0) STU WHERE ( [STU].SC = 5) ORDER BY [STU].[LN], [STU].[FN];`.
|
||||
* The query in SQL is: `SELECT [STU].[NS] AS [Next Schl], [STU].[ID] AS [Student ID], [STU].[SEM] AS [StuEmail], STU.FN AS [First Name], STU.LN AS [Last Name], [STU].[GR] AS [Grade], [STU].[FNA] AS [First Name Alias], [STU].[LNA] AS [Last Name Alias] FROM (SELECT [STU].* FROM STU WHERE [STU].DEL = 0) STU WHERE ( [STU].SC = 5) ORDER BY [STU].[LN], [STU].[FN];`.
|
||||
* Run the query in Aeries as a `Report`, select TXT, and upload here.
|
||||
*
|
||||
* Note: The headers for the CSV are not important and will be ignored. The order of the data is very important.
|
||||
*
|
||||
* Aeries adds a header per 'page' of data (I think 35 entries per page).
|
||||
* Example:
|
||||
* Anderson Valley Jr/Sr High School,6/11/2022
|
||||
* 2021-2022,Page 1
|
||||
* Student ID, Email, First Name,Last Name,Grade,(opt) First Name Alias
|
||||
* @type: Currently only supports 'CSV' or 'Aeries Text Report'
|
||||
* @type: Currently only supports 'csv' or 'aeries-txt'
|
||||
*
|
||||
* TODO: We are assuming that we are importing all active students from the external system. Any other assumption would require too much in the way of GUI
|
||||
* TODO: Import should have a site id column that is the external site id.
|
||||
* TODO: Each imported student should be attached to the correct site
|
||||
* TODO: Any students not imported should be marked as deactivated
|
||||
*/
|
||||
'students.loadCsv'(csv, type, siteId) {
|
||||
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
|
||||
check(csv, String);
|
||||
check(siteId, String);
|
||||
'students.loadCsv'(csv, type, test) {
|
||||
try {
|
||||
if (Roles.userIsInRole(Meteor.userId(), "admin", {anyScope: true})) {
|
||||
check(csv, String)
|
||||
|
||||
let site = Sites.findOne({_id: siteId});
|
||||
let sites = Sites.find().fetch()
|
||||
let sitesByExternalId = {}
|
||||
|
||||
if(site) {
|
||||
let cleanCsv;
|
||||
let lines = csv.split(/\r?\n/);
|
||||
let pageHeader = type === 'Aeries Text Report' ? lines[0] : null; // Skip the repeating header lines for an Aeries text report.
|
||||
let skip = type === 'CSV' ? 1 : 0; // Skip the first line of a CSV file (headers).
|
||||
// Map all sites by external ID so we can quickly find the site for each imported student.
|
||||
for (let site of sites)
|
||||
if (site.externalId)
|
||||
sitesByExternalId[site.externalId] = site
|
||||
|
||||
//Note: Only include active students since we don't want to repeatedly make students non-active (resetting the timestamp).
|
||||
let existingStudents = Students.find({active: true}).fetch()
|
||||
let existingStudentIds = new Set()
|
||||
|
||||
// Collect all pre-existing student ID's. Will remove them as we import, and use the remaining set to de-activate the students no longer in the district.
|
||||
for (let student of existingStudents)
|
||||
existingStudentIds.add(student.id)
|
||||
|
||||
let cleanCsv
|
||||
let lines = csv.split(/\r?\n/)
|
||||
let pageHeader = type === 'aeries-txt' ? lines[0] : null // Skip the repeating header lines for an Aeries text report.
|
||||
let skip = type === 'csv' ? 1 : 0 // Skip the first line of a CSV file (headers).
|
||||
|
||||
// Remove headers from the CSV.
|
||||
for(const line of lines) {
|
||||
if (skip > 0) skip--;
|
||||
for (const line of lines) {
|
||||
if (skip > 0) skip--
|
||||
else if (pageHeader && line === pageHeader) {
|
||||
skip = 2;
|
||||
skip = 2
|
||||
} else {
|
||||
if(!cleanCsv) cleanCsv = "";
|
||||
else cleanCsv += '\r\n';
|
||||
cleanCsv += line;
|
||||
if (!cleanCsv) cleanCsv = ""
|
||||
else cleanCsv += '\r\n'
|
||||
cleanCsv += line
|
||||
}
|
||||
}
|
||||
|
||||
const bound = Meteor.bindEnvironment((callback) => {callback();});
|
||||
const bound = Meteor.bindEnvironment((callback) => {
|
||||
callback();
|
||||
})
|
||||
|
||||
parse(cleanCsv, {}, function(err, records) {
|
||||
parse(cleanCsv, {}, function (err, records) {
|
||||
bound(() => {
|
||||
if(err) console.error(err);
|
||||
else {
|
||||
let foundIds = new Set();
|
||||
let duplicates = [];
|
||||
console.log("Found " + records.length + " records.");
|
||||
|
||||
for(const values of records) {
|
||||
let id = values[0];
|
||||
let email = values[1];
|
||||
let firstName = values[2];
|
||||
let lastName = values[3];
|
||||
let grade = parseInt(values[4], 10);
|
||||
let firstNameAlias = "";
|
||||
let active = true;
|
||||
|
||||
if(values.length > 5) firstNameAlias = values[5];
|
||||
|
||||
let student = {siteId, email, id, firstName, lastName, grade, firstNameAlias, active};
|
||||
|
||||
// Track the student ID's and record duplicates. This is used to ensure our counts are accurate later.
|
||||
if(foundIds.has(student.id)) {
|
||||
duplicates.push(student.id);
|
||||
}
|
||||
else {
|
||||
foundIds.add(student.id);
|
||||
}
|
||||
|
||||
try {
|
||||
Students.upsert({id: student.id}, {$set: student});
|
||||
}
|
||||
catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(duplicates.length + " records were duplicates:");
|
||||
console.log(duplicates);
|
||||
}
|
||||
});
|
||||
readCsv(err, records, sitesByExternalId, existingStudentIds, test)
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
console.log("Failed to find the site with the ID: " + siteId);
|
||||
}
|
||||
} catch(err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* Reads the CSV file containing Student data and updates and adds students to the system. Students not in the CSV are marked as non-active.
|
||||
* @param err
|
||||
* @param records
|
||||
* @param sitesByExternalId
|
||||
* @param existingStudentIds
|
||||
* @param test
|
||||
* @returns {string}
|
||||
*/
|
||||
const readCsv = (err, records, sitesByExternalId, existingStudentIds, test) => {
|
||||
let output = ""
|
||||
|
||||
if (err) console.error(err)
|
||||
else {
|
||||
let foundIds = new Set()
|
||||
let duplicates = []
|
||||
let count = 0
|
||||
let nonActiveCount = 0
|
||||
|
||||
if (test)
|
||||
output += "Found " + records.length + " records.\r\n"
|
||||
|
||||
try {
|
||||
for(const values of records) {
|
||||
let nextSchool = values[0]
|
||||
let siteId = sitesByExternalId[nextSchool] ? sitesByExternalId[nextSchool]._id : null
|
||||
let id = values[1]
|
||||
let email = values[2]
|
||||
let firstName = values[3]
|
||||
let lastName = values[4]
|
||||
let grade = parseInt(values[5], 10)
|
||||
let firstNameAlias = ""
|
||||
let active = true
|
||||
|
||||
if (values.length > 6) firstNameAlias = values[6];
|
||||
|
||||
// Ignore students at a site not in the system.
|
||||
if(siteId) {
|
||||
let student = { siteId, email, id, firstName, lastName, grade, firstNameAlias, active, activeChangeTimestamp: ""}
|
||||
|
||||
// Track the student ID's and record duplicates. This is used to ensure our counts are accurate later.
|
||||
// Note: We should never have duplicates in a perfect system, but in reality we do seem to end up with some duplicates in the SIS system's data.
|
||||
// There can be perfectly understandable reasons for this, so we will ignore them here since it shouldn't affect us.
|
||||
if (foundIds.has(student.id)) {
|
||||
duplicates.push(student.id)
|
||||
} else {
|
||||
foundIds.add(student.id)
|
||||
}
|
||||
|
||||
count++
|
||||
|
||||
if (!test) {
|
||||
try {
|
||||
existingStudentIds.delete(student.id)
|
||||
Students.upsert({id: student.id}, {$set: student})
|
||||
} catch (err) {
|
||||
console.log("Error while calling Students.upsert(..)")
|
||||
console.error(err)
|
||||
}
|
||||
} else {
|
||||
if (existingStudentIds.has(student.id)) {
|
||||
existingStudentIds.delete(student.id)
|
||||
output += "Updating existing student: " + student + "\r\n"
|
||||
} else output += "Adding student: " + student + "\r\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
console.log("Caught exception (while processing students imported via CSV): ")
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
// Change active status for all remaining students in the set (ones who were not in the import).
|
||||
for (let studentId of existingStudentIds) {
|
||||
nonActiveCount++
|
||||
|
||||
if (test) {
|
||||
output += "Changing active status for student: " + Students.findOne({id: studentId}) + "\r\n"
|
||||
} else {
|
||||
try {
|
||||
Students.update({id: studentId}, {
|
||||
$set: {
|
||||
active: false,
|
||||
activeChangeTimestamp: new Date()
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.log("Student ID: " + studentId)
|
||||
console.log("Error updating Student to be non-active:")
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(duplicates.length + " records were duplicates:")
|
||||
console.log(duplicates)
|
||||
console.log("")
|
||||
console.log("Added or updated " + count + " students.")
|
||||
console.log("Update " + nonActiveCount + " students to non-active status.")
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import {Roles} from 'meteor/alanning:roles';
|
||||
import React, { useState } from 'react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {createTheme, ThemeProvider} from '@mui/material/styles'
|
||||
import { useTracker } from 'meteor/react-meteor-data';
|
||||
import _ from 'lodash';
|
||||
|
||||
@@ -42,9 +42,9 @@ const AssetTypeEditor = ({value, close}) => {
|
||||
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);
|
||||
Meteor.call("assetTypes.update", value._id, name, description, year);
|
||||
else
|
||||
Meteor.call("assetType.add", name, description, year);
|
||||
Meteor.call("assetTypes.add", name, description, year);
|
||||
}
|
||||
const rejectChanges = () => {
|
||||
close()
|
||||
@@ -97,7 +97,7 @@ export default () => {
|
||||
maxHeight: '40rem',
|
||||
keyHandler: (e, selected) => {
|
||||
if(selected && selected._id && e.key === "Delete") {
|
||||
Meteor.call("assetType.remove", selected._id);
|
||||
Meteor.call("assetTypes.remove", selected._id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,14 +28,15 @@ const cssButtonContainer = {
|
||||
|
||||
const SiteEditor = ({value, close}) => {
|
||||
const [name, setName] = useState(value.name || "")
|
||||
const [externalId, setExternalId] = useState(value.externalId || "")
|
||||
|
||||
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);
|
||||
Meteor.call("sites.update", value._id, name, externalId);
|
||||
else
|
||||
Meteor.call("sites.add", name);
|
||||
Meteor.call("sites.add", name, externalId);
|
||||
}
|
||||
const rejectChanges = () => {
|
||||
close()
|
||||
@@ -45,6 +46,7 @@ const SiteEditor = ({value, close}) => {
|
||||
<div style={cssFieldContainer}>
|
||||
<h1>Site Editor</h1>
|
||||
<TextField style={cssEditorField} variant="standard" label="Name" value={name} onChange={(e) => {setName(e.target.value)}}/>
|
||||
<TextField style={cssEditorField} variant="standard" label="External ID" value={externalId} onChange={(e) => {setExternalId(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>
|
||||
@@ -68,6 +70,10 @@ export default () => {
|
||||
name: "Name",
|
||||
value: (row) => row.name,
|
||||
},
|
||||
{
|
||||
name: "External ID",
|
||||
value: (row) => row.externalId,
|
||||
},
|
||||
]
|
||||
|
||||
const options = {
|
||||
|
||||
@@ -9,6 +9,17 @@ import Select from '@mui/material/Select';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import {Students} from "/imports/api/students";
|
||||
import {Sites} from "/imports/api/sites";
|
||||
import Box from "@mui/material/Box";
|
||||
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
|
||||
import ToggleButton from "@mui/material/ToggleButton";
|
||||
import {InputLabel, List, ListItemButton, ListItemText, Switch} from "@mui/material";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
|
||||
const cssSitesSelect = {
|
||||
margin: '0.6rem 0',
|
||||
@@ -24,7 +35,7 @@ const cssFieldColumnContainer = {
|
||||
}
|
||||
const cssGridFieldContainer = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: "1fr 1fr 1fr",
|
||||
gridTemplateColumns: "1fr 1fr 1fr 1fr",
|
||||
columnGap: '1rem',
|
||||
rowGap: '0.4rem',
|
||||
marginBottom: '1.5rem'
|
||||
@@ -39,8 +50,10 @@ const StudentEditor = ({value, close, defaultSiteId}) => {
|
||||
const [email, setEmail] = useState(value.email || "")
|
||||
const [id, setId] = useState(value.id || "")
|
||||
const [firstName, setFirstName] = useState(value.firstName || "")
|
||||
const [firstNameAlias, setFirstNameAlias] = useState(value.firstNameAlias || "")
|
||||
const [lastName, setLastName] = useState(value.lastName || "")
|
||||
const [grade, setGrade] = useState(value.grade || "")
|
||||
const [active, setActive] = useState(value.active)
|
||||
const [siteId, setSiteId] = useState(value.siteId ? value.siteId : defaultSiteId)
|
||||
|
||||
const {sites} = useTracker(() => {
|
||||
@@ -57,9 +70,9 @@ const StudentEditor = ({value, close, defaultSiteId}) => {
|
||||
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);
|
||||
Meteor.call("students.update", value._id, id, firstName, firstNameAlias, lastName, email, siteId, grade, active);
|
||||
else
|
||||
Meteor.call("students.add", id, firstName, lastName, email, siteId, grade);
|
||||
Meteor.call("students.add", id, firstName, firstNameAlias, lastName, email, siteId, grade, active);
|
||||
}
|
||||
const rejectChanges = () => {
|
||||
close()
|
||||
@@ -72,7 +85,9 @@ const StudentEditor = ({value, close, defaultSiteId}) => {
|
||||
<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)}}/>
|
||||
<FormControlLabel control={<Switch variant="standard" checked={active} onChange={(e) => {setActive(e.target.checked)}}/>} label="Active"/>
|
||||
<TextField variant="standard" label="First Name" value={firstName} onChange={(e) => {setFirstName(e.target.value)}}/>
|
||||
<TextField variant="standard" label="Alias" value={firstNameAlias} onChange={(e) => {setFirstNameAlias(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) => {
|
||||
@@ -103,8 +118,23 @@ export default () => {
|
||||
return {sites}
|
||||
});
|
||||
|
||||
const ACTIVE_BOTH = "both"
|
||||
const ACTIVE_ONLY = "active"
|
||||
const ACTIVE_OFF = "inactive"
|
||||
const [active, setActive] = useState(ACTIVE_BOTH)
|
||||
const [nameSearch, setNameSearch] = useState("")
|
||||
|
||||
const {students} = useTracker(() => {
|
||||
const studentQuery = site === siteAll._id ? {} : {siteId: site}
|
||||
|
||||
if(active !== ACTIVE_BOTH) {
|
||||
studentQuery["active"] = active === ACTIVE_ONLY
|
||||
}
|
||||
|
||||
if(nameSearch && nameSearch.length > 2) {
|
||||
studentQuery["$or"] = [{firstName: {$regex: nameSearch, $options: 'i'}}, {firstNameAlias: {$regex: nameSearch, $options: 'i'}}, {lastName: {$regex: nameSearch, $options: 'i'}}]
|
||||
}
|
||||
|
||||
let students = Students.find(studentQuery).fetch();
|
||||
|
||||
return {students}
|
||||
@@ -132,6 +162,10 @@ export default () => {
|
||||
name: "First Name",
|
||||
value: (row) => row.firstName,
|
||||
},
|
||||
{
|
||||
name: "Alias",
|
||||
value: (row) => row.firstNameAlias,
|
||||
},
|
||||
{
|
||||
name: "Last Name",
|
||||
value: (row) => row.lastName,
|
||||
@@ -149,6 +183,10 @@ export default () => {
|
||||
else return 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Active",
|
||||
value: (row) => row.active ? "Active" : "Inactive",
|
||||
},
|
||||
]
|
||||
|
||||
const options = {
|
||||
@@ -163,13 +201,80 @@ export default () => {
|
||||
}
|
||||
}
|
||||
|
||||
const importData = (type) => {
|
||||
let input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.onchange = _ => {
|
||||
let files = Array.from(input.files)
|
||||
|
||||
if(files.length === 1) {
|
||||
let reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
Meteor.call("students.loadCsv", reader.result, type, testImportOnly, (err, result) => {
|
||||
//Note: It would be nice to have feedback about the operation, but right now I cannot figure out how to wait on the server for the result of the callback that is wrapped by a bindEnvironment call.
|
||||
if(err) console.log(err)
|
||||
// else if(testImportOnly) console.log(result)
|
||||
})
|
||||
}
|
||||
reader.readAsText(input.files[0])
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
const [showImportDialog, setShowImportDialog] = useState(false)
|
||||
const [testImportOnly, setTestImportOnly] = useState(false)
|
||||
|
||||
const openImportDialog = () => {
|
||||
setTestImportOnly(false)
|
||||
setShowImportDialog(true)
|
||||
}
|
||||
const closeImportDialog = (cause, importType) => {
|
||||
if(importType) importData(importType)
|
||||
|
||||
if(showImportDialog) setShowImportDialog(false)
|
||||
}
|
||||
|
||||
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>
|
||||
<Dialog open={showImportDialog}>
|
||||
<DialogTitle>Import</DialogTitle>
|
||||
<DialogContent style={{display: 'flex', flexDirection: 'column'}}>
|
||||
<p>Imports students for the entire district and deals with altering the "active" flag for students no longer in the district or who have graduated.</p>
|
||||
<h3>Middle of Year</h3>
|
||||
<pre>LIST STU SC ID SEM FN LN GR FNA</pre>
|
||||
<p>Run any time during the year to capture changes in student body.</p>
|
||||
<h3>End of Year</h3>
|
||||
<pre>LIST STU NS ID SEM FN LN NG FNA IF NG >= 12</pre>
|
||||
<p>Use this query for the import if the school year is over (before summer school). It utilizes the next school & next grade fields instead of the current grade/school.</p>
|
||||
<FormControlLabel control={<Checkbox checked={testImportOnly} onChange={(e) => setTestImportOnly(e.target.checked)}/>} label="Test Only"/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => closeImportDialog("button")}>Cancel</Button>
|
||||
<Button onClick={() => closeImportDialog("button", 'csv')}>Import CSV</Button>
|
||||
<Button onClick={() => closeImportDialog("button", 'aeries-txt')}>Import Aeries TXT</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Box component="div" sx={{m: 2, p: 2, border: '1px dashed grey'}}>
|
||||
<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>
|
||||
<Box component="div" sx={{display: "inline-block", marginLeft: "2rem"}}>
|
||||
<InputLabel htmlFor="activeGroup" sx={{fontSize: "0.8rem"}}>Active Students</InputLabel>
|
||||
<ToggleButtonGroup id="activeGroup" color="primary" value={active} exclusive onChange={(e)=>{setActive(e.target.value)}} aria-label="Active Students">
|
||||
<ToggleButton value={ACTIVE_BOTH}>All</ToggleButton>
|
||||
<ToggleButton value={ACTIVE_ONLY}>Active</ToggleButton>
|
||||
<ToggleButton value={ACTIVE_OFF}>Inactive</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
<TextField sx={{margin: "0.7rem 0 0 2rem"}} variant="standard" label="Name Search" value={nameSearch} onChange={(e) => {setNameSearch(e.target.value)}}/>
|
||||
<Box component="div" sx={{display: "inline-block", float: "right", marginTop: "1rem"}}>
|
||||
<Button variant="contained" color='secondary' className="button" onClick={openImportDialog}>Import</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<SimpleTable rows={students} columns={columns} options={options}/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -62,6 +62,7 @@ const AddAssets = ({assetTypes}) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>1: Select Asset Types To Add</div>
|
||||
<Box style={cssContainer}>
|
||||
<FormControl style={cssComponent}>
|
||||
<InputLabel id="selectAssetTypesLabel">Available Asset Types</InputLabel>
|
||||
@@ -81,6 +82,7 @@ const AddAssets = ({assetTypes}) => {
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<div>2: Select Asset Type and Enter Asset Data</div>
|
||||
<Box style={cssContainer}>
|
||||
<div style={{maxHeight: '26rem', overflowY:'auto', minWidth: '10rem', minHeight: '10rem'}}>
|
||||
<List>
|
||||
|
||||
@@ -65,13 +65,13 @@ const AssetEditor = ({value, close}) => {
|
||||
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} variant="standard" label="Asset ID" value={assetId} onChange={(e) => {setAssetId(e.target.value.toUpperCase())}}/>
|
||||
<TextField style={cssEditorField} variant="standard" label="Serial" value={serial} onChange={(e) => {setSerial(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}>
|
||||
|
||||
@@ -24,6 +24,15 @@ export default () => {
|
||||
path: '/byAsset',
|
||||
href: 'byAsset'
|
||||
},
|
||||
{
|
||||
title: "Report",
|
||||
getElement: () => {
|
||||
const AssignmentsReport = lazy(()=>import('./Assignments/Report'))
|
||||
return <AssignmentsReport/>
|
||||
},
|
||||
path: '/report',
|
||||
href: 'report'
|
||||
},
|
||||
]
|
||||
|
||||
return <TabNav tabs={tabs}/>
|
||||
|
||||
@@ -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,18 +42,22 @@ const cssEditorField = {
|
||||
}
|
||||
|
||||
const AssignmentsByAsset = () => {
|
||||
const navigate = useNavigate()
|
||||
const navigateType = useNavigationType()
|
||||
const location = useLocation()
|
||||
const state = location.state
|
||||
const theme = useTheme();
|
||||
const [assetId, setAssetId] = 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 [assetIdInput, setAssetIdInput] = useState(undefined)
|
||||
|
||||
|
||||
const {foundAsset} = useTracker(() => {
|
||||
let foundAsset = null;
|
||||
|
||||
@@ -64,15 +73,76 @@ const AssignmentsByAsset = () => {
|
||||
}
|
||||
|
||||
return {foundAsset}
|
||||
});
|
||||
}, [assetId]);
|
||||
|
||||
//This works too well. The field always gets focus anytime anything is typed anywhere.
|
||||
// useEffect(() => {
|
||||
// if(assetIdInput) assetIdInput.focus()
|
||||
// })
|
||||
// Set a timer function to create history for the browser if the user pauses on an asset long enough.
|
||||
useEffect(() => {
|
||||
let clearTimer;
|
||||
|
||||
const unassign = () => {
|
||||
// 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([])
|
||||
|
||||
// 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.
|
||||
setUnassignDialogEditConditionOnly(editConditionOnly)
|
||||
setUnassignComment("")
|
||||
setUnassignCondition(foundAsset.condition ? foundAsset.condition : conditions[2])
|
||||
setUnassignConditionDetails(foundAsset.conditionDetails || "")
|
||||
@@ -82,18 +152,102 @@ const AssignmentsByAsset = () => {
|
||||
setOpenUnassignDialog(false)
|
||||
|
||||
if(unassign === true) {
|
||||
// Call assets.unassign(assetId, comment, condition, conditionDetails, date)
|
||||
Meteor.call('assets.unassign', foundAsset.assetId, unassignComment, unassignCondition, unassignConditionDetails, (err, result) => {
|
||||
if(err) console.error(err)
|
||||
else if(assetIdInput) assetIdInput.focus()
|
||||
})
|
||||
if(unassignDialogEditConditionOnly) {
|
||||
Meteor.call('assets.updateCondition', foundAsset._id, unassignCondition, unassignConditionDetails, (err, result) => {
|
||||
if(err) console.error(err)
|
||||
else if(assetIdInput) assetIdInput.focus()
|
||||
})
|
||||
}
|
||||
else {
|
||||
// Call assets.unassign(assetId, comment, condition, conditionDetails, date)
|
||||
Meteor.call('assets.unassign', foundAsset.assetId, unassignComment, unassignCondition, unassignConditionDetails, (err, result) => {
|
||||
if (err) console.error(err)
|
||||
else if (assetIdInput) assetIdInput.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [tab, setTab] = useState('assignments')
|
||||
|
||||
const RenderUsage = ({data}) => {
|
||||
return (
|
||||
<>
|
||||
<ul>
|
||||
{data.map((next, i) => (
|
||||
<li key={next._id}>
|
||||
{next.person && (
|
||||
<>
|
||||
User: <Link to={"/assignments/byPerson"} state={{search: next.person.lastName, person: next.person}}>{next.person.firstName} {next.person.lastName} {next.person.grade ? "~ " + next.person.grade : ""} ({next.email})</Link><br/>
|
||||
</>
|
||||
)}
|
||||
{!next.person && (
|
||||
<>
|
||||
User: N/A<br/>
|
||||
</>
|
||||
)}
|
||||
{/*Device ID: <Link to={"/search?deviceId=" + encodeURIComponent(next.deviceId)}>{next.deviceId}</Link><br/>*/}
|
||||
{/*{next.asset && (*/}
|
||||
{/* <>Asset ID: <Link to={"/search?assetId=" + encodeURIComponent(next.asset.assetId)}>{next.asset.assetId}</Link><br/></>*/}
|
||||
{/*)}*/}
|
||||
{/*<>Asset Type: {next.assetType ? next.assetType.name : "Unknown"}<br/></>*/}
|
||||
{/*Serial: <Link to={"/search?serial=" + encodeURIComponent(next.serial)}>{next.serial}</Link><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: <Link to={"/assignments/byPerson"} state={{search: next.assignee.lastName, person: next.assignee}}>{next.assignee.firstName} {next.assignee.lastName} {next.assignee.grade ? "~ " + next.assignee.grade : ""} ({next.assignee.email})</Link><br/>
|
||||
</>
|
||||
)}
|
||||
{/*{next.asset && (*/}
|
||||
{/* <>*/}
|
||||
{/* Asset ID: <Link to={"/search?assetId=" + encodeURIComponent(next.asset.assetId)}>{next.asset.assetId}</Link><br/>*/}
|
||||
{/* </>*/}
|
||||
{/*)}*/}
|
||||
{/*{next.assetType && (*/}
|
||||
{/* <>*/}
|
||||
{/* Asset Type: {next.assetType.name}<br/>*/}
|
||||
{/* </>*/}
|
||||
{/*)}*/}
|
||||
{/*Serial: <Link to={"/search?serial=" + encodeURIComponent(next.serial)}>{next.serial}</Link><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={openUnassignDialog} onClose={unassignDialogClosed}>
|
||||
<DialogTitle>Unassign Asset</DialogTitle>
|
||||
<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)}}>
|
||||
@@ -102,32 +256,54 @@ const AssignmentsByAsset = () => {
|
||||
})}
|
||||
</TextField>
|
||||
</div>
|
||||
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} variant="standard" label="Comment" value={unassignComment} onChange={(e) => {setUnassignComment(e.target.value)}}/>
|
||||
{!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)}>Unassign</Button>
|
||||
<Button onClick={() => unassignDialogClosed(true)}>{unassignDialogEditConditionOnly ? "Save" : "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.toUpperCase())}}/>
|
||||
<TextField style={{...cssEditorField, maxWidth: "40rem", minWidth: "10rem"}} variant="standard" label="Asset ID" inputRef={input=>setAssetIdInput(input)} value={assetId} onChange={(e) => {setAssetId(e.target.value.toUpperCase())}}/>
|
||||
{foundAsset && (
|
||||
<div>
|
||||
<h3 style={{margin: "0 0 0.5rem 0"}}>Asset ID: {foundAsset.assetId}</h3>
|
||||
<h3 style={{margin: "0 0 0.5rem 0"}}>Serial: {foundAsset.serial}</h3>
|
||||
<h3 style={{margin: "0 0 0.5rem 0"}}>Current Condition: {foundAsset.condition}</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>
|
||||
<div>Condition Details: {foundAsset.conditionDetails}</div>
|
||||
{foundAsset.assignee && (
|
||||
<>
|
||||
<div>Assigned on: {new Date(foundAsset.assignmentDate).toLocaleDateString("en-US")} ({Math.ceil((new Date().getTime() - foundAsset.assignmentDate) / (1000*60*60*24))} days)</div>
|
||||
<div>Assigned to: <Link to={"/assignments/byPerson"} state={{search: foundAsset.assignee.lastName, person: foundAsset.assignee}}>{foundAsset.assignee.firstName} {foundAsset.assignee.lastName} {foundAsset.assignee.grade && foundAsset.assignee.grade} ({foundAsset.assignee.email})</Link></div>
|
||||
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign(false)}>Unassign</Button>
|
||||
{" "}
|
||||
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign(true)}>Edit</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel value="assignmentHistory">
|
||||
<RenderAssignments data={assignmentData}/>
|
||||
</TabPanel>
|
||||
<TabPanel value="usageHistory">
|
||||
<RenderUsage data={usageData}/>
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</div>
|
||||
)}
|
||||
</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} (<Link to={"/search?email=" + encodeURIComponent(foundAsset.assignee.email)}>{foundAsset.assignee.email}</Link>)</div>
|
||||
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign()}>Unassign</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import _ from 'lodash';
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import {InputLabel, List, ListItem, ListItemButton, ListItemText} from "@mui/material";
|
||||
import {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';
|
||||
@@ -19,7 +19,13 @@ 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";
|
||||
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',
|
||||
@@ -32,10 +38,15 @@ const cssEditorField = {
|
||||
}
|
||||
|
||||
const AssignmentsByPerson = () => {
|
||||
const theme = useTheme();
|
||||
const [searchType, setSearchType] = useState("Email")
|
||||
const [search, setSearch] = useState("")
|
||||
const [selectedPerson, setSelectedPerson] = useState("")
|
||||
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])
|
||||
@@ -43,12 +54,13 @@ const AssignmentsByPerson = () => {
|
||||
|
||||
//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 [assetIdInput, setAssetIdInput] = useState(undefined)
|
||||
const [searchInput, setSearchInput] = useState(undefined)
|
||||
|
||||
const {people} = useTracker(() => {
|
||||
let people = [];
|
||||
@@ -56,13 +68,17 @@ const AssignmentsByPerson = () => {
|
||||
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'}}
|
||||
}
|
||||
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 = Students.find(query).fetch();
|
||||
const staff = Staff.find(query).fetch();
|
||||
@@ -74,7 +90,7 @@ const AssignmentsByPerson = () => {
|
||||
}
|
||||
|
||||
return {people}
|
||||
});
|
||||
}, [search]);
|
||||
|
||||
const {assets} = useTracker(() => {
|
||||
let assets = [];
|
||||
@@ -88,7 +104,7 @@ const AssignmentsByPerson = () => {
|
||||
}
|
||||
|
||||
return {assets}
|
||||
});
|
||||
}, [selectedPerson]);
|
||||
|
||||
const {foundAsset} = useTracker(() => {
|
||||
let foundAsset = null;
|
||||
@@ -105,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 {
|
||||
@@ -115,8 +156,8 @@ const AssignmentsByPerson = () => {
|
||||
const assign = () => {
|
||||
if(foundAsset) {
|
||||
//Open the dialog to get condition.
|
||||
setUnassignCondition(foundAsset.condition ? foundAsset.condition : conditions[2])
|
||||
setUnassignConditionDetails(foundAsset.conditionDetails || "")
|
||||
setAssignCondition(foundAsset.condition ? foundAsset.condition : conditions[2])
|
||||
setAssignConditionDetails(foundAsset.conditionDetails || "")
|
||||
setOpenAssignDialog(true)
|
||||
}
|
||||
}
|
||||
@@ -138,13 +179,14 @@ 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) => {
|
||||
const unassign = (asset, editConditionOnly) => {
|
||||
// Open the dialog to get condition and comment.
|
||||
setUnassignDialogEditConditionOnly(editConditionOnly)
|
||||
setUnassignAsset(asset);
|
||||
setUnassignComment("")
|
||||
setUnassignCondition(asset.condition ? asset.condition : conditions[2])
|
||||
@@ -155,11 +197,19 @@ const AssignmentsByPerson = () => {
|
||||
setOpenUnassignDialog(false)
|
||||
|
||||
if(unassign === true) {
|
||||
// Call assets.unassign(assetId, comment, condition, conditionDetails, date)
|
||||
Meteor.call('assets.unassign', unassignAsset.assetId, unassignComment, unassignCondition, unassignConditionDetails, (err, result) => {
|
||||
if(err) console.error(err)
|
||||
else if(assetIdInput) assetIdInput.focus()
|
||||
})
|
||||
if(unassignDialogEditConditionOnly) {
|
||||
Meteor.call('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)
|
||||
Meteor.call('assets.unassign', unassignAsset.assetId, unassignComment, unassignCondition, unassignConditionDetails, (err, result) => {
|
||||
if(err) console.error(err)
|
||||
else if(assetIdInput) assetIdInput.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +222,94 @@ const AssignmentsByPerson = () => {
|
||||
// '&: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}>
|
||||
@@ -193,7 +331,7 @@ const AssignmentsByPerson = () => {
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={openUnassignDialog} onClose={unassignDialogClosed}>
|
||||
<DialogTitle>Unassign Asset</DialogTitle>
|
||||
<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)}}>
|
||||
@@ -202,72 +340,89 @@ const AssignmentsByPerson = () => {
|
||||
})}
|
||||
</TextField>
|
||||
</div>
|
||||
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} variant="standard" label="Comment" value={unassignComment} onChange={(e) => {setUnassignComment(e.target.value)}}/>
|
||||
{!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)}>Unassign</Button>
|
||||
<Button onClick={() => unassignDialogClosed(true)}>{unassignDialogEditConditionOnly ? "Save" : "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 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}>
|
||||
<div style={{maxHeight: '26rem', overflowY:'auto', minWidth: '10rem', minHeight: '10rem'}}>
|
||||
<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) => {setSelectedPerson(next)}}>
|
||||
<ListItemText primary={next.firstName + " " + next.lastName} secondary={next.email}/>
|
||||
<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: '1rem 0 0 .5rem'}}>
|
||||
<div style={{display: 'flex', flexDirection: 'column', margin: '0 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.toUpperCase())}}/></div>
|
||||
<div>{foundAsset && foundAsset.assetType.name}</div>
|
||||
<div>{foundAsset && <Link to={"/search?assetId=" + encodeURIComponent(foundAsset.assetId)}>{foundAsset.assetId}</Link>}</div>
|
||||
<div>{foundAsset && <Link to={"/search?serial=" + encodeURIComponent(foundAsset.serial)}>{foundAsset.serial}</Link>}</div>
|
||||
{foundAsset && foundAsset.assignee && (
|
||||
<div>Assigned To: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName}</div>
|
||||
)}
|
||||
<Button variant="contained" color='primary' className="button" disabled={!foundAsset || foundAsset.assignee !== undefined} onClick={()=>assign()}>Assign</Button>
|
||||
</div>
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{assets.map((next, i) => {
|
||||
return (
|
||||
<div key={next._id} style={{...getAssetTileStyles(i), ...cssAssetTile}}>
|
||||
<div>{next.assetType.name}</div>
|
||||
<div><Link to={"/search?assetId=" + encodeURIComponent(next.assetId)}>{next.assetId}</Link></div>
|
||||
<div><Link to={"/search?serial=" + encodeURIComponent(next.serial)}>{next.serial}</Link></div>
|
||||
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign(next)}>Unassign</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{/*<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>
|
||||
</>
|
||||
|
||||
168
imports/ui/pages/Assignments/Report.jsx
Normal file
168
imports/ui/pages/Assignments/Report.jsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import {Meteor} from "meteor/meteor";
|
||||
import React from "react";
|
||||
import {useTheme} from "@mui/material/styles";
|
||||
import {useTracker} from "meteor/react-meteor-data";
|
||||
import {AssetTypes} from "/imports/api/asset-types";
|
||||
import {Assets} from "/imports/api/assets";
|
||||
import {Students} from "/imports/api/students";
|
||||
import {Staff} from "/imports/api/staff";
|
||||
import SimpleTable from "/imports/ui/util/SimpleTable";
|
||||
import moment from "moment";
|
||||
import {Box, Grid} from "@mui/material";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
const AssignmentsReport = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
const {people} = useTracker(() => {
|
||||
let assets = Assets.find({assigneeId: {$exists: true}, assigneeType: "Student"}).fetch()
|
||||
let assetTypes = AssetTypes.find().fetch()
|
||||
let students = Students.find().fetch()
|
||||
let people = []
|
||||
|
||||
let assetTypesById = assetTypes.reduce((map, obj) => {map[obj._id] = obj; return map}, {})
|
||||
let studentsById = students.reduce((map, obj) => {map[obj._id] = obj; return map}, {})
|
||||
// let staffById = staff.reduce((map, obj) => {map[obj._id] = obj; return map}, {})
|
||||
|
||||
for(let next of assets) {
|
||||
let student = studentsById[next.assigneeId]
|
||||
let assetType = assetTypesById[next.assetTypeId]
|
||||
|
||||
people.push({_id: next._id, firstName: student.firstName, lastName: student.lastName, grade: student.grade, assetName: assetType.name, checkOutDate: next.assignmentDate, condition: next.condition, assetTag: next.assetId})
|
||||
}
|
||||
|
||||
people.sort((a, b) => {
|
||||
let firstName = a.firstName.localeCompare(b.firstName)
|
||||
let lastName = a.lastName.localeCompare(b.lastName)
|
||||
|
||||
return a.grade === b.grade ? lastName ? lastName : firstName : b.grade - a.grade
|
||||
})
|
||||
|
||||
return {people}
|
||||
});
|
||||
|
||||
const exportData = () => {
|
||||
let csv
|
||||
|
||||
for(let next of people) {
|
||||
if(csv) csv += "\r\n"
|
||||
else csv = "First Name;Last Name;Grade;Asset Name;Check Out Date;Condition;Asset Tag\r\n"
|
||||
|
||||
csv += next.firstName + ";" + next.lastName + ";" + next.grade + ";" + next.assetName + ";" + next.checkOutDate + ";" + next.condition + ";" + next.assetTag + ";"
|
||||
}
|
||||
|
||||
let blob = new Blob([csv], {type: 'text/csv'})
|
||||
let a = document.createElement('a')
|
||||
a.download = 'export.csv'
|
||||
a.href = window.URL.createObjectURL(blob)
|
||||
a.click()
|
||||
}
|
||||
|
||||
// const {checkedOutAssets} = useTracker(() => {
|
||||
// let assets = Assets.find({assigneeId: {$exists: true}}).fetch()
|
||||
// let assetTypes = AssetTypes.find().fetch()
|
||||
// let students = Students.find().fetch()
|
||||
// let staff = Staff.find().fetch()
|
||||
//
|
||||
// let assetTypesById = assetTypes.reduce((map, obj) => {map[obj._id] = obj; return map}, {})
|
||||
// let studentsById = students.reduce((map, obj) => {map[obj._id] = obj; return map}, {})
|
||||
// let staffById = staff.reduce((map, obj) => {map[obj._id] = obj; return map}, {})
|
||||
//
|
||||
// for(let next of assets) {
|
||||
// next.assetType = assetTypesById[next.assetTypeId]
|
||||
// if(next.assigneeType === "Staff")
|
||||
// next.person = staffById[next.assigneeId]
|
||||
// else
|
||||
// next.person = studentsById[next.assigneeId]
|
||||
// }
|
||||
//
|
||||
// let checkedOutAssets = [];
|
||||
//
|
||||
// for(let next of assets) {
|
||||
// if(next.assigneeType === "Student") {
|
||||
// checkedOutAssets.push(next);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return {checkedOutAssets}
|
||||
// });
|
||||
|
||||
// people.push({firstName: student.firstName, lastName: student.lastName, grade: student.grade, assetName: assetType.name, checkOutDate: next.assignmentDate, condition: next.condition, assetTag: next.assetTag})
|
||||
const columns = [
|
||||
{
|
||||
name: "First Name",
|
||||
//value: (checkedOutAsset) => checkedOutAsset.person ? checkedOutAsset.person.firstName : ""
|
||||
value: (next) => next.firstName
|
||||
},
|
||||
{
|
||||
name: "Last Name",
|
||||
// value: (checkedOutAsset) => checkedOutAsset.person ? checkedOutAsset.person.lastName : ""
|
||||
value: (next) => next.lastName
|
||||
},
|
||||
{
|
||||
name: "Grade",
|
||||
value: (next) => next.grade,
|
||||
},
|
||||
{
|
||||
name: "Asset",
|
||||
value: (next) => next.assetName,
|
||||
},
|
||||
{
|
||||
name: "Asset Tag",
|
||||
value: (next) => next.assetTag,
|
||||
},
|
||||
{
|
||||
name: "Checkout Date",
|
||||
value: (next) => moment(next.assignmentDate).format("YYYY-MM-DD"),
|
||||
},
|
||||
{
|
||||
name: "Condition",
|
||||
value: (next) => next.condition,
|
||||
},
|
||||
]
|
||||
|
||||
const options = {
|
||||
key: (row) => row._id,
|
||||
// editor: (row, close) => {return (<AssetTypeEditor value={row} close={close}/>)},
|
||||
add: false,
|
||||
maxHeight: '40rem',
|
||||
keyHandler: (e, selected) => {
|
||||
// if(selected && selected._id && e.key === "Delete") {
|
||||
// Meteor.call("assetTypes.remove", selected._id);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Box component="div" sx={{m: 2, p: 2, border: '1px dashed grey'}}>
|
||||
<Button variant="contained" color='secondary' className="button" onClick={()=>exportData()}>Export</Button>
|
||||
{/* <h4 style={{margin: 0, padding: 0}}>Filter</h4>*/}
|
||||
{/* <Grid container spacing={2}>*/}
|
||||
{/* <Grid item xs={4}>*/}
|
||||
{/* <TextField style={cssEditorField} select variant="standard" value={assetTypeId} onChange={(e)=>{setAssetTypeId(e.target.value)}} label="Grade">*/}
|
||||
{/* {assetTypes.map((assetType, i) => {*/}
|
||||
{/* return <MenuItem key={i} value={assetType._id}>{assetType.name}</MenuItem>*/}
|
||||
{/* })}*/}
|
||||
{/* </TextField>*/}
|
||||
{/* </Grid>*/}
|
||||
{/* </Grid>*/}
|
||||
</Box>
|
||||
<SimpleTable rows={people} columns={columns} options={options}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default () => {
|
||||
Meteor.subscribe('students');
|
||||
Meteor.subscribe('staff');
|
||||
Meteor.subscribe('assetTypes');
|
||||
Meteor.subscribe('assets');
|
||||
|
||||
return (
|
||||
<AssignmentsReport/>
|
||||
)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ 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 {Assets, conditions, functionalConditions} from "/imports/api/assets";
|
||||
import {AssetTypes} from "/imports/api/asset-types";
|
||||
import {Students} from "/imports/api/students";
|
||||
import {Staff} from "/imports/api/staff";
|
||||
@@ -34,6 +34,21 @@ const cssEditorField = {
|
||||
const Statistics = () => {
|
||||
const [selectedMissingAsset, setSelectedMissingAsset] = useState("")
|
||||
|
||||
const {assetStatistics} = useTracker(() => {
|
||||
let assetStatistics = []
|
||||
const assetTypes = AssetTypes.find({}, {year: 1}).fetch()
|
||||
|
||||
for(let type of assetTypes) {
|
||||
let count = Assets.find({assetTypeId: type._id, condition: {$in: functionalConditions}}).count()
|
||||
|
||||
if(count > 0) {
|
||||
assetStatistics.push({name: type.name, count})
|
||||
}
|
||||
}
|
||||
|
||||
return {assetStatistics}
|
||||
})
|
||||
|
||||
const {missingAssets} = useTracker(() => {
|
||||
let missingAssets = [];
|
||||
|
||||
@@ -54,16 +69,32 @@ const Statistics = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Missing Equipment</h1>
|
||||
<List>
|
||||
{missingAssets.map((next, i) => {
|
||||
return (
|
||||
<ListItemButton key={next._id} style={getListItemStyle(next)} selected={selectedMissingAsset === next} onClick={(e) => {setSelectedMissingAsset(next)}}>
|
||||
<ListItemText primary={next.assetId} secondary={next.assetType.name}/>
|
||||
</ListItemButton>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
<div style={{display: 'flex', flexFlow: 'row wrap', justifyContent: 'center', alignItems: 'flex-start', columnGap: '1rem'}}>
|
||||
<div>
|
||||
<h1>Equipment Counts</h1>
|
||||
<List sx={{overflow: "auto", maxHeight: "30rem", position: "relative", width: "30rem", border: "1px solid #999", marginBottom: "2rem"}}>
|
||||
{assetStatistics.map((next, i) => {
|
||||
return (
|
||||
<ListItem key={next.name} style={getListItemStyle(next)}>
|
||||
<ListItemText primary={next.name} secondary={next.count}/>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Missing Equipment</h1>
|
||||
<List sx={{overflow: "auto", maxHeight: "30rem", position: "relative", width: "30rem", border: "1px solid #999", marginBottom: "2rem"}}>
|
||||
{missingAssets.map((next, i) => {
|
||||
return (
|
||||
<ListItemButton key={next._id} style={getListItemStyle(next)} selected={selectedMissingAsset === next} onClick={(e) => {setSelectedMissingAsset(next)}}>
|
||||
<ListItemText primary={next.assetId} secondary={next.assetType.name}/>
|
||||
</ListItemButton>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
"@lexical/headless": "^0.5.0",
|
||||
"@lexical/link": "^0.5.0",
|
||||
"@lexical/react": "^0.5.0",
|
||||
"@mui/icons-material": "^5.10.2",
|
||||
"@mui/material": "^5.10.2",
|
||||
"@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",
|
||||
@@ -32,7 +34,7 @@
|
||||
"typescript": "^4.8.4",
|
||||
"umbrellajs": "^3.3.1",
|
||||
"underscore": "^1.13.2",
|
||||
"verbum": "^0.4.0",
|
||||
"verbum": "^0.5.0",
|
||||
"winston": "^3.7.2",
|
||||
"winston-daily-rotate-file": "^4.6.1",
|
||||
"ws": "^8.4.2"
|
||||
|
||||
@@ -4,7 +4,8 @@ import '../imports/api/';
|
||||
import './google-oauth.js';
|
||||
import '/imports/startup/accounts-config.js';
|
||||
import './logging';
|
||||
|
||||
//Currently all the websocket based reporting is done via the data collection system.
|
||||
//import './websocket'
|
||||
|
||||
|
||||
|
||||
|
||||
36
server/websocket.js
Normal file
36
server/websocket.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import WebSocket, {WebSocketServer} from 'ws'
|
||||
|
||||
try {
|
||||
const server = new WebSocketServer({
|
||||
port: 3001,
|
||||
perMessageDeflate: {
|
||||
zlibDeflateOptions: {
|
||||
// See zlib defaults.
|
||||
chunkSize: 1024,
|
||||
memLevel: 7,
|
||||
level: 3
|
||||
},
|
||||
zlibInflateOptions: {
|
||||
chunkSize: 10 * 1024
|
||||
},
|
||||
// Other options settable:
|
||||
clientNoContextTakeover: true, // Defaults to negotiated value.
|
||||
serverNoContextTakeover: true, // Defaults to negotiated value.
|
||||
serverMaxWindowBits: 10, // Defaults to negotiated value.
|
||||
// Below options specified as default values.
|
||||
concurrencyLimit: 10, // Limits zlib concurrency for perf.
|
||||
threshold: 1024 // Size (in bytes) below which messages
|
||||
// should not be compressed if context takeover is disabled.
|
||||
}
|
||||
})
|
||||
|
||||
server.on('connection', function connection(socket) {
|
||||
socket.on('message', function message(data) {
|
||||
console.log("received: %s", data)
|
||||
socket.send("pong")
|
||||
})
|
||||
})
|
||||
}
|
||||
catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
Reference in New Issue
Block a user