Cleaned up the chromebooks view; Reorganized the admin view; Added functionality to import students and staff (still need to add/test functionality to edit them and delete them).
This commit is contained in:
2
.idea/watcherTasks.xml
generated
2
.idea/watcherTasks.xml
generated
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectTasksOptions">
|
<component name="ProjectTasksOptions">
|
||||||
<TaskOptions isEnabled="false">
|
<TaskOptions isEnabled="true">
|
||||||
<option name="arguments" value="$FileName$:$FileNameWithoutExtension$.css" />
|
<option name="arguments" value="$FileName$:$FileNameWithoutExtension$.css" />
|
||||||
<option name="checkSyntaxErrors" value="true" />
|
<option name="checkSyntaxErrors" value="true" />
|
||||||
<option name="description" />
|
<option name="description" />
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>District Central</title>
|
<title>District Central</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="./theme/smui.css">
|
||||||
|
<meteor-bundled-css />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
|
||||||
@import './theme/smui.css'
|
|
||||||
@import "./simple-grid.sass"
|
@import "./simple-grid.sass"
|
||||||
@import "./app.sass"
|
@import "./app.sass"
|
||||||
@import './material-icons.sass'
|
@import './material-icons.sass'
|
||||||
|
|
||||||
|
//.mdc-floating-label
|
||||||
|
// transform: translateY(-80%)
|
||||||
|
//.mdc-select__selected-text-container
|
||||||
|
// transform: translateY(30%)
|
||||||
@@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
|
|||||||
import { Mongo } from 'meteor/mongo';
|
import { Mongo } from 'meteor/mongo';
|
||||||
import { check } from 'meteor/check';
|
import { check } from 'meteor/check';
|
||||||
import { MongoClient } from 'mongodb';
|
import { MongoClient } from 'mongodb';
|
||||||
|
import {Assets} from "/imports/api/assets";
|
||||||
//import {Roles} from 'alanning/roles';
|
//import {Roles} from 'alanning/roles';
|
||||||
|
|
||||||
//export const Records = new Mongo.Collection('records');
|
//export const Records = new Mongo.Collection('records');
|
||||||
@@ -48,6 +49,14 @@ if (Meteor.isServer) {
|
|||||||
$regex: params.serial,
|
$regex: params.serial,
|
||||||
$options: "i"
|
$options: "i"
|
||||||
} : params.serial;
|
} : params.serial;
|
||||||
|
else if (params.assetId) {
|
||||||
|
let asset = Assets.findOne({assetId: params.assetId});
|
||||||
|
|
||||||
|
if(asset.serial) {
|
||||||
|
// An exact search.
|
||||||
|
query.serial = asset.serial;
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (params.email) query.email = params.regex ? {
|
else if (params.email) query.email = params.regex ? {
|
||||||
$regex: params.email,
|
$regex: params.email,
|
||||||
$options: "i"
|
$options: "i"
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import {Mongo} from "meteor/mongo";
|
import {Mongo} from "meteor/mongo";
|
||||||
import {Meteor} from "meteor/meteor";
|
import {Meteor} from "meteor/meteor";
|
||||||
import { Roles } from 'meteor/alanning:roles';
|
import { Roles } from 'meteor/alanning:roles';
|
||||||
|
import {check} from "meteor/check";
|
||||||
|
import {Sites} from "/imports/api/sites";
|
||||||
|
import {parse} from "csv-parse";
|
||||||
|
import {Students} from "/imports/api/students";
|
||||||
|
|
||||||
export const Staff = new Mongo.Collection('staff');
|
export const Staff = new Mongo.Collection('staff');
|
||||||
|
|
||||||
@@ -21,5 +25,129 @@ Meteor.methods({
|
|||||||
//TODO: Need to first verify there are no checked out assets to the staff member.
|
//TODO: Need to first verify there are no checked out assets to the staff member.
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Assumes that the ID field is a unique ID that never changes for staff.
|
||||||
|
* This must be true in order for duplicate staff to be avoided.
|
||||||
|
* Will automatically update staff data, including the site he/she is associated with.
|
||||||
|
*
|
||||||
|
* Expects the CSV string to contain comma delimited data in the form:
|
||||||
|
* ID, email, first name, last name
|
||||||
|
*
|
||||||
|
* The query in Aeries is: `LIST STF ID FN LN EM PSC`.
|
||||||
|
* A more complete Aeries query: `LIST STF ID FN LN EM BY FN IF PSC = 5`
|
||||||
|
* Note that you will want to run this query for each school, and the district. The example above sets the school to #5 (PSC).
|
||||||
|
* Run the query in Aeries as a `Report`, select TXT, and upload here.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* StuEmail,Student ID,First Name,Last Name,Grade,First Name Alias,Last Name Alias
|
||||||
|
* @type: Currently only supports 'CSV' or 'Aeries Text Report'
|
||||||
|
*/
|
||||||
|
'staff.loadCsv'(csv, type, siteId) {
|
||||||
|
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
|
||||||
|
check(csv, String);
|
||||||
|
check(siteId, String);
|
||||||
|
|
||||||
|
let site = Sites.findOne({_id: siteId});
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
// Remove headers from the CSV.
|
||||||
|
for(const line of lines) {
|
||||||
|
if (skip > 0) skip--;
|
||||||
|
else if (pageHeader && line === pageHeader) {
|
||||||
|
skip = 2;
|
||||||
|
} else {
|
||||||
|
if(!cleanCsv) cleanCsv = "";
|
||||||
|
else cleanCsv += '\r\n';
|
||||||
|
cleanCsv += line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log(cleanCsv);
|
||||||
|
|
||||||
|
// Note: This doesn't work because some values are quoted and contain commas as a value character.
|
||||||
|
// Parse the CSV (now without any headers).
|
||||||
|
// lines = cleanCsv.split(/\r\n/);
|
||||||
|
// for(const line of lines) {
|
||||||
|
// let values = line.split(/\s*,\s*/);
|
||||||
|
//
|
||||||
|
// if(values.length >= 5) {
|
||||||
|
// 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};
|
||||||
|
//
|
||||||
|
// // console.log(student);
|
||||||
|
// // Update or insert in the db.
|
||||||
|
// console.log("Upserting: " + student);
|
||||||
|
// Students.upsert({id}, {$set: student});
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const bound = Meteor.bindEnvironment((callback) => {callback();});
|
||||||
|
|
||||||
|
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 staff = {siteId, email, id, firstName, lastName, active};
|
||||||
|
|
||||||
|
// Track the staff ID's and record duplicates. This is used to ensure our counts are accurate later.
|
||||||
|
if(foundIds.has(staff.id)) {
|
||||||
|
duplicates.push(staff.id);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
foundIds.add(staff.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log(staff);
|
||||||
|
try {
|
||||||
|
Staff.upsert({id: staff.id}, {$set: staff});
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(duplicates.length + " records were duplicates:");
|
||||||
|
console.log(duplicates);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("Failed to find the site with the ID: " + siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Mongo } from 'meteor/mongo';
|
|||||||
import { check } from 'meteor/check';
|
import { check } from 'meteor/check';
|
||||||
import {Sites} from "./sites";
|
import {Sites} from "./sites";
|
||||||
import { Roles } from 'meteor/alanning:roles';
|
import { Roles } from 'meteor/alanning:roles';
|
||||||
|
import {parse} from 'csv-parse';
|
||||||
|
|
||||||
export const Students = new Mongo.Collection('students');
|
export const Students = new Mongo.Collection('students');
|
||||||
|
|
||||||
@@ -30,20 +31,28 @@ Meteor.methods({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
* Assumes that the ID field is a unique ID that never changes for a student.
|
||||||
|
* This must be true in order for duplicate students to be avoided.
|
||||||
|
* Will automatically update a student's data, including the site he/she is associated with.
|
||||||
|
*
|
||||||
* 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 SEM ID FN LN GR FNA LNA`.
|
* The query in Aeries is: `LIST STU ID SEM FN LN NG FNA`.
|
||||||
* The query in SQL is: `SELECT [STU].[SEM] AS [StuEmail], [STU].[ID] AS [Student ID], 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];`.
|
* 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`
|
||||||
|
* 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];`.
|
||||||
* 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.
|
||||||
*
|
*
|
||||||
* 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
|
||||||
* StuEmail,Student ID,First Name,Last Name,Grade,First Name Alias,Last Name Alias
|
* Student ID, Email, First Name,Last Name,Grade,(opt) First Name Alias
|
||||||
|
* @type: Currently only supports 'CSV' or 'Aeries Text Report'
|
||||||
*/
|
*/
|
||||||
'students.loadCsv'(csv, siteId) {
|
'students.loadCsv'(csv, type, siteId) {
|
||||||
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
|
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
|
||||||
check(csv, String);
|
check(csv, String);
|
||||||
check(siteId, String);
|
check(siteId, String);
|
||||||
@@ -51,36 +60,99 @@ Meteor.methods({
|
|||||||
let site = Sites.findOne({_id: siteId});
|
let site = Sites.findOne({_id: siteId});
|
||||||
|
|
||||||
if(site) {
|
if(site) {
|
||||||
|
let cleanCsv;
|
||||||
let lines = csv.split(/\r?\n/);
|
let lines = csv.split(/\r?\n/);
|
||||||
let pageHeader = lines[0];
|
let pageHeader = type === 'Aeries Text Report' ? lines[0] : null; // Skip the repeating header lines for an Aeries text report.
|
||||||
let skip;
|
let skip = type === 'CSV' ? 1 : 0; // Skip the first line of a CSV file (headers).
|
||||||
|
|
||||||
for (const line of lines) {
|
// Remove headers from the CSV.
|
||||||
|
for(const line of lines) {
|
||||||
if (skip > 0) skip--;
|
if (skip > 0) skip--;
|
||||||
else if (line === pageHeader) {
|
else if (pageHeader && line === pageHeader) {
|
||||||
skip = 2;
|
skip = 2;
|
||||||
} else {
|
} else {
|
||||||
let values = line.split(/\s*,\s*/);
|
if(!cleanCsv) cleanCsv = "";
|
||||||
|
else cleanCsv += '\r\n';
|
||||||
if(values.length === 7) {
|
cleanCsv += line;
|
||||||
let email = values[0];
|
|
||||||
let id = values[1];
|
|
||||||
let firstName = values[2];
|
|
||||||
let lastName = values[3];
|
|
||||||
let grade = parseInt(values[4], 10);
|
|
||||||
let firstNameAlias = values[5];
|
|
||||||
let active = true;
|
|
||||||
let student = {siteId, email, id, firstName, lastName, grade, firstNameAlias, active};
|
|
||||||
|
|
||||||
// console.log(student);
|
|
||||||
// Update or insert in the db.
|
|
||||||
Students.upsert({id}, {$set: student});
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Find existing student. Update student, and move them to the new site.
|
|
||||||
//TODO: Create student if none exists.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//console.log(cleanCsv);
|
||||||
|
|
||||||
|
// Note: This doesn't work because some values are quoted and contain commas as a value character.
|
||||||
|
// Parse the CSV (now without any headers).
|
||||||
|
// lines = cleanCsv.split(/\r\n/);
|
||||||
|
// for(const line of lines) {
|
||||||
|
// let values = line.split(/\s*,\s*/);
|
||||||
|
//
|
||||||
|
// if(values.length >= 5) {
|
||||||
|
// 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};
|
||||||
|
//
|
||||||
|
// // console.log(student);
|
||||||
|
// // Update or insert in the db.
|
||||||
|
// console.log("Upserting: " + student);
|
||||||
|
// Students.upsert({id}, {$set: student});
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const bound = Meteor.bindEnvironment((callback) => {callback();});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log(student);
|
||||||
|
try {
|
||||||
|
Students.upsert({id: student.id}, {$set: student});
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(duplicates.length + " records were duplicates:");
|
||||||
|
console.log(duplicates);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("Failed to find the site with the ID: " + siteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,358 +1,30 @@
|
|||||||
<script>
|
<script>
|
||||||
import {Meteor} from "meteor/meteor";
|
import Tab, { Label } from '@smui/tab';
|
||||||
import {onMount} from "svelte";
|
import TabBar from '@smui/tab-bar';
|
||||||
import {Sites} from "../api/sites";
|
import AssetTypes from "/imports/ui/Admin/AssetTypes.svelte";
|
||||||
import GridTable from "./GridTable.svelte";
|
import Sites from "/imports/ui/Admin/Sites.svelte";
|
||||||
import {writable} from "svelte/store";
|
import Students from "/imports/ui/Admin/Students.svelte";
|
||||||
import TextField from '@smui/textfield';
|
import Staff from "/imports/ui/Admin/Staff.svelte";
|
||||||
import HelperText from '@smui/textfield/helper-text';
|
|
||||||
import {Students} from "../api/students";
|
|
||||||
import {Staff} from "../api/staff";
|
|
||||||
import {AssetTypes} from "../api/asset-types";
|
|
||||||
|
|
||||||
onMount(async () => {
|
let activeTab = null;
|
||||||
Meteor.subscribe('sites');
|
|
||||||
Meteor.subscribe('assetTypes');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should not be needed now. Did not work well - had a bug somewhere.
|
|
||||||
// const fixRecords = () => {Meteor.call("admin.fixRecords");}
|
|
||||||
|
|
||||||
const siteColumns = [
|
|
||||||
{
|
|
||||||
key: "_id",
|
|
||||||
title: "ID",
|
|
||||||
value: v => v._id,
|
|
||||||
minWidth: 20,
|
|
||||||
weight: 1,
|
|
||||||
cls: "id",
|
|
||||||
}, {
|
|
||||||
key: "name",
|
|
||||||
title: "Name",
|
|
||||||
value: v => v.name,
|
|
||||||
minWidth: 100,
|
|
||||||
weight: 1,
|
|
||||||
cls: "name",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const siteActions = {
|
|
||||||
title: "Actions",
|
|
||||||
headerWidgets: [
|
|
||||||
{icon: "add_box", action: () => {editedSite.set({name: ""});}, tooltip: "Add a new Site."}
|
|
||||||
],
|
|
||||||
rowWidgets: [
|
|
||||||
{icon: "add_circle", action: (v) => {editedSite.set(v)}},
|
|
||||||
{icon: "delete", action: (v) => {deleteSite(v)}}
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const deleteSite = site => {
|
|
||||||
//TODO:
|
|
||||||
};
|
|
||||||
// Create a holder for the site being edited. This allows us to clear the editor when the user finishes, and allows the table or parent view to setup the editor.
|
|
||||||
let editedSite = writable(null);
|
|
||||||
let dirtySite;
|
|
||||||
// Copy the edited site when ever it changes, set some defaults for a new site object (to make the view happy).
|
|
||||||
editedSite.subscribe(site => {dirtySite = Object.assign({name:""}, site)});
|
|
||||||
// Load the sites (reactive).
|
|
||||||
let sites = Sites.find({});
|
|
||||||
const applySiteChanges = () => {
|
|
||||||
if(dirtySite._id)
|
|
||||||
Meteor.call("sites.update", dirtySite._id, dirtySite.name);
|
|
||||||
else
|
|
||||||
Meteor.call("sites.add", dirtySite.name);
|
|
||||||
editedSite.set(null);
|
|
||||||
dirtySite = null;
|
|
||||||
}
|
|
||||||
const rejectSiteChanges = () => {
|
|
||||||
editedSite.set(null);
|
|
||||||
}
|
|
||||||
let selectedSite = null;
|
|
||||||
const onSiteSelection = (e) => {
|
|
||||||
selectedSite = Sites.findOne({_id: e.detail});
|
|
||||||
}
|
|
||||||
|
|
||||||
let students = null;
|
|
||||||
let staff = null;
|
|
||||||
$: {
|
|
||||||
if(selectedSite) {
|
|
||||||
Meteor.subscribe('students', selectedSite._id);
|
|
||||||
Meteor.subscribe('staff', selectedSite._id);
|
|
||||||
students = Students.find({siteId: selectedSite._id});
|
|
||||||
staff = Staff.find({siteId: selectedSite._id});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadStudents = () => {
|
|
||||||
// console.log(files);
|
|
||||||
// console.log(selectedSite);
|
|
||||||
// console.log(selectedSite._id);
|
|
||||||
if(files && files.length) {
|
|
||||||
let file = files[0];
|
|
||||||
let reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
// console.log("Sending Data");
|
|
||||||
// console.log(selectedSite._id);
|
|
||||||
// console.log(reader.result);
|
|
||||||
Meteor.call('students.loadCsv', reader.result, selectedSite._id);
|
|
||||||
}
|
|
||||||
reader.readAsText(file, "UTF-8");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let files;
|
|
||||||
|
|
||||||
const studentColumns = [
|
|
||||||
{
|
|
||||||
key: "_id",
|
|
||||||
title: "ID",
|
|
||||||
value: v => v._id,
|
|
||||||
minWidth: 20,
|
|
||||||
weight: 1,
|
|
||||||
cls: "id",
|
|
||||||
}, {
|
|
||||||
key: "email",
|
|
||||||
title: "Email",
|
|
||||||
value: v => v.email,
|
|
||||||
minWidth: 100,
|
|
||||||
weight: 1,
|
|
||||||
cls: "email",
|
|
||||||
}, {
|
|
||||||
key: "firstName",
|
|
||||||
title: "First Name",
|
|
||||||
value: v => v.firstName,
|
|
||||||
minWidth: 100,
|
|
||||||
weight: 1,
|
|
||||||
cls: "firstName",
|
|
||||||
}, {
|
|
||||||
key: "lastName",
|
|
||||||
title: "Last Name",
|
|
||||||
value: v => v.lastName,
|
|
||||||
minWidth: 100,
|
|
||||||
weight: 1,
|
|
||||||
cls: "lastName",
|
|
||||||
}, {
|
|
||||||
key: "grade",
|
|
||||||
title: "Grade",
|
|
||||||
value: v => v.grade,
|
|
||||||
minWidth: 100,
|
|
||||||
weight: 1,
|
|
||||||
cls: "grade",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
let editedStudent = writable(null);
|
|
||||||
const onStudentSelection = (e) => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const staffColumns = [
|
|
||||||
{
|
|
||||||
key: "_id",
|
|
||||||
title: "ID",
|
|
||||||
value: v => v._id,
|
|
||||||
minWidth: 20,
|
|
||||||
weight: 1,
|
|
||||||
cls: "id",
|
|
||||||
}, {
|
|
||||||
key: "email",
|
|
||||||
title: "Email",
|
|
||||||
value: v => v.email,
|
|
||||||
minWidth: 100,
|
|
||||||
weight: 1,
|
|
||||||
cls: "email",
|
|
||||||
}, {
|
|
||||||
key: "firstName",
|
|
||||||
title: "First Name",
|
|
||||||
value: v => v.firstName,
|
|
||||||
minWidth: 100,
|
|
||||||
weight: 1,
|
|
||||||
cls: "firstName",
|
|
||||||
}, {
|
|
||||||
key: "lastName",
|
|
||||||
title: "Last Name",
|
|
||||||
value: v => v.lastName,
|
|
||||||
minWidth: 100,
|
|
||||||
weight: 1,
|
|
||||||
cls: "lastName",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
let editedStaff = writable(null);
|
|
||||||
const onStaffSelection = (e) => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetTypesColumns = [
|
|
||||||
{
|
|
||||||
key: "_id",
|
|
||||||
title: "ID",
|
|
||||||
value: v => v._id,
|
|
||||||
minWidth: 20,
|
|
||||||
weight: 1,
|
|
||||||
cls: "id",
|
|
||||||
}, {
|
|
||||||
key: "name",
|
|
||||||
title: "Name",
|
|
||||||
value: v => v.name,
|
|
||||||
minWidth: 100,
|
|
||||||
weight: 1,
|
|
||||||
cls: "name",
|
|
||||||
}, {
|
|
||||||
key: "description",
|
|
||||||
title: "Description",
|
|
||||||
value: v => v.description,
|
|
||||||
minWidth: 100,
|
|
||||||
weight: 1,
|
|
||||||
cls: "description",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const assetTypesActions = {
|
|
||||||
title: "Actions",
|
|
||||||
headerWidgets: [
|
|
||||||
{icon: "add_box", action: () => {editedAssetType.set({});}, tooltip: "Add a new asset type."}
|
|
||||||
],
|
|
||||||
rowWidgets: [
|
|
||||||
{icon: "add_circle", action: (v) => {editedAssetType.set(v)}},
|
|
||||||
{icon: "delete", action: (v) => {deleteAssetType(v)}}
|
|
||||||
],
|
|
||||||
};
|
|
||||||
let editedAssetType = writable(null);
|
|
||||||
const onAssetTypeSelection = (e) => {
|
|
||||||
}
|
|
||||||
let dirtyAssetType;
|
|
||||||
// Copy the edited value when ever it changes, set some defaults for a new value object (to make the view happy).
|
|
||||||
editedAssetType.subscribe(v => {dirtyAssetType = Object.assign({name: "", description: ""}, v)});
|
|
||||||
// Load the sites (reactive).
|
|
||||||
let assetTypes = AssetTypes.find({});
|
|
||||||
const deleteAssetType = assetType => {
|
|
||||||
//TODO:
|
|
||||||
};
|
|
||||||
const applyAssetTypeChanges = () => {
|
|
||||||
if(dirtyAssetType._id)
|
|
||||||
Meteor.call("assetTypes.update", dirtyAssetType._id, dirtyAssetType.name, dirtyAssetType.description);
|
|
||||||
else
|
|
||||||
Meteor.call("assetTypes.add", dirtyAssetType.name, dirtyAssetType.description);
|
|
||||||
editedAssetType.set(null);
|
|
||||||
dirtyAssetType = null;
|
|
||||||
}
|
|
||||||
const rejectAssetTypeChanges = () => {
|
|
||||||
editedSite.set(null);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Sites</h2>
|
<TabBar tabs={[{id:'sites', label:'Sites'}, {id:'students', label:'Students'}, {id:'staff', label:'Staff'}, {id:'assetTypes', label:'Asset Types'}]} minWidth let:tab bind:active={activeTab}>
|
||||||
<GridTable bind:rows={sites} columns="{siteColumns}" actions="{siteActions}" rowKey="{(v) => {return v._id}}" bind:edited="{editedSite}" on:selection={onSiteSelection}>
|
<Tab {tab}>
|
||||||
{#if dirtySite}
|
<Label>{tab.label}</Label>
|
||||||
<div class="editorContainer">
|
</Tab>
|
||||||
<div style="grid-column: 1/span 1">
|
</TabBar>
|
||||||
<TextField type="text" style="width: 100%" bind:value={dirtySite.name} label="Name">
|
{#if activeTab && activeTab.id === 'sites'}
|
||||||
<HelperText slot="helper">Provide a unique name for the site.</HelperText>
|
<Sites></Sites>
|
||||||
</TextField>
|
{:else if activeTab && activeTab.id === 'students'}
|
||||||
</div>
|
<Students></Students>
|
||||||
|
{:else if activeTab && activeTab.id === 'staff'}
|
||||||
<button type="button" style="grid-column: 2/span 1;" class="button accept-button material-icons material-symbols-outlined" on:click={applySiteChanges}>
|
<Staff></Staff>
|
||||||
check
|
{:else if activeTab && activeTab.id === 'assetTypes'}
|
||||||
</button>
|
<AssetTypes></AssetTypes>
|
||||||
<button type="button" style="grid-column: 3/span 1;" class="button reject-button material-icons material-symbols-outlined" on:click={rejectSiteChanges}>
|
|
||||||
close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</GridTable>
|
|
||||||
|
|
||||||
{#if selectedSite}
|
|
||||||
<h2>Site Students</h2>
|
|
||||||
<form on:submit|preventDefault={uploadStudents}>
|
|
||||||
<input style="display: inline-block" type="file" multiple="false" accept="text/csv" bind:files={files}/>
|
|
||||||
<input type="submit" value="Upload"/>
|
|
||||||
</form>
|
|
||||||
<GridTable bind:rows={students} columns="{studentColumns}" actions="{null}" rowKey="{(v) => {return v._id}}" bind:edited="{editedStudent}" on:selection={onStudentSelection}>
|
|
||||||
{#if dirtySite}
|
|
||||||
<div class="editorContainer">
|
|
||||||
<div style="grid-column: 1/span 1">
|
|
||||||
<TextField type="text" style="width: 100%" bind:value={dirtySite.name} label="Name">
|
|
||||||
<HelperText slot="helper">Provide a unique name for the site.</HelperText>
|
|
||||||
</TextField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" style="grid-column: 2/span 1;" class="button accept-button material-icons material-symbols-outlined" on:click={applySiteChanges}>
|
|
||||||
check
|
|
||||||
</button>
|
|
||||||
<button type="button" style="grid-column: 3/span 1;" class="button reject-button material-icons material-symbols-outlined" on:click={rejectSiteChanges}>
|
|
||||||
close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</GridTable>
|
|
||||||
|
|
||||||
<h2>Site Staff</h2>
|
|
||||||
<GridTable bind:rows={staff} columns="{staffColumns}" actions="{null}" rowKey="{(v) => {return v._id}}" bind:edited="{editedStaff}" on:selection={onStaffSelection}>
|
|
||||||
{#if dirtySite}
|
|
||||||
<div class="editorContainer">
|
|
||||||
<div style="grid-column: 1/span 1">
|
|
||||||
<TextField type="text" style="width: 100%" bind:value={dirtySite.name} label="Name">
|
|
||||||
<HelperText slot="helper">Provide a unique name for the site.</HelperText>
|
|
||||||
</TextField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" style="grid-column: 2/span 1;" class="button accept-button material-icons material-symbols-outlined" on:click={applySiteChanges}>
|
|
||||||
check
|
|
||||||
</button>
|
|
||||||
<button type="button" style="grid-column: 3/span 1;" class="button reject-button material-icons material-symbols-outlined" on:click={rejectSiteChanges}>
|
|
||||||
close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</GridTable>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h2>Asset Types</h2>
|
|
||||||
<GridTable bind:rows={assetTypes} columns="{assetTypesColumns}" actions="{assetTypesActions}" rowKey="{(v) => {return v._id}}" bind:edited="{editedAssetType}" on:selection={onAssetTypeSelection}>
|
|
||||||
{#if dirtyAssetType}
|
|
||||||
<div class="editorContainer">
|
|
||||||
<div style="grid-column: 1/span 1">
|
|
||||||
<TextField type="text" style="width: 100%" bind:value={dirtyAssetType.name} label="Name">
|
|
||||||
<HelperText slot="helper">Provide a unique name for the asset type.</HelperText>
|
|
||||||
</TextField>
|
|
||||||
<TextField type="text" style="width: 100%" bind:value={dirtyAssetType.description} label="Description">
|
|
||||||
<HelperText slot="helper">A detailed description.</HelperText>
|
|
||||||
</TextField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" style="grid-column: 2/span 1;" class="button accept-button material-icons material-symbols-outlined" on:click={applyAssetTypeChanges}>check</button>
|
|
||||||
<button type="button" style="grid-column: 3/span 1;" class="button reject-button material-icons material-symbols-outlined" on:click={rejectAssetTypeChanges}>close</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</GridTable>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
form {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.editorContainer {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(10px, 1fr) minmax(3rem, 3rem) minmax(3rem, 3rem);
|
|
||||||
}
|
|
||||||
.accept-button, .reject-button {
|
|
||||||
font-size: .8rem;
|
|
||||||
padding: .6rem;
|
|
||||||
margin: auto;
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 0;
|
|
||||||
font-weight: 800;
|
|
||||||
alignment: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.accept-button {
|
|
||||||
background-color: rgba(61, 148, 61, 0.91);
|
|
||||||
}
|
|
||||||
.reject-button {
|
|
||||||
background-color: rgba(176, 64, 64, 0.61);
|
|
||||||
}
|
|
||||||
.accept-button:hover {
|
|
||||||
background-color: rgb(61, 148, 61);
|
|
||||||
}
|
|
||||||
.reject-button:hover {
|
|
||||||
background-color: rgb(176, 64, 64);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
124
imports/ui/Admin/AssetTypes.svelte
Normal file
124
imports/ui/Admin/AssetTypes.svelte
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<script>
|
||||||
|
import {Meteor} from "meteor/meteor";
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
import GridTable from "./../GridTable.svelte";
|
||||||
|
import {writable} from "svelte/store";
|
||||||
|
import TextField from '@smui/textfield';
|
||||||
|
import HelperText from '@smui/textfield/helper-text';
|
||||||
|
import {AssetTypes} from "../../api/asset-types";
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
Meteor.subscribe('assetTypes');
|
||||||
|
});
|
||||||
|
|
||||||
|
const assetTypesColumns = [
|
||||||
|
{
|
||||||
|
key: "_id",
|
||||||
|
title: "ID",
|
||||||
|
value: v => v._id,
|
||||||
|
minWidth: 20,
|
||||||
|
weight: 1,
|
||||||
|
cls: "id",
|
||||||
|
}, {
|
||||||
|
key: "name",
|
||||||
|
title: "Name",
|
||||||
|
value: v => v.name,
|
||||||
|
minWidth: 100,
|
||||||
|
weight: 1,
|
||||||
|
cls: "name",
|
||||||
|
}, {
|
||||||
|
key: "description",
|
||||||
|
title: "Description",
|
||||||
|
value: v => v.description,
|
||||||
|
minWidth: 100,
|
||||||
|
weight: 1,
|
||||||
|
cls: "description",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const assetTypesActions = {
|
||||||
|
title: "Actions",
|
||||||
|
headerWidgets: [
|
||||||
|
{icon: "add_box", action: () => {editedAssetType.set({});}, tooltip: "Add a new asset type."}
|
||||||
|
],
|
||||||
|
rowWidgets: [
|
||||||
|
{icon: "add_circle", action: (v) => {editedAssetType.set(v)}},
|
||||||
|
{icon: "delete", action: (v) => {deleteAssetType(v)}}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let editedAssetType = writable(null);
|
||||||
|
const onAssetTypeSelection = (e) => {
|
||||||
|
}
|
||||||
|
let dirtyAssetType;
|
||||||
|
// Copy the edited value when ever it changes, set some defaults for a new value object (to make the view happy).
|
||||||
|
editedAssetType.subscribe(v => {dirtyAssetType = Object.assign({name: "", description: ""}, v)});
|
||||||
|
// Load the sites (reactive).
|
||||||
|
let assetTypes = AssetTypes.find({});
|
||||||
|
const deleteAssetType = assetType => {
|
||||||
|
//TODO:
|
||||||
|
};
|
||||||
|
const applyAssetTypeChanges = () => {
|
||||||
|
if(dirtyAssetType._id)
|
||||||
|
Meteor.call("assetTypes.update", dirtyAssetType._id, dirtyAssetType.name, dirtyAssetType.description);
|
||||||
|
else
|
||||||
|
Meteor.call("assetTypes.add", dirtyAssetType.name, dirtyAssetType.description);
|
||||||
|
editedAssetType.set(null);
|
||||||
|
dirtyAssetType = null;
|
||||||
|
}
|
||||||
|
const rejectAssetTypeChanges = () => {
|
||||||
|
editedAssetType.set(null);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>Asset Types</h2>
|
||||||
|
<GridTable bind:rows={assetTypes} columns="{assetTypesColumns}" actions="{assetTypesActions}" rowKey="{(v) => {return v._id}}" bind:edited="{editedAssetType}" on:selection={onAssetTypeSelection}>
|
||||||
|
{#if dirtyAssetType}
|
||||||
|
<div class="editorContainer">
|
||||||
|
<div style="grid-column: 1/span 1">
|
||||||
|
<TextField type="text" style="width: 100%" bind:value={dirtyAssetType.name} label="Name">
|
||||||
|
<HelperText slot="helper">Provide a unique name for the asset type.</HelperText>
|
||||||
|
</TextField>
|
||||||
|
<TextField type="text" style="width: 100%" bind:value={dirtyAssetType.description} label="Description">
|
||||||
|
<HelperText slot="helper">A detailed description.</HelperText>
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" style="grid-column: 2/span 1;" class="button accept-button material-icons material-symbols-outlined" on:click={applyAssetTypeChanges}>check</button>
|
||||||
|
<button type="button" style="grid-column: 3/span 1;" class="button reject-button material-icons material-symbols-outlined" on:click={rejectAssetTypeChanges}>close</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</GridTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.editorContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(10px, 1fr) minmax(3rem, 3rem) minmax(3rem, 3rem);
|
||||||
|
}
|
||||||
|
.accept-button, .reject-button {
|
||||||
|
font-size: .8rem;
|
||||||
|
padding: .6rem;
|
||||||
|
margin: auto;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0;
|
||||||
|
font-weight: 800;
|
||||||
|
alignment: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.accept-button {
|
||||||
|
background-color: rgba(61, 148, 61, 0.91);
|
||||||
|
}
|
||||||
|
.reject-button {
|
||||||
|
background-color: rgba(176, 64, 64, 0.61);
|
||||||
|
}
|
||||||
|
.accept-button:hover {
|
||||||
|
background-color: rgb(61, 148, 61);
|
||||||
|
}
|
||||||
|
.reject-button:hover {
|
||||||
|
background-color: rgb(176, 64, 64);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
117
imports/ui/Admin/Sites.svelte
Normal file
117
imports/ui/Admin/Sites.svelte
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<script>
|
||||||
|
import {Meteor} from "meteor/meteor";
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
import {Sites} from "../../api/sites";
|
||||||
|
import GridTable from "./../GridTable.svelte";
|
||||||
|
import {writable} from "svelte/store";
|
||||||
|
import TextField from '@smui/textfield';
|
||||||
|
import HelperText from '@smui/textfield/helper-text';
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
Meteor.subscribe('sites');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not be needed now. Did not work well - had a bug somewhere.
|
||||||
|
// const fixRecords = () => {Meteor.call("admin.fixRecords");}
|
||||||
|
|
||||||
|
const siteColumns = [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
title: "Name",
|
||||||
|
value: v => v.name,
|
||||||
|
minWidth: 100,
|
||||||
|
weight: 1,
|
||||||
|
cls: "name",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const siteActions = {
|
||||||
|
title: "Actions",
|
||||||
|
headerWidgets: [
|
||||||
|
{icon: "add_box", action: () => {editedSite.set({name: ""});}, tooltip: "Add a new Site."}
|
||||||
|
],
|
||||||
|
rowWidgets: [
|
||||||
|
{icon: "add_circle", action: (v) => {editedSite.set(v)}},
|
||||||
|
{icon: "delete", action: (v) => {deleteSite(v)}}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const deleteSite = site => {
|
||||||
|
//TODO:
|
||||||
|
};
|
||||||
|
// Create a holder for the site being edited. This allows us to clear the editor when the user finishes, and allows the table or parent view to setup the editor.
|
||||||
|
let editedSite = writable(null);
|
||||||
|
let dirtySite;
|
||||||
|
// Copy the edited site when ever it changes, set some defaults for a new site object (to make the view happy).
|
||||||
|
editedSite.subscribe(site => {dirtySite = Object.assign({name:""}, site)});
|
||||||
|
// Load the sites (reactive).
|
||||||
|
let sites = Sites.find({});
|
||||||
|
const applySiteChanges = () => {
|
||||||
|
if(dirtySite._id)
|
||||||
|
Meteor.call("sites.update", dirtySite._id, dirtySite.name);
|
||||||
|
else
|
||||||
|
Meteor.call("sites.add", dirtySite.name);
|
||||||
|
editedSite.set(null);
|
||||||
|
dirtySite = null;
|
||||||
|
}
|
||||||
|
const rejectSiteChanges = () => {
|
||||||
|
editedSite.set(null);
|
||||||
|
}
|
||||||
|
let selectedSite = null;
|
||||||
|
const onSiteSelection = (e) => {
|
||||||
|
selectedSite = Sites.findOne({_id: e.detail});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>Sites</h2>
|
||||||
|
<GridTable bind:rows={sites} columns="{siteColumns}" actions="{siteActions}" rowKey="{(v) => {return v._id}}" bind:edited="{editedSite}" on:selection={onSiteSelection}>
|
||||||
|
{#if dirtySite}
|
||||||
|
<div class="editorContainer">
|
||||||
|
<div style="grid-column: 1/span 1">
|
||||||
|
<TextField type="text" style="width: 100%" bind:value={dirtySite.name} label="Name">
|
||||||
|
<HelperText slot="helper">Provide a unique name for the site.</HelperText>
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" style="grid-column: 2/span 1;" class="button accept-button material-icons material-symbols-outlined" on:click={applySiteChanges}>
|
||||||
|
check
|
||||||
|
</button>
|
||||||
|
<button type="button" style="grid-column: 3/span 1;" class="button reject-button material-icons material-symbols-outlined" on:click={rejectSiteChanges}>
|
||||||
|
close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</GridTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.editorContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(10px, 1fr) minmax(3rem, 3rem) minmax(3rem, 3rem);
|
||||||
|
}
|
||||||
|
.accept-button, .reject-button {
|
||||||
|
font-size: .8rem;
|
||||||
|
padding: .6rem;
|
||||||
|
margin: auto;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0;
|
||||||
|
font-weight: 800;
|
||||||
|
alignment: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.accept-button {
|
||||||
|
background-color: rgba(61, 148, 61, 0.91);
|
||||||
|
}
|
||||||
|
.reject-button {
|
||||||
|
background-color: rgba(176, 64, 64, 0.61);
|
||||||
|
}
|
||||||
|
.accept-button:hover {
|
||||||
|
background-color: rgb(61, 148, 61);
|
||||||
|
}
|
||||||
|
.reject-button:hover {
|
||||||
|
background-color: rgb(176, 64, 64);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
223
imports/ui/Admin/Staff.svelte
Normal file
223
imports/ui/Admin/Staff.svelte
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<script>
|
||||||
|
import {Meteor} from "meteor/meteor";
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
import {Sites} from "../../api/sites";
|
||||||
|
import GridTable from "./../GridTable.svelte";
|
||||||
|
import {writable} from "svelte/store";
|
||||||
|
import TextField from '@smui/textfield';
|
||||||
|
import HelperText from '@smui/textfield/helper-text';
|
||||||
|
import {Staff} from "../../api/staff";
|
||||||
|
import Select, { Option } from '@smui/select';
|
||||||
|
import Dialog, { Title, Content, Actions } from '@smui/dialog';
|
||||||
|
import Button, { Label } from '@smui/button';
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
Meteor.subscribe('sites');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the sites (reactive).
|
||||||
|
let sites = Sites.find({});
|
||||||
|
let selectedSiteId;
|
||||||
|
|
||||||
|
let staff = null;
|
||||||
|
let staffCount = 0;
|
||||||
|
$: {
|
||||||
|
if(selectedSiteId) {
|
||||||
|
Meteor.subscribe('staff', selectedSiteId);
|
||||||
|
staff = Staff.find({siteId: selectedSiteId});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: {
|
||||||
|
if(staff) {
|
||||||
|
staffCount = $staff.length;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
staffCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uploadType = 'CSV'
|
||||||
|
|
||||||
|
const uploadStaff = () => {
|
||||||
|
// console.log(files);
|
||||||
|
// console.log(selectedSite);
|
||||||
|
// console.log(selectedSite._id);
|
||||||
|
if(files && files.length) {
|
||||||
|
let file = files[0];
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
// console.log("Sending Data");
|
||||||
|
// console.log(selectedSite._id);
|
||||||
|
// console.log(reader.result);
|
||||||
|
Meteor.call('staff.loadCsv', reader.result, uploadType, selectedSiteId);
|
||||||
|
}
|
||||||
|
reader.readAsText(file, "UTF-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let files;
|
||||||
|
|
||||||
|
const staffColumns = [
|
||||||
|
{
|
||||||
|
key: "id",
|
||||||
|
title: "ID",
|
||||||
|
value: v => v.id,
|
||||||
|
minWidth: 100,
|
||||||
|
weight: 1,
|
||||||
|
cls: "id",
|
||||||
|
}, {
|
||||||
|
key: "email",
|
||||||
|
title: "Email",
|
||||||
|
value: v => v.email,
|
||||||
|
minWidth: 100,
|
||||||
|
weight: 1,
|
||||||
|
cls: "email",
|
||||||
|
}, {
|
||||||
|
key: "firstName",
|
||||||
|
title: "First Name",
|
||||||
|
value: v => v.firstName,
|
||||||
|
minWidth: 100,
|
||||||
|
weight: 1,
|
||||||
|
cls: "firstName",
|
||||||
|
}, {
|
||||||
|
key: "lastName",
|
||||||
|
title: "Last Name",
|
||||||
|
value: v => v.lastName,
|
||||||
|
minWidth: 100,
|
||||||
|
weight: 1,
|
||||||
|
cls: "lastName",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const staffActions = {
|
||||||
|
title: "Actions",
|
||||||
|
headerWidgets: [
|
||||||
|
{icon: "add_box", action: () => {editedStaff.set({});}, tooltip: "Add a staff member."}
|
||||||
|
],
|
||||||
|
rowWidgets: [
|
||||||
|
{icon: "add_circle", action: (v) => {editedStaff.set(v)}},
|
||||||
|
{icon: "delete", action: (v) => {deleteStaff(v)}}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let editedStaff = writable(null);
|
||||||
|
let siteSelectComponent;
|
||||||
|
const onStaffSelection = (e) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
let dirtyStaff;
|
||||||
|
// Copy the edited value when ever it changes, set some defaults for a new value object (to make the view happy).
|
||||||
|
editedStaff.subscribe(v => {dirtyStaff = Object.assign({email: "", firstName: "", lastName: ""}, v)});
|
||||||
|
const deleteStaff = staff => {
|
||||||
|
//TODO:
|
||||||
|
};
|
||||||
|
const applyStaffChanges = () => {
|
||||||
|
if(dirtyStaff._id)
|
||||||
|
Meteor.call("staff.update", dirtyStaff);
|
||||||
|
else
|
||||||
|
Meteor.call("staff.add", dirtyStaff);
|
||||||
|
editedStaff.set(null);
|
||||||
|
dirtyStaff = null;
|
||||||
|
}
|
||||||
|
const rejectStaffChanges = () => {
|
||||||
|
editedStaff.set(null);
|
||||||
|
}
|
||||||
|
let openExportHelpDialog = false;
|
||||||
|
const clickOpenExportHelpDialog = () => {
|
||||||
|
openExportHelpDialog = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<Select bind:value={selectedSiteId} label="Site" bind:this={siteSelectComponent}>
|
||||||
|
{#each $sites as site}
|
||||||
|
<Option value={site._id}>{site.name}</Option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<h2>Site Staff ({staffCount})</h2>
|
||||||
|
<form on:submit|preventDefault={uploadStaff}>
|
||||||
|
<input style="display: inline-block" type="file" multiple="false" accept="text/*" bind:files={files}/>
|
||||||
|
<select bind:value={uploadType}>
|
||||||
|
<option value="CSV">Comma Separated Value (CSV)</option>
|
||||||
|
<option value="Aeries Text Report">Aeries Text Report (TXT)</option>
|
||||||
|
</select>
|
||||||
|
<input disabled="{!selectedSiteId}" type="submit" value="Upload"/>
|
||||||
|
<input type="button" value="?" on:click={clickOpenExportHelpDialog}/>
|
||||||
|
</form>
|
||||||
|
<GridTable bind:rows={staff} columns="{staffColumns}" actions="{staffActions}" rowKey="{(v) => {return v._id}}" bind:edited="{editedStaff}" on:selection={onStaffSelection}>
|
||||||
|
{#if dirtyStaff}
|
||||||
|
<div class="editorContainer">
|
||||||
|
<div style="grid-column: 1/span 1">
|
||||||
|
<TextField type="text" style="width: 100%" bind:value={dirtyStaff.email} label="Email">
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
<div style="grid-column: 1/span 1">
|
||||||
|
<TextField type="text" style="width: 100%" bind:value={dirtyStaff.firstName} label="First Name">
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
<div style="grid-column: 1/span 1">
|
||||||
|
<TextField type="text" style="width: 100%" bind:value={dirtyStaff.lastName} label="Last Name">
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" style="grid-column: 2/span 1;" class="button accept-button material-icons material-symbols-outlined" on:click={applyStaffChanges}>
|
||||||
|
check
|
||||||
|
</button>
|
||||||
|
<button type="button" style="grid-column: 3/span 1;" class="button reject-button material-icons material-symbols-outlined" on:click={rejectStaffChanges}>
|
||||||
|
close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</GridTable>
|
||||||
|
<Dialog bind:open={openExportHelpDialog} aria-labelledby="exportHelpTitle" aria-describedby="exportHelpContent" surface$style="width: 800px; max-width: calc(100vw - 32px);">
|
||||||
|
<Title id="exportHelpTitle">Exporting Staff Data</Title>
|
||||||
|
<Content id="exportHelpContent">
|
||||||
|
<h3>Aeries</h3>
|
||||||
|
<p>For the Aeries system, log into your Aeries web interface and navigate to the query page. Enter the following query (change PSC = 5 to your school's number):</p>
|
||||||
|
<pre style="font-size: 0.7rem"><code>LIST STF ID FN LN EM BY FN IF PSC = 5 AND TG # I</code></pre>
|
||||||
|
<p>Run the query and validate that all staff have an email address and a staff ID. The `TG # I` hides staff that are inactive.</p>
|
||||||
|
<p>You have two options for export. You can:</p>
|
||||||
|
<ol class="help">
|
||||||
|
<li class="help">Click the `Report` button to generate a TXT formatted report. Use Single Spacing, Automatic Orientation and no page breaks. The generated text is CSV but with a repeating collection of headers for each page. The headers will be ignored.</li>
|
||||||
|
<li class="help">Click the `Excel` button to generate an excel spreadsheet of the results. Open this with any spreadsheet software (Excel, LibreOffice Calc, or Google Sheets) and save as CSV. The first line in the CSV file is assumed to be a header and will be ignored.</li>
|
||||||
|
</ol>
|
||||||
|
<p>Now upload the generated CSV file (or text file).</p>
|
||||||
|
</Content>
|
||||||
|
<Actions>
|
||||||
|
<Button action="accept">
|
||||||
|
<Label>Done</Label>
|
||||||
|
</Button>
|
||||||
|
</Actions>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.editorContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(10px, 1fr) minmax(3rem, 3rem) minmax(3rem, 3rem);
|
||||||
|
}
|
||||||
|
.accept-button, .reject-button {
|
||||||
|
font-size: .8rem;
|
||||||
|
padding: .6rem;
|
||||||
|
margin: auto;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0;
|
||||||
|
font-weight: 800;
|
||||||
|
alignment: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.accept-button {
|
||||||
|
background-color: rgba(61, 148, 61, 0.91);
|
||||||
|
}
|
||||||
|
.reject-button {
|
||||||
|
background-color: rgba(176, 64, 64, 0.61);
|
||||||
|
}
|
||||||
|
.accept-button:hover {
|
||||||
|
background-color: rgb(61, 148, 61);
|
||||||
|
}
|
||||||
|
.reject-button:hover {
|
||||||
|
background-color: rgb(176, 64, 64);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
236
imports/ui/Admin/Students.svelte
Normal file
236
imports/ui/Admin/Students.svelte
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
<script>
|
||||||
|
import {Meteor} from "meteor/meteor";
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
import {Sites} from "../../api/sites";
|
||||||
|
import GridTable from "./../GridTable.svelte";
|
||||||
|
import {writable} from "svelte/store";
|
||||||
|
import TextField from '@smui/textfield';
|
||||||
|
import HelperText from '@smui/textfield/helper-text';
|
||||||
|
import {Students} from "../../api/students";
|
||||||
|
import Select, { Option } from '@smui/select';
|
||||||
|
import Dialog, { Title, Content, Actions } from '@smui/dialog';
|
||||||
|
import Button, { Label } from '@smui/button';
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
Meteor.subscribe('sites');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the sites (reactive).
|
||||||
|
let sites = Sites.find({});
|
||||||
|
let selectedSiteId;
|
||||||
|
|
||||||
|
let students;
|
||||||
|
let studentCount = 0;
|
||||||
|
$: {
|
||||||
|
if(selectedSiteId) {
|
||||||
|
Meteor.subscribe('students', selectedSiteId);
|
||||||
|
students = Students.find({siteId: selectedSiteId});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: {
|
||||||
|
if(students) {
|
||||||
|
studentCount = $students.length;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
studentCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uploadType = 'CSV'
|
||||||
|
|
||||||
|
const uploadStudents = () => {
|
||||||
|
// console.log(files);
|
||||||
|
// console.log(selectedSite);
|
||||||
|
// console.log(selectedSite._id);
|
||||||
|
if(files && files.length) {
|
||||||
|
let file = files[0];
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
// console.log("Sending Data");
|
||||||
|
// console.log(selectedSite._id);
|
||||||
|
// console.log(reader.result);
|
||||||
|
Meteor.call('students.loadCsv', reader.result, uploadType, selectedSiteId);
|
||||||
|
}
|
||||||
|
reader.readAsText(file, "UTF-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let files;
|
||||||
|
|
||||||
|
const studentColumns = [
|
||||||
|
{
|
||||||
|
key: "email",
|
||||||
|
title: "Email",
|
||||||
|
value: v => v.email,
|
||||||
|
minWidth: 100,
|
||||||
|
weight: 1,
|
||||||
|
cls: "email",
|
||||||
|
}, {
|
||||||
|
key: "firstName",
|
||||||
|
title: "First Name",
|
||||||
|
value: v => v.firstName,
|
||||||
|
minWidth: 100,
|
||||||
|
weight: 1,
|
||||||
|
cls: "firstName",
|
||||||
|
}, {
|
||||||
|
key: "lastName",
|
||||||
|
title: "Last Name",
|
||||||
|
value: v => v.lastName,
|
||||||
|
minWidth: 100,
|
||||||
|
weight: 1,
|
||||||
|
cls: "lastName",
|
||||||
|
}, {
|
||||||
|
key: "grade",
|
||||||
|
title: "Grade",
|
||||||
|
value: v => v.grade,
|
||||||
|
minWidth: 100,
|
||||||
|
weight: 1,
|
||||||
|
cls: "grade",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const studentsActions = {
|
||||||
|
title: "Actions",
|
||||||
|
headerWidgets: [
|
||||||
|
{icon: "add_box", action: () => {editedStudent.set({});}, tooltip: "Add a student."}
|
||||||
|
],
|
||||||
|
rowWidgets: [
|
||||||
|
{icon: "add_circle", action: (v) => {editedStudent.set(v)}},
|
||||||
|
{icon: "delete", action: (v) => {deleteStudent(v)}}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let siteSelectComponent;
|
||||||
|
let editedStudent = writable(null);
|
||||||
|
const onStudentSelection = (e) => {
|
||||||
|
}
|
||||||
|
let dirtyStudent;
|
||||||
|
// Copy the edited value when ever it changes, set some defaults for a new value object (to make the view happy).
|
||||||
|
editedStudent.subscribe(v => {dirtyStudent = Object.assign({email: "", firstName: "", lastName: "", grade: ""}, v)});
|
||||||
|
const deleteStudent = student => {
|
||||||
|
//TODO:
|
||||||
|
};
|
||||||
|
const applyStudentChanges = () => {
|
||||||
|
if(dirtyStudent._id)
|
||||||
|
Meteor.call("students.update", dirtyStudent);
|
||||||
|
else
|
||||||
|
Meteor.call("students.add", dirtyStudent);
|
||||||
|
editedStudent.set(null);
|
||||||
|
dirtyStudent = null;
|
||||||
|
}
|
||||||
|
const rejectStudentChanges = () => {
|
||||||
|
editedStudent.set(null);
|
||||||
|
}
|
||||||
|
let openExportHelpDialog = false;
|
||||||
|
const clickOpenExportHelpDialog = () => {
|
||||||
|
openExportHelpDialog = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<Select bind:value={selectedSiteId} label="Site" bind:this={siteSelectComponent}>
|
||||||
|
{#each $sites as site}
|
||||||
|
<Option value={site._id}>{site.name}</Option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
<h2>Site Students ({studentCount})</h2>
|
||||||
|
<form on:submit|preventDefault={uploadStudents}>
|
||||||
|
<input style="display: inline-block" type="file" multiple="false" accept="text/*" bind:files={files}/>
|
||||||
|
<select bind:value={uploadType}>
|
||||||
|
<option value="CSV">Comma Separated Value (CSV)</option>
|
||||||
|
<option value="Aeries Text Report">Aeries Text Report (TXT)</option>
|
||||||
|
</select>
|
||||||
|
<input disabled="{!selectedSiteId}" type="submit" value="Upload"/>
|
||||||
|
<input type="button" value="?" on:click={clickOpenExportHelpDialog}/>
|
||||||
|
</form>
|
||||||
|
<GridTable bind:rows={students} columns="{studentColumns}" actions="{studentsActions}" rowKey="{(v) => {return v._id}}" bind:edited="{editedStudent}" on:selection={onStudentSelection}>
|
||||||
|
{#if dirtyStudent}
|
||||||
|
<div class="editorContainer">
|
||||||
|
<div style="grid-column: 1/span 1">
|
||||||
|
<TextField type="text" style="width: 100%" bind:value={dirtyStudent.email} label="Email">
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
<div style="grid-column: 1/span 1">
|
||||||
|
<TextField type="text" style="width: 100%" bind:value={dirtyStudent.firstName} label="First Name">
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
<div style="grid-column: 1/span 1">
|
||||||
|
<TextField type="text" style="width: 100%" bind:value={dirtyStudent.lastName} label="Last Name">
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
<div style="grid-column: 1/span 1">
|
||||||
|
<TextField type="text" style="width: 100%" bind:value={dirtyStudent.grade} label="Grade">
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" style="grid-column: 2/span 1;" class="button accept-button material-icons material-symbols-outlined" on:click={applyStudentChanges}>
|
||||||
|
check
|
||||||
|
</button>
|
||||||
|
<button type="button" style="grid-column: 3/span 1;" class="button reject-button material-icons material-symbols-outlined" on:click={rejectStudentChanges}>
|
||||||
|
close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</GridTable>
|
||||||
|
|
||||||
|
<Dialog bind:open={openExportHelpDialog} aria-labelledby="exportHelpTitle" aria-describedby="exportHelpContent" surface$style="width: 800px; max-width: calc(100vw - 32px);">
|
||||||
|
<Title id="exportHelpTitle">Exporting Student Data</Title>
|
||||||
|
<Content id="exportHelpContent">
|
||||||
|
<h3>Aeries</h3>
|
||||||
|
<p>For the Aeries system, log into your Aeries web interface and navigate to the query page. Enter the following query:</p>
|
||||||
|
<pre style="font-size: 0.7rem"><code>LIST STU STU.ID STU.SEM STU.FN STU.LN STU.NG BY STU.ID STU.NG STU.SEM IF STU.NG >= 7 AND NG <= 12 AND STU.NS = 5</code></pre>
|
||||||
|
<p>Run the query and validate that all students have an email address and a student ID. You likely also want to check that the student `next grade (NG)` field is set correctly for the students.</p>
|
||||||
|
<p>You have two options for export. You can:</p>
|
||||||
|
<ol class="help">
|
||||||
|
<li class="help">Click the `Report` button to generate a TXT formatted report. Use Single Spacing, Automatic Orientation and no page breaks. The generated text is CSV but with a repeating collection of headers for each page. The headers will be ignored.</li>
|
||||||
|
<li class="help">Click the `Excel` button to generate an excel spreadsheet of the results. Open this with any spreadsheet software (Excel, LibreOffice Calc, or Google Sheets) and save as CSV. The first line in the CSV file is assumed to be a header and will be ignored.</li>
|
||||||
|
</ol>
|
||||||
|
<p>Now upload the generated CSV file (or text file).</p>
|
||||||
|
</Content>
|
||||||
|
<Actions>
|
||||||
|
<Button action="accept">
|
||||||
|
<Label>Done</Label>
|
||||||
|
</Button>
|
||||||
|
</Actions>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
ol.help {
|
||||||
|
list-style-type: decimal;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
li.help {
|
||||||
|
list-style: inherit;
|
||||||
|
padding-top: 0.2rem;
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
.editorContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(10px, 1fr) minmax(3rem, 3rem) minmax(3rem, 3rem);
|
||||||
|
}
|
||||||
|
.accept-button, .reject-button {
|
||||||
|
font-size: .8rem;
|
||||||
|
padding: .6rem;
|
||||||
|
margin: auto;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0;
|
||||||
|
font-weight: 800;
|
||||||
|
alignment: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.accept-button {
|
||||||
|
background-color: rgba(61, 148, 61, 0.91);
|
||||||
|
}
|
||||||
|
.reject-button {
|
||||||
|
background-color: rgba(176, 64, 64, 0.61);
|
||||||
|
}
|
||||||
|
.accept-button:hover {
|
||||||
|
background-color: rgb(61, 148, 61);
|
||||||
|
}
|
||||||
|
.reject-button:hover {
|
||||||
|
background-color: rgb(176, 64, 64);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,51 +1,4 @@
|
|||||||
|
|
||||||
<style>
|
|
||||||
a.button {
|
|
||||||
background: #7171ec;
|
|
||||||
border-radius: 999px;
|
|
||||||
box-shadow: #5E5DF0 0 10px 20px -10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: #FFFFFF;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: Inter,Helvetica,"Apple Color Emoji","Segoe UI Emoji",NotoColorEmoji,"Noto Color Emoji","Segoe UI Symbol","Android Emoji",EmojiSymbols,-apple-system,system-ui,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 24px;
|
|
||||||
opacity: 1;
|
|
||||||
outline: 0 solid transparent;
|
|
||||||
padding: 8px 18px;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
touch-action: manipulation;
|
|
||||||
width: fit-content;
|
|
||||||
word-break: break-word;
|
|
||||||
border: 0;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
--background: #fff;
|
|
||||||
--border-default: #D0D0DF;
|
|
||||||
--border-active: #3D6DF9;
|
|
||||||
/*--shadow-default: #{rgba(#202048, .12)};*/
|
|
||||||
/*--shadow-active: #{rgba(#3D6DF9, .25)};*/
|
|
||||||
--text-color: #818190;
|
|
||||||
--placeholder-color: #C9C9D9;
|
|
||||||
--placeholder-color-hover: #BABAC9;
|
|
||||||
--close: #818190;
|
|
||||||
--close-light: #BABAC9;
|
|
||||||
--close-background: #F1F1FA;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 240px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: inset 0 0 0 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.options {
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {Route, router, meta} from 'tinro';
|
import {Route, router, meta} from 'tinro';
|
||||||
import ChromebookScan from './ChromebookScan.svelte';
|
import ChromebookScan from './ChromebookScan.svelte';
|
||||||
@@ -55,25 +8,35 @@
|
|||||||
import Icon from '@smui/textfield/icon';
|
import Icon from '@smui/textfield/icon';
|
||||||
import { Icon as CommonIcon } from '@smui/common';
|
import { Icon as CommonIcon } from '@smui/common';
|
||||||
import DateInput from "./DateInput.svelte";
|
import DateInput from "./DateInput.svelte";
|
||||||
|
import Button, { Label } from '@smui/button';
|
||||||
|
import IconButton from '@smui/icon-button';
|
||||||
|
|
||||||
let serialInput = null;
|
let serialInput = null;
|
||||||
let emailInput = null;
|
let emailInput = null;
|
||||||
let dateInput = null;
|
let dateInput = null;
|
||||||
|
let assetIdInput = null;
|
||||||
|
|
||||||
function serialSearch() {
|
function serialSearch() {
|
||||||
router.goto("/chromebooks?serial=" + encodeURIComponent(serialInput) + "®ex=true");
|
router.goto("/chromebooks?serial=" + encodeURIComponent(serialInput) + "®ex=true");
|
||||||
}
|
}
|
||||||
|
function assetIdSearch() {
|
||||||
|
router.goto("/chromebooks?assetId=" + encodeURIComponent(assetIdInput) + "®ex=true");
|
||||||
|
}
|
||||||
function emailSearch() {
|
function emailSearch() {
|
||||||
router.goto("/chromebooks?email=" + encodeURIComponent(emailInput) + "®ex=true");
|
router.goto("/chromebooks?email=" + encodeURIComponent(emailInput) + "®ex=true");
|
||||||
}
|
}
|
||||||
function dateSearch() {
|
function dateSearch() {
|
||||||
console.log("Date Search")
|
// console.log("Date Search")
|
||||||
console.log(dateInput);
|
// console.log(dateInput);
|
||||||
|
|
||||||
if(dateInput) {
|
if(dateInput) {
|
||||||
console.log(dateInput instanceof Date)
|
if(typeof dateInput === 'string' || dateInput instanceof String) {
|
||||||
|
let parts = dateInput.split('-');
|
||||||
|
dateInput = new Date(parts[0], parts[1]-1, parts[2]);
|
||||||
|
}
|
||||||
|
// console.log(dateInput instanceof Date)
|
||||||
//console.log(!isNaN(date.valueOf()));
|
//console.log(!isNaN(date.valueOf()));
|
||||||
console.log(dateInput.getTime())
|
// console.log(dateInput.getTime())
|
||||||
}
|
}
|
||||||
if(dateInput && dateInput instanceof Date)
|
if(dateInput && dateInput instanceof Date)
|
||||||
router.goto("/chromebooks?date=" + encodeURIComponent(dateInput.getTime()));
|
router.goto("/chromebooks?date=" + encodeURIComponent(dateInput.getTime()));
|
||||||
@@ -102,18 +65,21 @@
|
|||||||
|
|
||||||
let deviceId = null;
|
let deviceId = null;
|
||||||
let serial = null;
|
let serial = null;
|
||||||
|
let assetId = null;
|
||||||
let email = null;
|
let email = null;
|
||||||
let date = null;
|
let date = null;
|
||||||
let regex = false;
|
let regex = false;
|
||||||
$: router.subscribe(query => {
|
$: router.subscribe(query => {
|
||||||
deviceId = router.location.query.get("deviceId");
|
deviceId = router.location.query.get("deviceId");
|
||||||
serial = router.location.query.get("serial");
|
serial = router.location.query.get("serial");
|
||||||
|
assetId = router.location.query.get('assetId');
|
||||||
email = router.location.query.get("email");
|
email = router.location.query.get("email");
|
||||||
regex = router.location.query.get("regex");
|
regex = router.location.query.get("regex");
|
||||||
date = router.location.query.get("date");
|
date = router.location.query.get("date");
|
||||||
|
|
||||||
if(deviceId) deviceId = decodeURIComponent(deviceId);
|
if(deviceId) deviceId = decodeURIComponent(deviceId);
|
||||||
if(serial) serial = decodeURIComponent(serial);
|
if(serial) serial = decodeURIComponent(serial);
|
||||||
|
if(assetId) assetId = decodeURIComponent(assetId);
|
||||||
if(email) email = decodeURIComponent(email);
|
if(email) email = decodeURIComponent(email);
|
||||||
if(date) date = decodeURIComponent(date);
|
if(date) date = decodeURIComponent(date);
|
||||||
if(regex) regex = true;
|
if(regex) regex = true;
|
||||||
@@ -121,6 +87,7 @@
|
|||||||
// console.log("Query:");
|
// console.log("Query:");
|
||||||
// console.log(deviceId);
|
// console.log(deviceId);
|
||||||
// console.log(serial);
|
// console.log(serial);
|
||||||
|
// console.log(assetId);
|
||||||
// console.log(email);
|
// console.log(email);
|
||||||
// console.log(date);
|
// console.log(date);
|
||||||
});
|
});
|
||||||
@@ -131,6 +98,7 @@
|
|||||||
|
|
||||||
if(deviceId) params.deviceId = deviceId;
|
if(deviceId) params.deviceId = deviceId;
|
||||||
else if(serial) params.serial = serial;
|
else if(serial) params.serial = serial;
|
||||||
|
else if(assetId) params.assetId = assetId;
|
||||||
else if(email) params.email = email;
|
else if(email) params.email = email;
|
||||||
else if(date) params.date = parseInt(date, 10);
|
else if(date) params.date = parseInt(date, 10);
|
||||||
|
|
||||||
@@ -176,30 +144,53 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<TextField type="text" bind:value={emailInput} on:keypress={(e) => {if(e.keyCode === 13) emailSearch()}} label="Email">
|
<TextField type="text" bind:value={emailInput} on:keypress={(e) => {if(e.keyCode === 13) emailSearch()}} label="Email">
|
||||||
<svelte:fragment slot="label">
|
<!-- <svelte:fragment slot="label">-->
|
||||||
<CommonIcon class="material-icons" style="font-size: 1em; line-height: normal; vertical-align: top;">email</CommonIcon> Email
|
<!-- <CommonIcon class="material-icons" style="font-size: 1em; line-height: normal; vertical-align: top;">email</CommonIcon> Email-->
|
||||||
</svelte:fragment>
|
<!-- </svelte:fragment>-->
|
||||||
<Icon class="material-icons" slot="trailingIcon">search</Icon>
|
<IconButton slot="trailingIcon" class="material-icons" on:click={emailSearch}>search</IconButton>
|
||||||
<HelperText slot="helper">Type any part of an email address.</HelperText>
|
<HelperText slot="helper">Type any part of an email address.</HelperText>
|
||||||
</TextField>
|
</TextField>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li style="display: flex; flex-direction: row; flex-wrap: wrap; justify-content: flex-start; align-items: center; align-content: flex-start; gap: 2rem;">
|
||||||
By Chromebook Device ID: <a href="/chromebooks/scan" className="button">Scan</a>
|
<div style="display: inline-block;">
|
||||||
|
<TextField type="text" bind:value={serialInput} on:keypress={(e) => {if(e.keyCode === 13) serialSearch()}} label="Serial Number">
|
||||||
|
<IconButton slot="trailingIcon" class="material-icons" on:click={serialSearch}>search</IconButton>
|
||||||
|
<HelperText slot="helper">Type any part of a device serial number.</HelperText>
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
<div style="display: inline-block">
|
||||||
|
or
|
||||||
|
<Button style="margin-left: 2rem;" color="secondary" touch variant="raised" on:click={() => {document.location.href="/chromebooks/scan"}}>
|
||||||
|
<Label style="color: white">Scan</Label>
|
||||||
|
<Icon style="color: white" class="material-icons">qr_code_scanner</Icon>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
By Chromebook Serial Number: <br/>
|
<TextField type="text" bind:value={assetIdInput} on:keypress={(e) => {if(e.keyCode === 13) assetIdSearch()}} label="Asset ID">
|
||||||
<input type="text" bind:value="{serialInput}" placeholder="Serial Number"/><br/>
|
<IconButton slot="trailingIcon" class="material-icons" on:click={assetIdSearch}>search</IconButton>
|
||||||
<button type="button" role="button" on:click={serialSearch}>Search</button>
|
<HelperText slot="helper">Type any part of an Asset ID.</HelperText>
|
||||||
|
</TextField>
|
||||||
</li>
|
</li>
|
||||||
|
<!-- <li>-->
|
||||||
|
<!-- By Chromebook Serial Number: <br/>-->
|
||||||
|
<!-- <input type="text" bind:value="{serialInput}" placeholder="Serial Number"/><br/>-->
|
||||||
|
<!-- <button type="button" role="button" on:click={serialSearch}>Search</button>-->
|
||||||
|
<!-- </li>-->
|
||||||
|
<!-- <li>-->
|
||||||
|
<!-- By Email Address: <br/>-->
|
||||||
|
<!-- <input type="text" bind:value="{emailInput}" placeholder="Email"/>@avpanthers.org<br/>-->
|
||||||
|
<!-- <button type="button" role="button" on:click={emailSearch}>Search</button>-->
|
||||||
|
<!-- </li>-->
|
||||||
<li>
|
<li>
|
||||||
By Email Address: <br/>
|
<TextField type="date" bind:value={dateInput} on:keypress={(e) => {if(e.keyCode === 13) dateSearch()}} label="Date Search">
|
||||||
<input type="text" bind:value="{emailInput}" placeholder="Email"/>@avpanthers.org<br/>
|
<IconButton slot="trailingIcon" class="material-icons" on:click={dateSearch}>search</IconButton>
|
||||||
<button type="button" role="button" on:click={emailSearch}>Search</button>
|
<HelperText slot="helper">Find records since...</HelperText>
|
||||||
</li>
|
</TextField>
|
||||||
<li>
|
|
||||||
All records since: <br/>
|
<!-- All records since: <br/>-->
|
||||||
<DateInput bind:date={dateInput}/>
|
<!-- <DateInput bind:date={dateInput}/>-->
|
||||||
<button type="button" role="button" on:click={dateSearch}>Search</button>
|
<!-- <button type="button" role="button" on:click={dateSearch}>Search</button>-->
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,5 +216,51 @@
|
|||||||
<Route path="/scan">
|
<Route path="/scan">
|
||||||
<ChromebookScan/>
|
<ChromebookScan/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/byStudent">TODO: Use student email to look up Chromebook history</Route>
|
|
||||||
<Route path="/byChromebook">TODO: Use chromebook ID (or picture of ID) to look up Chromebook history</Route>
|
|
||||||
|
<style>
|
||||||
|
a.button {
|
||||||
|
background: #7171ec;
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow: #5E5DF0 0 10px 20px -10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #FFFFFF;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: Inter,Helvetica,"Apple Color Emoji","Segoe UI Emoji",NotoColorEmoji,"Noto Color Emoji","Segoe UI Symbol","Android Emoji",EmojiSymbols,-apple-system,system-ui,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 24px;
|
||||||
|
opacity: 1;
|
||||||
|
outline: 0 solid transparent;
|
||||||
|
padding: 8px 18px;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
touch-action: manipulation;
|
||||||
|
width: fit-content;
|
||||||
|
word-break: break-word;
|
||||||
|
border: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
--background: #fff;
|
||||||
|
--border-default: #D0D0DF;
|
||||||
|
--border-active: #3D6DF9;
|
||||||
|
/*--shadow-default: #{rgba(#202048, .12)};*/
|
||||||
|
/*--shadow-active: #{rgba(#3D6DF9, .25)};*/
|
||||||
|
--text-color: #818190;
|
||||||
|
--placeholder-color: #C9C9D9;
|
||||||
|
--placeholder-color-hover: #BABAC9;
|
||||||
|
--close: #818190;
|
||||||
|
--close-light: #BABAC9;
|
||||||
|
--close-background: #F1F1FA;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 240px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: inset 0 0 0 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -130,21 +130,23 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each $rows as row (rowKey(row))}
|
{#if rows}
|
||||||
<tr data-key="{rowKey(row)}" class:hidden={row === $edited} on:mousedown={(e) => selectRow(e, row)} on:dblclick={(e) => {$edited = row}}>
|
{#each $rows as row (rowKey(row))}
|
||||||
{#each columns as column}
|
<tr data-key="{rowKey(row)}" class:hidden={row === $edited} on:mousedown={(e) => selectRow(e, row)} on:dblclick={(e) => {$edited = row}}>
|
||||||
{#if column.isActions}
|
{#each columns as column}
|
||||||
<td>
|
{#if column.isActions}
|
||||||
{#each column.rowWidgets as widget}
|
<td>
|
||||||
<span class="material-icons material-symbols-outlined" on:click={widget.action(row)}>{widget.icon}</span>
|
{#each column.rowWidgets as widget}
|
||||||
{/each}
|
<span class="material-icons material-symbols-outlined" on:click={widget.action(row)}>{widget.icon}</span>
|
||||||
</td>
|
{/each}
|
||||||
{:else}
|
</td>
|
||||||
<td>{column.value(row)}</td>
|
{:else}
|
||||||
{/if}
|
<td>{column.value(row)}</td>
|
||||||
{/each}
|
{/if}
|
||||||
</tr>
|
{/each}
|
||||||
{/each}
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<!--<button on:click={() => {$edited = null}} type="button">Stop Editing</button>-->
|
<!--<button on:click={() => {$edited = null}} type="button">Stop Editing</button>-->
|
||||||
|
|||||||
3794
package-lock.json
generated
3794
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.16.7",
|
"@babel/runtime": "^7.16.7",
|
||||||
"connect-route": "^0.1.5",
|
"connect-route": "^0.1.5",
|
||||||
|
"csv-parse": "^5.3.0",
|
||||||
"dayjs": "^1.11.3",
|
"dayjs": "^1.11.3",
|
||||||
"html5-qrcode": "^2.2.0",
|
"html5-qrcode": "^2.2.0",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@smui/common": "^6.0.0-beta.16",
|
"@smui/common": "^6.0.0-beta.16",
|
||||||
|
"@smui/icon-button": "^6.0.0",
|
||||||
"@smui/select": "^6.0.0-beta.16",
|
"@smui/select": "^6.0.0-beta.16",
|
||||||
"@smui/textfield": "^6.0.0-beta.16",
|
"@smui/textfield": "^6.0.0-beta.16",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user