Updated meteor; Modified the assignments byPerson page considerably to improve the workflow; Added an external id to sites; Added an import for students; Improved the students page.
This commit is contained in:
@@ -6,14 +6,14 @@
|
|||||||
|
|
||||||
meteor-base@1.5.1 # Packages every Meteor app needs to have
|
meteor-base@1.5.1 # Packages every Meteor app needs to have
|
||||||
mobile-experience@1.1.0 # Packages for a great mobile UX
|
mobile-experience@1.1.0 # Packages for a great mobile UX
|
||||||
mongo@1.16.5 # The database Meteor supports right now
|
mongo@1.16.6 # The database Meteor supports right now
|
||||||
jquery # Wrapper package for npm-installed jquery
|
jquery # Wrapper package for npm-installed jquery
|
||||||
reactive-var@1.0.12 # Reactive variable for tracker
|
reactive-var@1.0.12 # Reactive variable for tracker
|
||||||
|
|
||||||
standard-minifier-css@1.9.0 # CSS 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
|
standard-minifier-js@2.8.1 # JS minifier run for production mode
|
||||||
es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers
|
es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers
|
||||||
ecmascript@0.16.6 # Enable ECMAScript2015+ syntax in app code
|
ecmascript@0.16.7 # Enable ECMAScript2015+ syntax in app code
|
||||||
typescript@4.9.4 # Enable TypeScript syntax in .ts and .tsx modules
|
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
|
shell-server@0.5.0 # Server-side component of the `meteor shell` command
|
||||||
|
|
||||||
@@ -27,3 +27,4 @@ google-config-ui@1.0.3 # Adds the UI for logging in via Google
|
|||||||
alanning:roles # Adds roles to the user
|
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)
|
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.11.0
|
METEOR@2.12
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
accounts-base@2.2.7
|
accounts-base@2.2.8
|
||||||
accounts-google@1.4.0
|
accounts-google@1.4.0
|
||||||
accounts-oauth@1.4.2
|
accounts-oauth@1.4.2
|
||||||
accounts-password@2.3.4
|
accounts-password@2.3.4
|
||||||
@@ -7,30 +7,30 @@ accounts-ui-unstyled@1.7.0
|
|||||||
alanning:roles@3.4.0
|
alanning:roles@3.4.0
|
||||||
allow-deny@1.1.1
|
allow-deny@1.1.1
|
||||||
autoupdate@1.8.0
|
autoupdate@1.8.0
|
||||||
babel-compiler@7.10.3
|
babel-compiler@7.10.4
|
||||||
babel-runtime@1.5.1
|
babel-runtime@1.5.1
|
||||||
base64@1.0.12
|
base64@1.0.12
|
||||||
binary-heap@1.0.11
|
binary-heap@1.0.11
|
||||||
blaze@2.6.1
|
blaze@2.6.2
|
||||||
blaze-tools@1.1.3
|
blaze-tools@1.1.3
|
||||||
boilerplate-generator@1.7.1
|
boilerplate-generator@1.7.1
|
||||||
caching-compiler@1.2.2
|
caching-compiler@1.2.2
|
||||||
caching-html-compiler@1.2.1
|
caching-html-compiler@1.2.1
|
||||||
callback-hook@1.5.0
|
callback-hook@1.5.1
|
||||||
check@1.3.2
|
check@1.3.2
|
||||||
ddp@1.4.1
|
ddp@1.4.1
|
||||||
ddp-client@2.6.1
|
ddp-client@2.6.1
|
||||||
ddp-common@1.4.0
|
ddp-common@1.4.0
|
||||||
ddp-rate-limiter@1.1.1
|
ddp-rate-limiter@1.2.0
|
||||||
ddp-server@2.6.0
|
ddp-server@2.6.1
|
||||||
diff-sequence@1.1.2
|
diff-sequence@1.1.2
|
||||||
dynamic-import@0.7.2
|
dynamic-import@0.7.3
|
||||||
ecmascript@0.16.6
|
ecmascript@0.16.7
|
||||||
ecmascript-runtime@0.8.0
|
ecmascript-runtime@0.8.1
|
||||||
ecmascript-runtime-client@0.12.1
|
ecmascript-runtime-client@0.12.1
|
||||||
ecmascript-runtime-server@0.11.0
|
ecmascript-runtime-server@0.11.0
|
||||||
ejson@1.1.3
|
ejson@1.1.3
|
||||||
email@2.2.4
|
email@2.2.5
|
||||||
es5-shim@4.8.0
|
es5-shim@4.8.0
|
||||||
fetch@0.1.3
|
fetch@0.1.3
|
||||||
geojson-utils@1.0.11
|
geojson-utils@1.0.11
|
||||||
@@ -46,31 +46,31 @@ launch-screen@1.3.0
|
|||||||
less@4.0.0
|
less@4.0.0
|
||||||
localstorage@1.2.0
|
localstorage@1.2.0
|
||||||
logging@1.3.2
|
logging@1.3.2
|
||||||
meteor@1.11.1
|
meteor@1.11.2
|
||||||
meteor-base@1.5.1
|
meteor-base@1.5.1
|
||||||
meteortoys:toykit@10.0.0
|
meteortoys:toykit@10.0.0
|
||||||
minifier-css@1.6.2
|
minifier-css@1.6.4
|
||||||
minifier-js@2.7.5
|
minifier-js@2.7.5
|
||||||
minimongo@1.9.2
|
minimongo@1.9.3
|
||||||
mobile-experience@1.1.0
|
mobile-experience@1.1.0
|
||||||
mobile-status-bar@1.1.0
|
mobile-status-bar@1.1.0
|
||||||
modern-browsers@0.1.9
|
modern-browsers@0.1.9
|
||||||
modules@0.19.0
|
modules@0.19.0
|
||||||
modules-runtime@0.13.1
|
modules-runtime@0.13.1
|
||||||
mongo@1.16.5
|
mongo@1.16.6
|
||||||
mongo-decimal@0.1.3
|
mongo-decimal@0.1.3
|
||||||
mongo-dev-server@1.1.0
|
mongo-dev-server@1.1.0
|
||||||
mongo-id@1.0.8
|
mongo-id@1.0.8
|
||||||
msavin:mongol@10.0.1
|
msavin:mongol@10.0.1
|
||||||
npm-mongo@4.14.0
|
npm-mongo@4.16.0
|
||||||
oauth@2.2.0
|
oauth@2.2.0
|
||||||
oauth2@1.3.2
|
oauth2@1.3.2
|
||||||
observe-sequence@1.0.20
|
observe-sequence@1.0.21
|
||||||
ordered-dict@1.1.0
|
ordered-dict@1.1.0
|
||||||
promise@0.12.2
|
promise@0.12.2
|
||||||
random@1.2.1
|
random@1.2.1
|
||||||
rate-limit@1.0.9
|
rate-limit@1.1.1
|
||||||
react-fast-refresh@0.2.6
|
react-fast-refresh@0.2.7
|
||||||
react-meteor-data@2.7.2
|
react-meteor-data@2.7.2
|
||||||
reactive-dict@1.3.1
|
reactive-dict@1.3.1
|
||||||
reactive-var@1.0.12
|
reactive-var@1.0.12
|
||||||
@@ -81,19 +81,19 @@ service-configuration@1.3.1
|
|||||||
session@1.2.1
|
session@1.2.1
|
||||||
sha@1.0.9
|
sha@1.0.9
|
||||||
shell-server@0.5.0
|
shell-server@0.5.0
|
||||||
socket-stream-client@0.5.0
|
socket-stream-client@0.5.1
|
||||||
spacebars@1.3.0
|
spacebars@1.3.0
|
||||||
spacebars-compiler@1.3.1
|
spacebars-compiler@1.3.1
|
||||||
standard-minifier-css@1.9.0
|
standard-minifier-css@1.9.2
|
||||||
standard-minifier-js@2.8.1
|
standard-minifier-js@2.8.1
|
||||||
static-html@1.3.2
|
static-html@1.3.2
|
||||||
templating@1.4.2
|
templating@1.4.2
|
||||||
templating-compiler@1.4.1
|
templating-compiler@1.4.1
|
||||||
templating-runtime@1.6.1
|
templating-runtime@1.6.3
|
||||||
templating-tools@1.2.2
|
templating-tools@1.2.2
|
||||||
tracker@1.3.1
|
tracker@1.3.2
|
||||||
typescript@4.9.4
|
typescript@4.9.4
|
||||||
underscore@1.0.12
|
underscore@1.0.13
|
||||||
url@1.3.2
|
url@1.3.2
|
||||||
webapp@1.13.4
|
webapp@1.13.5
|
||||||
webapp-hashing@1.1.1
|
webapp-hashing@1.1.1
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {AssetAssignmentHistory} from "/imports/api/asset-assignment-history";
|
|||||||
|
|
||||||
export const Assets = new Mongo.Collection('assets');
|
export const Assets = new Mongo.Collection('assets');
|
||||||
export const conditions = ['New','Like New','Good','Okay','Damaged', 'Missing', 'Decommissioned']
|
export const conditions = ['New','Like New','Good','Okay','Damaged', 'Missing', 'Decommissioned']
|
||||||
|
export const functionalConditions = ['New','Like New','Good','Okay']
|
||||||
|
|
||||||
/*
|
/*
|
||||||
const AssetsSchema = new SimpleSchema({
|
const AssetsSchema = new SimpleSchema({
|
||||||
@@ -135,6 +136,24 @@ Meteor.methods({
|
|||||||
}
|
}
|
||||||
else throw new Meteor.Error("User Permission Error");
|
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");
|
||||||
|
},
|
||||||
'assets.remove'(_id) {
|
'assets.remove'(_id) {
|
||||||
check(_id, String);
|
check(_id, String);
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ if (Meteor.isServer) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Meteor.methods({
|
Meteor.methods({
|
||||||
'sites.update'(_id, name) {
|
'sites.update'(_id, name, externalId) {
|
||||||
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
|
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})) {
|
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
|
||||||
Sites.insert({name});
|
Sites.insert({name, externalId});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'sites.remove'(_id) {
|
'sites.remove'(_id) {
|
||||||
|
|||||||
@@ -17,19 +17,23 @@ if (Meteor.isServer) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Meteor.methods({
|
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})) {
|
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})) {
|
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) {
|
'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})) {
|
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'() {
|
'students.getPossibleGrades'() {
|
||||||
@@ -56,95 +60,183 @@ if (Meteor.isServer) {
|
|||||||
* Expects the CSV string to contain comma delimited data in the form:
|
* 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
|
* 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`.
|
* The query in Aeries is: `LIST STU NS ID SEM FN LN NG FNA IF NG <= 12`.
|
||||||
* 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`
|
* 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 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.
|
* 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.
|
* 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).
|
* Aeries adds a header per 'page' of data (I think 35 entries per page).
|
||||||
* Example:
|
* Example:
|
||||||
* Anderson Valley Jr/Sr High School,6/11/2022
|
* Anderson Valley Jr/Sr High School,6/11/2022
|
||||||
* 2021-2022,Page 1
|
* 2021-2022,Page 1
|
||||||
* Student ID, Email, First Name,Last Name,Grade,(opt) First Name Alias
|
* 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) {
|
'students.loadCsv'(csv, type, test) {
|
||||||
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
|
try {
|
||||||
check(csv, String);
|
if (Roles.userIsInRole(Meteor.userId(), "admin", {anyScope: true})) {
|
||||||
check(siteId, String);
|
check(csv, String)
|
||||||
|
|
||||||
let site = Sites.findOne({_id: siteId});
|
let sites = Sites.find().fetch()
|
||||||
|
let sitesByExternalId = {}
|
||||||
|
|
||||||
if(site) {
|
// Map all sites by external ID so we can quickly find the site for each imported student.
|
||||||
let cleanCsv;
|
for (let site of sites)
|
||||||
let lines = csv.split(/\r?\n/);
|
if (site.externalId)
|
||||||
let pageHeader = type === 'Aeries Text Report' ? lines[0] : null; // Skip the repeating header lines for an Aeries text report.
|
sitesByExternalId[site.externalId] = site
|
||||||
let skip = type === 'CSV' ? 1 : 0; // Skip the first line of a CSV file (headers).
|
|
||||||
|
//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.
|
// Remove headers from the CSV.
|
||||||
for(const line of lines) {
|
for (const line of lines) {
|
||||||
if (skip > 0) skip--;
|
if (skip > 0) skip--
|
||||||
else if (pageHeader && line === pageHeader) {
|
else if (pageHeader && line === pageHeader) {
|
||||||
skip = 2;
|
skip = 2
|
||||||
} else {
|
} else {
|
||||||
if(!cleanCsv) cleanCsv = "";
|
if (!cleanCsv) cleanCsv = ""
|
||||||
else cleanCsv += '\r\n';
|
else cleanCsv += '\r\n'
|
||||||
cleanCsv += line;
|
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(() => {
|
bound(() => {
|
||||||
if(err) console.error(err);
|
readCsv(err, records, sitesByExternalId, existingStudentIds, test)
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
} catch(err) {
|
||||||
console.log("Failed to find the site with the ID: " + siteId);
|
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 { Meteor } from 'meteor/meteor';
|
||||||
import {Roles} from 'meteor/alanning:roles';
|
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 {createTheme, ThemeProvider} from '@mui/material/styles'
|
||||||
import { useTracker } from 'meteor/react-meteor-data';
|
import { useTracker } from 'meteor/react-meteor-data';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|||||||
@@ -28,14 +28,15 @@ const cssButtonContainer = {
|
|||||||
|
|
||||||
const SiteEditor = ({value, close}) => {
|
const SiteEditor = ({value, close}) => {
|
||||||
const [name, setName] = useState(value.name || "")
|
const [name, setName] = useState(value.name || "")
|
||||||
|
const [externalId, setExternalId] = useState(value.externalId || "")
|
||||||
|
|
||||||
const applyChanges = () => {
|
const applyChanges = () => {
|
||||||
close()
|
close()
|
||||||
//TODO Should invert this and only close if there was success on the server.
|
//TODO Should invert this and only close if there was success on the server.
|
||||||
if(value._id)
|
if(value._id)
|
||||||
Meteor.call("sites.update", value._id, name);
|
Meteor.call("sites.update", value._id, name, externalId);
|
||||||
else
|
else
|
||||||
Meteor.call("sites.add", name);
|
Meteor.call("sites.add", name, externalId);
|
||||||
}
|
}
|
||||||
const rejectChanges = () => {
|
const rejectChanges = () => {
|
||||||
close()
|
close()
|
||||||
@@ -45,6 +46,7 @@ const SiteEditor = ({value, close}) => {
|
|||||||
<div style={cssFieldContainer}>
|
<div style={cssFieldContainer}>
|
||||||
<h1>Site Editor</h1>
|
<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="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}>
|
<div style={cssButtonContainer}>
|
||||||
<Button variant="contained" className="button accept-button" onClick={applyChanges}>Accept</Button>
|
<Button variant="contained" className="button accept-button" onClick={applyChanges}>Accept</Button>
|
||||||
<Button variant="outlined" className="button reject-button" onClick={rejectChanges}>Reject</Button>
|
<Button variant="outlined" className="button reject-button" onClick={rejectChanges}>Reject</Button>
|
||||||
@@ -68,6 +70,10 @@ export default () => {
|
|||||||
name: "Name",
|
name: "Name",
|
||||||
value: (row) => row.name,
|
value: (row) => row.name,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "External ID",
|
||||||
|
value: (row) => row.externalId,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ import Select from '@mui/material/Select';
|
|||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import {Students} from "/imports/api/students";
|
import {Students} from "/imports/api/students";
|
||||||
import {Sites} from "/imports/api/sites";
|
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 = {
|
const cssSitesSelect = {
|
||||||
margin: '0.6rem 0',
|
margin: '0.6rem 0',
|
||||||
@@ -24,7 +35,7 @@ const cssFieldColumnContainer = {
|
|||||||
}
|
}
|
||||||
const cssGridFieldContainer = {
|
const cssGridFieldContainer = {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: "1fr 1fr 1fr",
|
gridTemplateColumns: "1fr 1fr 1fr 1fr",
|
||||||
columnGap: '1rem',
|
columnGap: '1rem',
|
||||||
rowGap: '0.4rem',
|
rowGap: '0.4rem',
|
||||||
marginBottom: '1.5rem'
|
marginBottom: '1.5rem'
|
||||||
@@ -39,8 +50,10 @@ const StudentEditor = ({value, close, defaultSiteId}) => {
|
|||||||
const [email, setEmail] = useState(value.email || "")
|
const [email, setEmail] = useState(value.email || "")
|
||||||
const [id, setId] = useState(value.id || "")
|
const [id, setId] = useState(value.id || "")
|
||||||
const [firstName, setFirstName] = useState(value.firstName || "")
|
const [firstName, setFirstName] = useState(value.firstName || "")
|
||||||
|
const [firstNameAlias, setFirstNameAlias] = useState(value.firstNameAlias || "")
|
||||||
const [lastName, setLastName] = useState(value.lastName || "")
|
const [lastName, setLastName] = useState(value.lastName || "")
|
||||||
const [grade, setGrade] = useState(value.grade || "")
|
const [grade, setGrade] = useState(value.grade || "")
|
||||||
|
const [active, setActive] = useState(value.active)
|
||||||
const [siteId, setSiteId] = useState(value.siteId ? value.siteId : defaultSiteId)
|
const [siteId, setSiteId] = useState(value.siteId ? value.siteId : defaultSiteId)
|
||||||
|
|
||||||
const {sites} = useTracker(() => {
|
const {sites} = useTracker(() => {
|
||||||
@@ -57,9 +70,9 @@ const StudentEditor = ({value, close, defaultSiteId}) => {
|
|||||||
close()
|
close()
|
||||||
//TODO Should invert this and only close if there was success on the server.
|
//TODO Should invert this and only close if there was success on the server.
|
||||||
if(value._id)
|
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
|
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 = () => {
|
const rejectChanges = () => {
|
||||||
close()
|
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="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="Email" value={email} onChange={(e) => {setEmail(e.target.value)}}/>
|
||||||
<TextField variant="standard" label="Grade" value={grade} onChange={(e) => {setGrade(e.target.value)}}/>
|
<TextField variant="standard" label="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="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 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)}}>
|
<TextField select variant="standard" label="Site" value={siteId} onChange={(e) => {setSiteId(e.target.value)}}>
|
||||||
{sites.map((next, i) => {
|
{sites.map((next, i) => {
|
||||||
@@ -103,8 +118,23 @@ export default () => {
|
|||||||
return {sites}
|
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 {students} = useTracker(() => {
|
||||||
const studentQuery = site === siteAll._id ? {} : {siteId: site}
|
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();
|
let students = Students.find(studentQuery).fetch();
|
||||||
|
|
||||||
return {students}
|
return {students}
|
||||||
@@ -132,6 +162,10 @@ export default () => {
|
|||||||
name: "First Name",
|
name: "First Name",
|
||||||
value: (row) => row.firstName,
|
value: (row) => row.firstName,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Alias",
|
||||||
|
value: (row) => row.firstNameAlias,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Last Name",
|
name: "Last Name",
|
||||||
value: (row) => row.lastName,
|
value: (row) => row.lastName,
|
||||||
@@ -149,6 +183,10 @@ export default () => {
|
|||||||
else return 1
|
else return 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Active",
|
||||||
|
value: (row) => row.active ? "Active" : "Inactive",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@@ -162,14 +200,81 @@ 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextField label="Site" style={cssSitesSelect} select variant="standard" value={site} onChange={(e)=>{setSite(e.target.value)}}>
|
<Dialog open={showImportDialog}>
|
||||||
{sites.map((next, i) => {
|
<DialogTitle>Import</DialogTitle>
|
||||||
return <MenuItem key={next._id} value={next._id}>{next.name}</MenuItem>
|
<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>
|
||||||
</TextField>
|
<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}/>
|
<SimpleTable rows={students} columns={columns} options={options}/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ export default () => {
|
|||||||
path: '/byAsset',
|
path: '/byAsset',
|
||||||
href: 'byAsset'
|
href: 'byAsset'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Report",
|
||||||
|
getElement: () => {
|
||||||
|
const AssignmentsReport = lazy(()=>import('./Assignments/Report'))
|
||||||
|
return <AssignmentsReport/>
|
||||||
|
},
|
||||||
|
path: '/report',
|
||||||
|
href: 'report'
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return <TabNav tabs={tabs}/>
|
return <TabNav tabs={tabs}/>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const AssignmentsByAsset = () => {
|
|||||||
|
|
||||||
//Dialog stuff.
|
//Dialog stuff.
|
||||||
const [openUnassignDialog, setOpenUnassignDialog] = useState(false)
|
const [openUnassignDialog, setOpenUnassignDialog] = useState(false)
|
||||||
|
const [unassignDialogEditConditionOnly, setUnassignDialogEditConditionOnly] = useState(false)
|
||||||
const [unassignCondition, setUnassignCondition] = useState(conditions[2])
|
const [unassignCondition, setUnassignCondition] = useState(conditions[2])
|
||||||
const [unassignComment, setUnassignComment] = useState("")
|
const [unassignComment, setUnassignComment] = useState("")
|
||||||
const [unassignConditionDetails, setUnassignConditionDetails] = useState("")
|
const [unassignConditionDetails, setUnassignConditionDetails] = useState("")
|
||||||
@@ -71,8 +72,9 @@ const AssignmentsByAsset = () => {
|
|||||||
// if(assetIdInput) assetIdInput.focus()
|
// if(assetIdInput) assetIdInput.focus()
|
||||||
// })
|
// })
|
||||||
|
|
||||||
const unassign = () => {
|
const unassign = (editConditionOnly) => {
|
||||||
// Open the dialog to get condition and comment.
|
// Open the dialog to get condition and comment.
|
||||||
|
setUnassignDialogEditConditionOnly(editConditionOnly)
|
||||||
setUnassignComment("")
|
setUnassignComment("")
|
||||||
setUnassignCondition(foundAsset.condition ? foundAsset.condition : conditions[2])
|
setUnassignCondition(foundAsset.condition ? foundAsset.condition : conditions[2])
|
||||||
setUnassignConditionDetails(foundAsset.conditionDetails || "")
|
setUnassignConditionDetails(foundAsset.conditionDetails || "")
|
||||||
@@ -82,18 +84,26 @@ const AssignmentsByAsset = () => {
|
|||||||
setOpenUnassignDialog(false)
|
setOpenUnassignDialog(false)
|
||||||
|
|
||||||
if(unassign === true) {
|
if(unassign === true) {
|
||||||
// Call assets.unassign(assetId, comment, condition, conditionDetails, date)
|
if(unassignDialogEditConditionOnly) {
|
||||||
Meteor.call('assets.unassign', foundAsset.assetId, unassignComment, unassignCondition, unassignConditionDetails, (err, result) => {
|
Meteor.call('assets.updateCondition', unassignAsset._id, unassignCondition, unassignConditionDetails, (err, result) => {
|
||||||
if(err) console.error(err)
|
if(err) console.error(err)
|
||||||
else if(assetIdInput) assetIdInput.focus()
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={openUnassignDialog} onClose={unassignDialogClosed}>
|
<Dialog open={openUnassignDialog} onClose={unassignDialogClosed}>
|
||||||
<DialogTitle>Unassign Asset</DialogTitle>
|
<DialogTitle>{unassignDialogEditConditionOnly ? "Edit Condition" : "Unassign Asset"}</DialogTitle>
|
||||||
<DialogContent style={{display: 'flex', flexDirection: 'column'}}>
|
<DialogContent style={{display: 'flex', flexDirection: 'column'}}>
|
||||||
<div>
|
<div>
|
||||||
<TextField style={cssEditorField} select variant="standard" label="Condition" value={unassignCondition} onChange={(e)=>{setUnassignCondition(e.target.value)}}>
|
<TextField style={cssEditorField} select variant="standard" label="Condition" value={unassignCondition} onChange={(e)=>{setUnassignCondition(e.target.value)}}>
|
||||||
@@ -102,11 +112,11 @@ const AssignmentsByAsset = () => {
|
|||||||
})}
|
})}
|
||||||
</TextField>
|
</TextField>
|
||||||
</div>
|
</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)}}/>
|
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} multiline rows={4} variant="outlined" label="Condition Details" value={unassignConditionDetails} onChange={(e) => {setUnassignConditionDetails(e.target.value)}}/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => unassignDialogClosed(true)}>Unassign</Button>
|
<Button onClick={() => unassignDialogClosed(true)}>{unassignDialogEditConditionOnly ? "Save" : "Unassign"}</Button>
|
||||||
<Button onClick={() => unassignDialogClosed(false)}>Cancel</Button>
|
<Button onClick={() => unassignDialogClosed(false)}>Cancel</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -123,7 +133,9 @@ const AssignmentsByAsset = () => {
|
|||||||
<>
|
<>
|
||||||
<div>Assigned on: {foundAsset.assignmentDate.toString()}</div>
|
<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>
|
<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>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import _ from 'lodash';
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
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 Box from "@mui/material/Box";
|
||||||
import ToggleButton from '@mui/material/ToggleButton';
|
import ToggleButton from '@mui/material/ToggleButton';
|
||||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||||
@@ -19,7 +19,9 @@ import {Assets, conditions} from "/imports/api/assets";
|
|||||||
import {AssetTypes} from "/imports/api/asset-types";
|
import {AssetTypes} from "/imports/api/asset-types";
|
||||||
import {Students} from "/imports/api/students";
|
import {Students} from "/imports/api/students";
|
||||||
import {Staff} from "/imports/api/staff";
|
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";
|
||||||
|
|
||||||
const cssTwoColumnContainer = {
|
const cssTwoColumnContainer = {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@@ -32,10 +34,15 @@ const cssEditorField = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AssignmentsByPerson = () => {
|
const AssignmentsByPerson = () => {
|
||||||
const theme = useTheme();
|
const navigate = useNavigate()
|
||||||
const [searchType, setSearchType] = useState("Email")
|
const navigateType = useNavigationType()
|
||||||
const [search, setSearch] = useState("")
|
const location = useLocation()
|
||||||
const [selectedPerson, setSelectedPerson] = useState("")
|
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 [assetId, setAssetId] = useState("")
|
||||||
const [openAssignDialog, setOpenAssignDialog] = useState(false)
|
const [openAssignDialog, setOpenAssignDialog] = useState(false)
|
||||||
const [assignCondition, setAssignCondition] = useState(conditions[2])
|
const [assignCondition, setAssignCondition] = useState(conditions[2])
|
||||||
@@ -43,6 +50,7 @@ const AssignmentsByPerson = () => {
|
|||||||
|
|
||||||
//Dialog stuff.
|
//Dialog stuff.
|
||||||
const [openUnassignDialog, setOpenUnassignDialog] = useState(false)
|
const [openUnassignDialog, setOpenUnassignDialog] = useState(false)
|
||||||
|
const [unassignDialogEditConditionOnly, setUnassignDialogEditConditionOnly] = useState(false)
|
||||||
const [unassignCondition, setUnassignCondition] = useState(conditions[2])
|
const [unassignCondition, setUnassignCondition] = useState(conditions[2])
|
||||||
const [unassignComment, setUnassignComment] = useState("")
|
const [unassignComment, setUnassignComment] = useState("")
|
||||||
const [unassignConditionDetails, setUnassignConditionDetails] = useState("")
|
const [unassignConditionDetails, setUnassignConditionDetails] = useState("")
|
||||||
@@ -56,13 +64,17 @@ const AssignmentsByPerson = () => {
|
|||||||
if(search && search.length > 1) {
|
if(search && search.length > 1) {
|
||||||
let query;
|
let query;
|
||||||
|
|
||||||
if(searchType === "Email") {
|
query = {$or: [{email: {$regex: search, $options: 'i'}}, {firstName: {$regex: search, $options: 'i'}}, {firstNameAlias: {$regex: search, $options: 'i'}}, {lastName: {$regex: search, $options: 'i'}}]}
|
||||||
query = {email: {$regex: search, $options: 'i'}};
|
// if(searchType === "Email") {
|
||||||
} else if(searchType === 'First Name') {
|
// query = {email: {$regex: search, $options: 'i'}};
|
||||||
query = {firstName: {$regex: search, $options: 'i'}}
|
// } else if(searchType === 'First Name') {
|
||||||
} else {
|
// query = {firstName: {$regex: search, $options: 'i'}}
|
||||||
query = {lastName: {$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 students = Students.find(query).fetch();
|
||||||
const staff = Staff.find(query).fetch();
|
const staff = Staff.find(query).fetch();
|
||||||
@@ -143,8 +155,9 @@ const AssignmentsByPerson = () => {
|
|||||||
// if(assetIdInput) assetIdInput.focus()
|
// if(assetIdInput) assetIdInput.focus()
|
||||||
// })
|
// })
|
||||||
|
|
||||||
const unassign = (asset) => {
|
const unassign = (asset, editConditionOnly) => {
|
||||||
// Open the dialog to get condition and comment.
|
// Open the dialog to get condition and comment.
|
||||||
|
setUnassignDialogEditConditionOnly(editConditionOnly)
|
||||||
setUnassignAsset(asset);
|
setUnassignAsset(asset);
|
||||||
setUnassignComment("")
|
setUnassignComment("")
|
||||||
setUnassignCondition(asset.condition ? asset.condition : conditions[2])
|
setUnassignCondition(asset.condition ? asset.condition : conditions[2])
|
||||||
@@ -155,11 +168,19 @@ const AssignmentsByPerson = () => {
|
|||||||
setOpenUnassignDialog(false)
|
setOpenUnassignDialog(false)
|
||||||
|
|
||||||
if(unassign === true) {
|
if(unassign === true) {
|
||||||
// Call assets.unassign(assetId, comment, condition, conditionDetails, date)
|
if(unassignDialogEditConditionOnly) {
|
||||||
Meteor.call('assets.unassign', unassignAsset.assetId, unassignComment, unassignCondition, unassignConditionDetails, (err, result) => {
|
Meteor.call('assets.updateCondition', unassignAsset._id, unassignCondition, unassignConditionDetails, (err, result) => {
|
||||||
if(err) console.error(err)
|
if(err) console.error(err)
|
||||||
else if(assetIdInput) assetIdInput.focus()
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +192,20 @@ const AssignmentsByPerson = () => {
|
|||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
// '&:nthChild(even)': {backgroundColor: '#935e5e'}
|
// '&:nthChild(even)': {backgroundColor: '#935e5e'}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changeSelectedPerson = (person) => {
|
||||||
|
setSelectedPerson(person)
|
||||||
|
navigate("/assignments", {replace: false, state: {person, search}});
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
if(!state) navigate("/assignments/byPerson", {replace: true, state: {search: "", person: null}})
|
||||||
|
else {
|
||||||
|
if(navigateType === NavigationType.Pop) {
|
||||||
|
setSearch(state.search)
|
||||||
|
setSelectedPerson(state.person)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -193,7 +228,7 @@ const AssignmentsByPerson = () => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={openUnassignDialog} onClose={unassignDialogClosed}>
|
<Dialog open={openUnassignDialog} onClose={unassignDialogClosed}>
|
||||||
<DialogTitle>Unassign Asset</DialogTitle>
|
<DialogTitle>{unassignDialogEditConditionOnly ? "Edit Condition" : "Unassign Asset"}</DialogTitle>
|
||||||
<DialogContent style={{display: 'flex', flexDirection: 'column'}}>
|
<DialogContent style={{display: 'flex', flexDirection: 'column'}}>
|
||||||
<div>
|
<div>
|
||||||
<TextField style={cssEditorField} select variant="standard" label="Condition" value={unassignCondition} onChange={(e)=>{setUnassignCondition(e.target.value)}}>
|
<TextField style={cssEditorField} select variant="standard" label="Condition" value={unassignCondition} onChange={(e)=>{setUnassignCondition(e.target.value)}}>
|
||||||
@@ -202,47 +237,51 @@ const AssignmentsByPerson = () => {
|
|||||||
})}
|
})}
|
||||||
</TextField>
|
</TextField>
|
||||||
</div>
|
</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)}}/>
|
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} multiline rows={4} variant="outlined" label="Condition Details" value={unassignConditionDetails} onChange={(e) => {setUnassignConditionDetails(e.target.value)}}/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => unassignDialogClosed(true)}>Unassign</Button>
|
<Button onClick={() => unassignDialogClosed(true)}>{unassignDialogEditConditionOnly ? "Save" : "Unassign"}</Button>
|
||||||
<Button onClick={() => unassignDialogClosed(false)}>Cancel</Button>
|
<Button onClick={() => unassignDialogClosed(false)}>Cancel</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Box style={{marginTop: '1rem',...cssTwoColumnContainer}}>
|
<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">
|
{/*<ToggleButtonGroup color="primary" value={searchType} exclusive onChange={(e, type)=>setSearchType(type)} aria-label="Search Type">*/}
|
||||||
<ToggleButton value="Email">Email</ToggleButton>
|
{/* <ToggleButton value="Email">Email</ToggleButton>*/}
|
||||||
<ToggleButton value="First Name">First Name</ToggleButton>
|
{/* <ToggleButton value="First Name">First Name</ToggleButton>*/}
|
||||||
<ToggleButton value="Last Name">Last Name</ToggleButton>
|
{/* <ToggleButton value="Last Name">Last Name</ToggleButton>*/}
|
||||||
</ToggleButtonGroup>
|
{/*</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" value={search} onChange={(e) => {setSearch(e.target.value)}}/>
|
<TextField style={cssEditorField} variant="standard" label="Search" value={search} onChange={(e) => {setSearch(e.target.value)}}/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box style={cssTwoColumnContainer}>
|
<Box style={{...cssTwoColumnContainer, gridTemplateColumns: "24rem 1fr"}}>
|
||||||
<div style={{maxHeight: '26rem', overflowY:'auto', minWidth: '10rem', minHeight: '10rem'}}>
|
<div style={{maxHeight: '26rem', overflowY:'auto', minWidth: '10rem', minHeight: '10rem', maxWidth: '40rem'}}>
|
||||||
<List>
|
<List>
|
||||||
{people.map((next, i) => {
|
{people.map((next, i) => {
|
||||||
return (
|
return (
|
||||||
<ListItemButton key={next._id} style={getListItemStyle(next)} selected={selectedPerson === next} onClick={(e) => {setSelectedPerson(next)}}>
|
<ListItemButton key={next._id} style={getListItemStyle(next)} selected={selectedPerson === next} onClick={(e) => {changeSelectedPerson(next)}}>
|
||||||
<ListItemText primary={next.firstName + " " + next.lastName} secondary={next.email}/>
|
<ListItemText primary={next.firstName + " " + (next.firstNameAlias ? "'" + next.firstNameAlias + "' " : "") + next.lastName + (next.grade ? " (" + next.grade + ")" : "")} secondary={next.email}/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
<div style={{display: 'flex', flexDirection: 'column', margin: '1rem 0 0 .5rem'}}>
|
<div style={{display: 'flex', flexDirection: 'column', margin: '0 0 0 .5rem'}}>
|
||||||
{selectedPerson && (
|
{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>
|
<h3 style={{margin: "0 0 0.5rem 0"}}>{selectedPerson.firstName + " " + (selectedPerson.firstNameAlias ? "'" + selectedPerson.firstNameAlias + "' " : "") + selectedPerson.lastName + (selectedPerson.grade ? " (" + selectedPerson.grade + ")" : "")}</h3>
|
||||||
<div>{foundAsset && foundAsset.assetType.name}</div>
|
<div style={{...cssAssetTile, paddingTop: "0"}}>
|
||||||
<div>{foundAsset && <Link to={"/search?assetId=" + encodeURIComponent(foundAsset.assetId)}>{foundAsset.assetId}</Link>}</div>
|
<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 && <Link to={"/search?serial=" + encodeURIComponent(foundAsset.serial)}>{foundAsset.serial}</Link>}</div>
|
<div>{foundAsset && foundAsset.assetType.name}</div>
|
||||||
{foundAsset && foundAsset.assignee && (
|
<div>{foundAsset && <Link to={"/search?assetId=" + encodeURIComponent(foundAsset.assetId)}>{foundAsset.assetId}</Link>}</div>
|
||||||
<div>Assigned To: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName}</div>
|
<div>{foundAsset && <Link to={"/search?serial=" + encodeURIComponent(foundAsset.serial)}>{foundAsset.serial}</Link>}</div>
|
||||||
)}
|
{foundAsset && foundAsset.assignee && (
|
||||||
<Button variant="contained" color='primary' className="button" disabled={!foundAsset || foundAsset.assignee !== undefined} onClick={()=>assign()}>Assign</Button>
|
<div>Assigned To: {foundAsset.assignee.firstName} {foundAsset.assignee.lastName}</div>
|
||||||
</div>
|
)}
|
||||||
|
<Button variant="contained" color='primary' className="button" disabled={!foundAsset || foundAsset.assignee !== undefined} onClick={()=>assign()}>Assign</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{assets.map((next, i) => {
|
{assets.map((next, i) => {
|
||||||
return (
|
return (
|
||||||
@@ -250,7 +289,9 @@ const AssignmentsByPerson = () => {
|
|||||||
<div>{next.assetType.name}</div>
|
<div>{next.assetType.name}</div>
|
||||||
<div><Link to={"/search?assetId=" + encodeURIComponent(next.assetId)}>{next.assetId}</Link></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>
|
<div><Link to={"/search?serial=" + encodeURIComponent(next.serial)}>{next.serial}</Link></div>
|
||||||
<Button variant="contained" color='secondary' className="button" onClick={()=>unassign(next)}>Unassign</Button>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
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 DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogContentText from '@mui/material/DialogContentText';
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
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 {AssetTypes} from "/imports/api/asset-types";
|
||||||
import {Students} from "/imports/api/students";
|
import {Students} from "/imports/api/students";
|
||||||
import {Staff} from "/imports/api/staff";
|
import {Staff} from "/imports/api/staff";
|
||||||
@@ -34,6 +34,21 @@ const cssEditorField = {
|
|||||||
const Statistics = () => {
|
const Statistics = () => {
|
||||||
const [selectedMissingAsset, setSelectedMissingAsset] = useState("")
|
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(() => {
|
const {missingAssets} = useTracker(() => {
|
||||||
let missingAssets = [];
|
let missingAssets = [];
|
||||||
|
|
||||||
@@ -54,16 +69,32 @@ const Statistics = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Missing Equipment</h1>
|
<div style={{display: 'flex', flexFlow: 'row wrap', justifyContent: 'center', alignItems: 'flex-start', columnGap: '1rem'}}>
|
||||||
<List>
|
<div>
|
||||||
{missingAssets.map((next, i) => {
|
<h1>Equipment Counts</h1>
|
||||||
return (
|
<List sx={{overflow: "auto", maxHeight: "30rem", position: "relative", width: "30rem", border: "1px solid #999", marginBottom: "2rem"}}>
|
||||||
<ListItemButton key={next._id} style={getListItemStyle(next)} selected={selectedMissingAsset === next} onClick={(e) => {setSelectedMissingAsset(next)}}>
|
{assetStatistics.map((next, i) => {
|
||||||
<ListItemText primary={next.assetId} secondary={next.assetType.name}/>
|
return (
|
||||||
</ListItemButton>
|
<ListItem key={next.name} style={getListItemStyle(next)}>
|
||||||
)
|
<ListItemText primary={next.name} secondary={next.count}/>
|
||||||
})}
|
</ListItem>
|
||||||
</List>
|
)
|
||||||
|
})}
|
||||||
|
</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,8 @@
|
|||||||
"@lexical/headless": "^0.5.0",
|
"@lexical/headless": "^0.5.0",
|
||||||
"@lexical/link": "^0.5.0",
|
"@lexical/link": "^0.5.0",
|
||||||
"@lexical/react": "^0.5.0",
|
"@lexical/react": "^0.5.0",
|
||||||
"@mui/icons-material": "^5.10.2",
|
"@mui/icons-material": "^5.11.16",
|
||||||
"@mui/material": "^5.10.2",
|
"@mui/material": "^5.13.4",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"csv-parse": "^5.3.0",
|
"csv-parse": "^5.3.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user